Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
94be9d7
fix(coding-agent): resolve v0.18.8 CI lifecycle regressions
Oct 8, 2026
2334d7d
test(sdk): isolate deadline recovery from worktree flush
Oct 8, 2026
92411fb
test(sdk): report deadline recovery stage on timeout
Oct 8, 2026
76c1ed1
test(sdk): trace deadline writes and await todo bridge
Oct 8, 2026
aa9f039
fix(async): retire stale queued generation in drain path
Oct 9, 2026
3d0f3d6
fix(async): preserve generation identity in queued cancellation
Oct 9, 2026
2124a4c
fix(async): consult stale tombstones in getJob for owned settlement
Oct 9, 2026
6c18008
fix(async): retire stale registrations using their admission endpoint
Oct 9, 2026
26ac155
test(async): add regression for owned registration retirement on stal…
Oct 9, 2026
4389b31
fix(async): preserve admission endpoint and retire stale queue entrie…
Oct 9, 2026
5d9d565
fix(async): retire stale queued generations independently of capacity
Oct 9, 2026
9da08e9
Merge origin/dev into pr-6512-rebase
Oct 9, 2026
f41b185
fix(async,tools,task): address three PR blockers
Oct 9, 2026
3e95148
fix: resolve Biome FORMAT errors in PR #6512
Oct 9, 2026
884749d
Merge current origin/dev
Oct 9, 2026
fafd3fe
fix(executor): restore authStorage instance mismatch validation
Oct 9, 2026
7c0a7ed
fix(sdk): wake deadline recovery immediately when tools drain
Oct 9, 2026
d1b63f0
style(sdk): biome format prompt-deadline-manager
Oct 9, 2026
0b99bb4
fix(sdk): separate terminal persistence chain to prevent head-of-line…
Oct 9, 2026
24e2f09
fix(sdk): revert separate terminal persistence chain that regressed s…
Oct 9, 2026
ed76dbf
test(sdk): instrument persistence chains for CI-only deadline hang
Oct 9, 2026
368a388
Merge origin/dev into PR branch
Oct 10, 2026
0821149
test(async): add regression test for stale queued generation cancella…
Oct 10, 2026
fd7702b
fix(async,sdk): preserve admission-endpoint ownership on valid queued…
Oct 10, 2026
d07362a
fix: format async/job-manager.ts per biome rules
Oct 10, 2026
6c03d77
fix(sdk): move persist instrumentation inside promise chain to preven…
Oct 10, 2026
2550e88
fix(format,test): format instrumentation calls and use safeRm in HOME…
Oct 10, 2026
8a88879
fix(ci): rebuild native addon to resolve RSS checkpoint measurement
Oct 10, 2026
20a22e1
fix: preserve admission endpoint in resumed job registration
Oct 10, 2026
cac68d8
docs(sdk): add changelog for deadline recovery tools-drain fix (#6508)
Oct 10, 2026
3d54a92
docs(changelog): use fragment section heading only
Yeachan-Heo Oct 10, 2026
8368c7b
fix: remove unused function parameters and fix formatting
Oct 10, 2026
e4c52f8
fix: prevent chain deadlock in reconciliation store transact operations
Oct 10, 2026
753e36e
fix: migrate queued resume registrations during manager rekey from E1…
Oct 10, 2026
0f074d8
fix: preserve admission endpoint during manager rekey for resumed job…
Oct 10, 2026
664fce8
Merge remote-tracking branch 'origin/dev' into fix/issue-6508
Oct 10, 2026
8f5efc7
instrumentation: add detailed lock/mutex tracking to persistence chain
Oct 10, 2026
4fc9154
test(sdk): log every reconciliation store chain op in the captured-ca…
gaebal-gajae Oct 10, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Fixes

- **SDK deadline recovery now immediately retries when pending tools drain (#6508)**. The deadline recovery loop was using a 1-second retry timer that could miss the exact moment when tools drained to zero pending. In CI environments with scheduler variation, the recovery loop could timeout waiting for the timer to fire even though tools had already drained. The fix adds immediate retry (0ms delay) when tools transition from pending to settled, ensuring the deadline recovery publishes the terminal result without waiting for the scheduled retry timer. Regression test in `session-runtime.test.ts` validates that deadline recovery completes even with pending tools that drain before timeout.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Fixed
- Keep accepted SDK prompt terminal outcomes private until exact run/tool settlement evidence is available.
- Preserve queued subagent owner and generation identity through shutdown cancellation and queue draining.
307 changes: 271 additions & 36 deletions packages/coding-agent/src/async/job-manager.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/coding-agent/src/prompts/tools/read.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ For `.sqlite`, `.sqlite3`, `.db`, `.db3`:

`agent://<id>`, `artifact://<id>`, `rule://<name>`, and `local://<name>.md` resolve transparently and accept the same line selectors as filesystem paths. Use `artifact://<id>` to recover full output that a previous bash/eval/tool result spilled or truncated.

Bundled skills have no filesystem home, so the skill tool and skill discovery report them as `embedded:gjc/skills/<name>/SKILL.md`; read that identifier verbatim. Bundled skill fragments, when surfaced, likewise use `embedded:gjc/skill-fragments/...` identifiers.
Bundled skills have no filesystem home, so the skill tool and skill discovery report them as `embedded:gjc/skills/<name>/SKILL.md`; read that identifier verbatim. Bundled skill fragments, when surfaced, likewise use `embedded:gjc/skill-fragments/…` identifiers.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Regenerate the tool catalog after changing the prompt

This changes the live read description, but src/tools/tool-catalog.generated.ts still contains embedded:gjc/skill-fragments/.... Consequently generateToolCatalogData() no longer equals the committed TOOL_CATALOG, so test/tools/tool-catalog.test.ts fails its reproducibility assertion and deferred/read-materialized tool metadata disagree. Regenerate the catalog and any prompt goldens after this edit.

AGENTS.md reference: AGENTS.md:L87-L87

Useful? React with 👍 / 👎.


<critical>
- Always include `path`; never call `read` with `{}`.
Expand Down
209 changes: 201 additions & 8 deletions packages/coding-agent/src/sdk/bus/reconciliation-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -670,11 +670,22 @@ export interface ReconciliationStore {
delete(): Promise<void>;
}

export interface ReconciliationStoreInstrumentationEvent {
at: number;
phase: "enqueued" | "started" | "completed" | "failed";
operation?: string;
substep?: string;
error?: string;
/** Detailed information about lock acquisition, promise identity, db state, etc. */
detail?: string;
}

export function createReconciliationStore(options: {
sessionFile: string | null | undefined;
sessionId: string;
fs?: ReconciliationStoreFs;
now?: () => number;
onInstrumentationForTests?: (event: ReconciliationStoreInstrumentationEvent) => void;
}): ReconciliationStore {
const fileFs = options.fs ?? nodeFs;
const now = options.now ?? Date.now;
Expand All @@ -701,20 +712,56 @@ export function createReconciliationStore(options: {
const directory = path.dirname(filePath);
let temporary: string | undefined;
try {
try {
options.onInstrumentationForTests?.({ at: Date.now(), phase: "started", substep: "mkdir" });
} catch {}
await fileFs.mkdir(directory, { recursive: true, mode: 0o700 });
try {
options.onInstrumentationForTests?.({ at: Date.now(), phase: "completed", substep: "mkdir" });
} catch {}
temporary = `${filePath}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
try {
options.onInstrumentationForTests?.({ at: Date.now(), phase: "started", substep: "writeFile" });
} catch {}
await fileFs.writeFile(temporary, `${JSON.stringify(document)}\n`, { mode: 0o600 });
try {
options.onInstrumentationForTests?.({ at: Date.now(), phase: "completed", substep: "writeFile" });
} catch {}
try {
try {
options.onInstrumentationForTests?.({ at: Date.now(), phase: "started", substep: "open" });
} catch {}
const handle = await fileFs.open(temporary, "r+");
try {
options.onInstrumentationForTests?.({ at: Date.now(), phase: "completed", substep: "open" });
} catch {}
try {
try {
options.onInstrumentationForTests?.({ at: Date.now(), phase: "started", substep: "sync" });
} catch {}
await handle.sync();
try {
options.onInstrumentationForTests?.({ at: Date.now(), phase: "completed", substep: "sync" });
} catch {}
} finally {
try {
options.onInstrumentationForTests?.({ at: Date.now(), phase: "started", substep: "close" });
} catch {}
await handle.close();
try {
options.onInstrumentationForTests?.({ at: Date.now(), phase: "completed", substep: "close" });
} catch {}
}
} catch {
// fsync optional on some fs seams
}
try {
options.onInstrumentationForTests?.({ at: Date.now(), phase: "started", substep: "rename" });
} catch {}
await fileFs.rename(temporary, filePath);
try {
options.onInstrumentationForTests?.({ at: Date.now(), phase: "completed", substep: "rename" });
} catch {}
} catch (error) {
// Every persistence-path failure (mkdir included) is evidence a drained
// window must surface, never silently treat as quiescent (#4743).
Expand All @@ -723,6 +770,9 @@ export function createReconciliationStore(options: {
code: "reconciliation_persist_failed",
});
unreportedPersistFailures.push(coded);
try {
options.onInstrumentationForTests?.({ at: Date.now(), phase: "failed", error: coded.message });
} catch {}
throw coded;
}
};
Expand Down Expand Up @@ -795,7 +845,26 @@ export function createReconciliationStore(options: {
const transact = async (
mutator: (records: DurableReconciliationRecord[]) => DurableReconciliationRecord[],
): Promise<void> => {
const promiseId = `reconcil-transact-${randomUUID().slice(0, 8)}`;
const enqueueTime = Date.now();
const run = async () => {
const lockWaitMs = Date.now() - enqueueTime;
try {
options.onInstrumentationForTests?.({
at: enqueueTime,
phase: "enqueued",
operation: "transact",
detail: `promise=${promiseId}`,
});
} catch {}
try {
options.onInstrumentationForTests?.({
at: Date.now(),
phase: "started",
operation: "transact",
detail: `promise=${promiseId} lock-wait=${lockWaitMs}ms`,
});
} catch {}
const next = mutator(memory.map(r => ({ ...r })));
await writeAtomic({
version: RECONCILIATION_STORE_VERSION,
Expand All @@ -805,19 +874,55 @@ export function createReconciliationStore(options: {
...(terminalKeyMemory.length > 0 ? { evictedTerminalKeys: terminalKeyMemory } : {}),
});
memory = next;
try {
options.onInstrumentationForTests?.({
at: Date.now(),
phase: "completed",
operation: "transact",
detail: `promise=${promiseId}`,
});
} catch {}
};
const pending = chain.then(run, run);
chain = pending.then(
const settled = pending.then(
() => undefined,
() => undefined,
);
await pending;
chain = settled;
try {
await pending;
} catch (error) {
try {
options.onInstrumentationForTests?.({
at: Date.now(),
phase: "failed",
operation: "transact",
detail: `promise=${promiseId} lock-wait=${Date.now() - enqueueTime}ms`,
error: error instanceof Error ? error.message : String(error),
});
} catch {}
throw error;
}
};

const transactTerminalScopes = async (
mutator: (scopes: DurableTerminalScopeRecord[]) => DurableTerminalScopeRecord[],
): Promise<void> => {
const run = async () => {
try {
options.onInstrumentationForTests?.({
at: Date.now(),
phase: "enqueued",
operation: "transactTerminalScopes",
});
} catch {}
try {
options.onInstrumentationForTests?.({
at: Date.now(),
phase: "started",
operation: "transactTerminalScopes",
});
} catch {}
const next = mutator(terminalMemory.map(s => ({ ...s })));
await writeAtomic({
version: RECONCILIATION_STORE_VERSION,
Expand All @@ -827,13 +932,33 @@ export function createReconciliationStore(options: {
...(terminalKeyMemory.length > 0 ? { evictedTerminalKeys: terminalKeyMemory } : {}),
});
terminalMemory = next;
try {
options.onInstrumentationForTests?.({
at: Date.now(),
phase: "completed",
operation: "transactTerminalScopes",
});
} catch {}
};
const pending = chain.then(run, run);
chain = pending.then(
const settled = pending.then(
() => undefined,
() => undefined,
);
await pending;
chain = settled;
try {
await pending;
} catch (error) {
try {
options.onInstrumentationForTests?.({
at: Date.now(),
phase: "failed",
operation: "transactTerminalScopes",
error: error instanceof Error ? error.message : String(error),
});
} catch {}
throw error;
}
};

const transactTerminalState = async (
Expand All @@ -843,6 +968,20 @@ export function createReconciliationStore(options: {
},
): Promise<void> => {
const run = async () => {
try {
options.onInstrumentationForTests?.({
at: Date.now(),
phase: "enqueued",
operation: "transactTerminalState",
});
} catch {}
try {
options.onInstrumentationForTests?.({
at: Date.now(),
phase: "started",
operation: "transactTerminalState",
});
} catch {}
const next = mutator({
scopes: terminalMemory.map(s => ({ ...s })),
keys: terminalKeyMemory.map(k => ({ ...k })),
Expand All @@ -856,19 +995,53 @@ export function createReconciliationStore(options: {
});
terminalMemory = next.scopes;
terminalKeyMemory = next.keys;
try {
options.onInstrumentationForTests?.({
at: Date.now(),
phase: "completed",
operation: "transactTerminalState",
});
} catch {}
};
const pending = chain.then(run, run);
chain = pending.then(
const settled = pending.then(
() => undefined,
() => undefined,
);
await pending;
chain = settled;
try {
await pending;
} catch (error) {
try {
options.onInstrumentationForTests?.({
at: Date.now(),
phase: "failed",
operation: "transactTerminalState",
error: error instanceof Error ? error.message : String(error),
});
} catch {}
throw error;
}
};

const transactTerminalKeys = async (
mutator: (keys: EvictedTerminalKeyEntry[]) => EvictedTerminalKeyEntry[],
): Promise<void> => {
const run = async () => {
try {
options.onInstrumentationForTests?.({
at: Date.now(),
phase: "enqueued",
operation: "transactTerminalKeys",
});
} catch {}
try {
options.onInstrumentationForTests?.({
at: Date.now(),
phase: "started",
operation: "transactTerminalKeys",
});
} catch {}
const next = mutator(terminalKeyMemory.map(k => ({ ...k })));
await writeAtomic({
version: RECONCILIATION_STORE_VERSION,
Expand All @@ -878,13 +1051,33 @@ export function createReconciliationStore(options: {
...(next.length > 0 ? { evictedTerminalKeys: next } : {}),
});
terminalKeyMemory = next;
try {
options.onInstrumentationForTests?.({
at: Date.now(),
phase: "completed",
operation: "transactTerminalKeys",
});
} catch {}
};
const pending = chain.then(run, run);
chain = pending.then(
const settled = pending.then(
() => undefined,
() => undefined,
);
await pending;
chain = settled;
try {
await pending;
} catch (error) {
try {
options.onInstrumentationForTests?.({
at: Date.now(),
phase: "failed",
operation: "transactTerminalKeys",
error: error instanceof Error ? error.message : String(error),
});
} catch {}
throw error;
}
};

const deleteStore = async (): Promise<void> => {
Expand Down
Loading
Loading