Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b37fc98
feat(daemon): reconstruct managed session catalog
sethkarten Aug 13, 2026
8b21a40
fix(daemon): preserve family authority boundaries
sethkarten Aug 13, 2026
0508661
fix(daemon): keep authority fd accounting balanced when the artifacts…
snimu Aug 13, 2026
565b97e
fix(daemon): treat fork lineage as family roots instead of topology c…
snimu Aug 13, 2026
340b79c
perf(daemon): read only session headers and stat parent probes in the…
snimu Aug 13, 2026
218cead
perf(daemon): serve each family walk from one looped openat helper
snimu Aug 13, 2026
450a311
fix(daemon): validate registry children against what the daemon actua…
snimu Aug 13, 2026
e4b8f94
fix(daemon): harden catalog helper and child topology
sethkarten Aug 13, 2026
b729c66
fix(daemon): bind catalog helper responses to requests
sethkarten Aug 13, 2026
47929b9
fix(daemon): fail closed on catalog helper termination
sethkarten Aug 13, 2026
87a84da
fix(daemon): stream trusted catalog metadata
sethkarten Aug 13, 2026
8ade7b0
fix(daemon): cap catalog metadata and classify roots
sethkarten Aug 13, 2026
8c7ae5a
fix(daemon): strictly classify catalog root artifacts
sethkarten Aug 13, 2026
ad0e0cc
fix(daemon): bound catalog metadata envelopes
sethkarten Aug 13, 2026
5fcfd26
fix(daemon): preserve catalog protocol envelopes
sethkarten Aug 13, 2026
ec7bc5c
test(daemon): cover bounded catalog protocol envelopes
sethkarten Aug 13, 2026
070f497
fix(daemon): recognize escaped catalog claims
sethkarten Aug 13, 2026
f4283c6
fix(daemon): scan malformed catalog prefixes
sethkarten Aug 13, 2026
753d91c
fix: bound daemon catalog root discovery
sethkarten Aug 13, 2026
8d3f2d3
fix(daemon): bound catalog root enumeration
sethkarten Aug 13, 2026
731af01
fix(daemon): harden catalog limit handling
sethkarten Aug 13, 2026
4e2bbd2
fix(daemon): preserve exact catalog root inode identity
sethkarten Aug 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 45 additions & 36 deletions packages/coding-agent/src/core/agent-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,72 +257,81 @@ function sameAgentSessionNameParent(
if (left.depth === 0 && right.depth === 0) {
return true;
}
return sameAgentFamilyParent(left, right, catalog);
if (sameAgentFamilyParent(left, right, catalog)) return true;

// A passive child can outlive the active parent row that would normally
// resolve its family edge. Name reservation must still protect that parent's
// sibling namespace, but this weaker direct-claim fallback is deliberately
// not used for family reach: reach continues to require an unambiguous,
// catalog-resolved parent.
if (left.depth !== right.depth || left.depth === 0) return false;
return (
(left.parentSessionId !== undefined && left.parentSessionId === right.parentSessionId) ||
(left.parentSessionPath !== undefined &&
right.parentSessionPath !== undefined &&
canonicalSessionPath(left.parentSessionPath) === canonicalSessionPath(right.parentSessionPath))
);
}

function sameAgentFamilyParent(
left: AgentSessionNameScope,
right: AgentSessionNameScope,
catalog: readonly AgentFamilyCatalogEntry[],
): boolean {
if (left.parentSessionPath !== undefined && left.parentSessionPath === right.parentSessionPath) {
return true;
}
if (left.parentSessionId !== undefined && left.parentSessionId === right.parentSessionId) {
return true;
}
const hasCatalogParentPair = (parentSessionId: string | undefined, parentSessionPath: string | undefined) =>
parentSessionId !== undefined &&
parentSessionPath !== undefined &&
catalog.some(
(entry) =>
(entry.id === parentSessionId && entry.sessionPath === parentSessionPath) ||
(entry.parentSessionId === parentSessionId && entry.parentSessionPath === parentSessionPath),
if (left.depth === 0 && right.depth === 0) {
return (
left.parentSessionId === undefined &&
left.parentSessionPath === undefined &&
right.parentSessionId === undefined &&
right.parentSessionPath === undefined
);
if (
hasCatalogParentPair(left.parentSessionId, right.parentSessionPath) ||
hasCatalogParentPair(right.parentSessionId, left.parentSessionPath)
) {
return true;
}
if (
left.depth === 0 &&
right.depth === 0 &&
left.parentSessionPath === undefined &&
right.parentSessionPath === undefined &&
left.parentSessionId === undefined &&
right.parentSessionId === undefined
) {
return true;
}
// Unresolved mixed identifiers stay unrelated to avoid false name conflicts across families.
return false;
if (left.depth !== right.depth || left.depth === 0) return false;
const parentFor = (child: AgentSessionNameScope) => {
const parents = catalog.filter((entry) => isAgentFamilyParent(entry, child));
return parents.length === 1 ? parents[0] : undefined;
};
const leftParent = parentFor(left);
const rightParent = parentFor(right);
return leftParent !== undefined && leftParent.id === rightParent?.id;
}

function isAgentFamilyParent(parent: AgentFamilyCatalogEntry, child: AgentFamilyCatalogEntry): boolean {
/**
* Validates one persisted parent edge. A child may supply either durable
* identifier, but when it supplies both they must identify this same direct
* parent. This keeps contradictory records from becoming relatives through
* whichever identifier happens to match.
*/
function isAgentFamilyParent(parent: AgentFamilyCatalogEntry, child: AgentSessionNameScope): boolean {
if (child.depth <= 0 || parent.depth !== child.depth - 1) return false;
const claimsId = child.parentSessionId !== undefined;
const claimsPath = child.parentSessionPath !== undefined;
return (
(child.parentSessionPath !== undefined && child.parentSessionPath === parent.sessionPath) ||
(child.parentSessionId !== undefined && child.parentSessionId === parent.id)
(claimsId || claimsPath) &&
(!claimsId || child.parentSessionId === parent.id) &&
(!claimsPath || child.parentSessionPath === parent.sessionPath)
);
}

/** Pure nuclear-family policy over persisted parent-edge snapshots. */
export function agentFamilyRelationship(
current: AgentFamilyCatalogEntry,
target: AgentFamilyCatalogEntry,
catalog: readonly AgentFamilyCatalogEntry[] = [current, target],
): AgentFamilyRelationship | undefined {
if (current.id === target.id) return undefined;
if (isAgentFamilyParent(target, current)) return "parent";
if (isAgentFamilyParent(current, target)) return "child";
if (current.depth === target.depth && sameAgentFamilyParent(current, target, [current, target])) return "sibling";
if (sameAgentFamilyParent(current, target, catalog)) return "sibling";
return undefined;
}

export function assertAgentFamilyReach(
current: AgentFamilyCatalogEntry,
target: AgentFamilyCatalogEntry,
catalog?: readonly AgentFamilyCatalogEntry[],
): AgentFamilyRelationship {
const relationship = agentFamilyRelationship(current, target);
const relationship = agentFamilyRelationship(current, target, catalog);
if (!relationship) throw new Error(AGENT_FAMILY_REACH_ERROR);
return relationship;
}
Expand Down
74 changes: 71 additions & 3 deletions packages/coding-agent/src/core/session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1013,12 +1013,80 @@ export async function readSessionInfo(filePath: string): Promise<SessionInfo | n
if (cached && cached.size === stats.size && cached.mtimeMs === stats.mtimeMs) {
return cached.info;
}
const info = await scanSessionInfo(filePath, stats);
const info = await scanSessionInfo(filePath, stats, readLinesAsBuffers(filePath));
sessionInfoCache.set(filePath, { size: stats.size, mtimeMs: stats.mtimeMs, info });
return info;
}

async function scanSessionInfo(filePath: string, stats: Awaited<ReturnType<typeof stat>>): Promise<SessionInfo | null> {
/**
* Parse only descriptor-authorized session-header bytes. Unlike readSessionInfo,
* this deliberately never follows a legacy parent path or scans the body.
*/
export function readSessionHeaderInfoFromBuffer(
filePath: string,
contents: Buffer,
metadata: { mtimeMs: number },
): SessionInfo | null {
try {
const newline = contents.indexOf(0x0a);
const line = contents
.subarray(0, newline === -1 ? contents.length : newline)
.toString("utf8")
.trim();
const header = JSON.parse(line) as Partial<SessionHeader>;
if (
header.type !== "session" ||
typeof header.id !== "string" ||
header.id === "" ||
(typeof header.cwd !== "string" && header.cwd !== undefined) ||
(typeof header.parentSession !== "string" && header.parentSession !== undefined) ||
(header.rlmDepth !== undefined && !isValidRlmDepth(header.rlmDepth))
) {
return null;
}
return {
path: filePath,
id: header.id,
cwd: header.cwd ?? "",
...(header.parentSession !== undefined ? { parentSessionPath: header.parentSession } : {}),
rlmDepth: header.rlmDepth ?? 0,
created: new Date(typeof header.timestamp === "string" ? header.timestamp : 0),
modified: new Date(metadata.mtimeMs),
messageCount: 0,
firstMessage: "(no messages)",
allMessagesText: "",
};
} catch {
return null;
}
}

/** Parse catalog-authorized bytes without reopening their pathname. */
export async function readSessionInfoFromBuffer(
filePath: string,
contents: Buffer,
metadata: { mtimeMs: number },
): Promise<SessionInfo | null> {
async function* lines(): AsyncGenerator<Buffer> {
let start = 0;
while (start < contents.length) {
const end = contents.indexOf(0x0a, start);
if (end === -1) {
yield contents.subarray(start);
return;
}
yield contents.subarray(start, end);
start = end + 1;
}
}
return scanSessionInfo(filePath, { mtime: new Date(metadata.mtimeMs) } as Awaited<ReturnType<typeof stat>>, lines());
}

async function scanSessionInfo(
filePath: string,
stats: Awaited<ReturnType<typeof stat>>,
lines: AsyncIterable<Buffer>,
): Promise<SessionInfo | null> {
try {
let header: SessionHeader | undefined;
let messageCount = 0;
Expand All @@ -1029,7 +1097,7 @@ async function scanSessionInfo(filePath: string, stats: Awaited<ReturnType<typeo
let agentStatus: AgentStatus | undefined;
let lastActivityTime: number | undefined;

for await (const lineBuffer of readLinesAsBuffers(filePath)) {
for await (const lineBuffer of lines) {
const line = lineBuffer.toString("utf8");
if (!line.trim()) continue;

Expand Down
Loading