Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
- Post-merge repair for #4542: `CHAT_DAEMON_GENERATIONS.discord` 64β†’65 and `.slack` 67β†’68 so the `SessionRouter` initial attachment replay change is generation-fenced for already-running Discord and Slack daemons. The semantic guard manifest is regenerated with the corrected generation and declaration digests.
- Cursor split responses now preserve canonical provider order across pre-admission artifact spilling, so a server-side tool result cannot be persisted after the assistant continuation that follows it. The canonical admission reservation is owned by each emission's own handler, so a host bridge or replay emitting the same `message_end` event object twice can no longer overwrite the reservation and deadlock every later canonical admission (#4536).
- Uncontended canonical `message_end` admissions no longer cost a microtask: the admission lane keeps a synchronous fast path when its predecessor slot is already released, restoring synchronous visibility of persisted appends for external emitters (successor-finalization and deep-interview continuation flows) while FIFO admission still holds under a genuinely in-flight predecessor (#4536).
- SDK `session.prompt`/`steer` dispatches that arrive while a default-model selection already owns the admission fence are now preserved as queued work instead of failing: the shared ingress awaits the selection fence, keeps same-session prompt entry fail-fast, and only private scheduled continuations (auto-compaction/queue continuation) bypass the fence with an explicit reentry capability. Queued dispatches reserve follow-up order before durable acceptance so a later plain prompt cannot overtake an earlier follow-up behind the same fence, and continuations parked behind a pending selection fence are tracked as settlement work so `waitForIdle` cannot report completion while the continuation is still waiting (#4519).
- Default-model selection now reserves a causal fence before credential probing, so an already accepted prompt preflight cannot be overtaken and a selection accepted first blocks later prompt preflight through durable publication. The fence does not hold session admission across `waitForIdle`, allowing inherited auto-compaction continuations to obtain prompt admission; same-session reentrancy still fails fast, successor sessions remain protected, and disposal deterministically drains accepted selections while rejecting queued prompts without unhandled rejections (#4519).
- Ordinary sessions no longer import or execute Claude Code and Codex directory hooks as competing runtime authorities. Runtime hook discovery is fail-closed to canonical native `.gjc/hooks/` providers while explicit configured paths, constrained plugin hooks, and foreign-provider import/diagnostic discovery remain available (#4516).
- Telegram/Slack/Discord outbound publications no longer freeze after a session-host rehost. A rehosted fleet re-attaches every session in one reconcile pass, and each attachment's initial `event_replay` was awaited inside the serialized `#reconcileTail`, so one slow replay (up to its full retry budget) wedged all later reconciles and the sends funneling through them; leases and inbound polling stayed green while delivery died until daemon restart. Reconcile-driven attachments now publish immediately and run initial replay on the attachment's ready tail (matching the reconnect path); `start()` still drains those tails so bootstrap callers observe replay completion. Replay ordering, generation fences, cross-session isolation, and provider hooks are unchanged (#4527).
Expand Down
3 changes: 3 additions & 0 deletions packages/coding-agent/src/extensibility/extensions/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,12 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
content: string | (TextContent | ImageContent)[],
options?: {
deliverAs?: "steer" | "followUp";
queuedAtDispatch?: boolean;
onPreflightAccepted?: () => void;
onPreflightAcceptCommit?: () => void | Promise<void>;
onQueuedPromoted?: () => void;
preflightSignal?: AbortSignal;
sdkRunToken?: string;
},
): Promise<void> {
return Promise.resolve(this.runtime.sendUserMessage(content, options));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ export class ExtensionRunner {
#isIdleFn: () => boolean = () => true;
#getActivePromptHandleFn: () => string | undefined = () => undefined;
#waitForIdleFn: () => Promise<void> = async () => {};
#abortFn: () => void = () => {};
#abortFn: () => void | Promise<void> = () => {};
#abortPromptAndWaitFn: NonNullable<ExtensionContextActions["abortPromptAndWait"]> = async () => {
throw new Error("abortPromptAndWait binding is unavailable");
};
Expand Down
7 changes: 5 additions & 2 deletions packages/coding-agent/src/extensibility/extensions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ export interface ExtensionContext {
/** Stable resource ownership identifier for the active prompt run. */
getActivePromptHandle(): string | undefined;
/** Abort the current agent operation */
abort(): void;
abort(): void | Promise<void>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add the coding-agent changelog entry

This changes observable coding-agent SDK admission, abort, and continuation behavior, but the commit contains no entry under packages/coding-agent/CHANGELOG.md's Unreleased section. The repository contract requires package changes to be recorded there, so the release would otherwise omit this user-visible fix from its changelog.

AGENTS.md reference: AGENTS.md:L188-L188

Useful? React with πŸ‘Β / πŸ‘Ž.

/** Abort and prove whether resources for a specific prompt settled. */
abortPromptAndWait?(handle: string, options: { graceMs: number }): Promise<RunSettlementProof>;
/** Whether there are queued messages waiting */
Expand Down Expand Up @@ -1186,8 +1186,11 @@ export interface ExtensionAPI {
content: string | (TextContent | ImageContent)[],
options?: {
deliverAs?: "steer" | "followUp";
/** Internal SDK signal preserving a busy dispatch across async admission fences. */
queuedAtDispatch?: boolean;
onPreflightAccepted?: () => void;
onPreflightAcceptCommit?: () => void | Promise<void>;
onQueuedPromoted?: () => void;
preflightSignal?: AbortSignal;
/** Internal SDK correlation owner for an exact queued follow-up. */
sdkRunToken?: string;
Expand Down Expand Up @@ -1505,7 +1508,7 @@ export interface ExtensionContextActions {
isIdle: () => boolean;
/** Stable resource ownership identifier for the active prompt run. */
getActivePromptHandle?: () => string | undefined;
abort: () => void;
abort: () => void | Promise<void>;
abortPromptAndWait?: (handle: string, options: { graceMs: number }) => Promise<RunSettlementProof>;

hasPendingMessages: () => boolean;
Expand Down
4 changes: 4 additions & 0 deletions packages/coding-agent/src/sdk/host/session-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,7 @@ describe("SessionSdkSessionRuntime", () => {
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-unwind-promoted-"));
let idle = true;
let promoted: (() => void) | undefined;
const queuedDispositions: boolean[] = [];
const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<void> | void>();
const api = {
on(event: string, handler: (event: unknown, ctx: ExtensionContext) => Promise<void> | void) {
Expand All @@ -446,10 +447,12 @@ describe("SessionSdkSessionRuntime", () => {
onPreflightAccepted?: () => void;
onPreflightAcceptCommit?: () => void;
onQueuedPromoted?: () => void;
queuedAtDispatch?: boolean;
}
| undefined,
) =>
Promise.resolve(options?.onPreflightAcceptCommit?.()).then(() => {
queuedDispositions.push(options?.queuedAtDispatch === true);
promoted = options?.onQueuedPromoted;
options?.onPreflightAccepted?.();
return {};
Expand Down Expand Up @@ -509,6 +512,7 @@ describe("SessionSdkSessionRuntime", () => {
idle = false;
prompt("conn-b", "unwind-b");
await waitResponse("unwind-b");
expect(queuedDispositions).toEqual([false, true]);
expect(promoted).toBeDefined();
// The unwind continuation promotes the queued steer to its own run: the
// correlation hook fires before agent_start...
Expand Down
8 changes: 5 additions & 3 deletions packages/coding-agent/src/sdk/host/session-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1317,6 +1317,7 @@ function createControlSurface(
onPreflightAcceptCommit: () => Promise<void>;
/** Fired when a queued submission (steering or follow-up) is promoted to its own run (SDK ownership correlation). */
onQueuedPromoted: () => void;
queuedAtDispatch: boolean;
}) => Promise<unknown>,
acceptedFields?: () => Record<string, unknown>,
allowCompletionFallback = false,
Expand Down Expand Up @@ -1392,6 +1393,7 @@ function createControlSurface(
// created at promotion so the submitting connection can
// terminal-abort that turn (review threads P1/P2).
onQueuedPromoted: () => onPromotedTurn?.(kind, correlation, requesterConnectionId),
queuedAtDispatch,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Exclude abort-and-prompt from queued dispatch classification

When turn.abort_and_prompt is invoked during an active turn, ctx.abort() begins asynchronous teardown but submit() immediately observes the session as non-idle, so this new field reaches sendUserMessage() as queuedAtDispatch: true. Because the aborted agent is still streaming, the replacement is classified as steering and queued into the dying run; an internal abort does not schedule the user-interrupt steering continuation, leaving the promised replacement stranded instead of starting a new turn. The abort-and-prompt route must force fresh-prompt delivery after abort settlement rather than inherit this busy-dispatch classification.

Useful? React with πŸ‘Β / πŸ‘Ž.

}),
);
void submission.then(
Expand Down Expand Up @@ -2316,10 +2318,10 @@ function createControlSurface(
};
return {
prompt: async (text, images, clientRef) =>
submit("prompt", clientRef, options =>
submit("prompt", clientRef, ({ queuedAtDispatch, ...options }) =>
api.sendUserMessage(
typeof images === "undefined" ? text : ([{ type: "text", text }, ...(images as never[])] as never),
options,
queuedAtDispatch ? { ...options, queuedAtDispatch: true } : options,
),
),
steer: async (text, clientRef) => {
Expand Down Expand Up @@ -2355,7 +2357,7 @@ function createControlSurface(
},
abortTerminal: terminalAbort,
abortAndPrompt: async text => {
ctx.abort();
await ctx.abort();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Await actual abort settlement before submitting the replacement

When turn.abort_and_prompt is called during an active turn, this does not wait for teardown: fresh evidence is that ExtensionContext.abort() is declared to return void (extensions/types.ts:372) and the runner's callback also returns void, so await ctx.abort() resolves immediately. submit() therefore still snapshots the session as busy, marks the replacement queuedAtDispatch, and sendUserMessage() can enqueue it as steering into the aborting run; because that run may exit without rearming steering, the accepted replacement remains stranded. Await a settlement-capable abort API before dispatching the prompt.

Useful? React with πŸ‘Β / πŸ‘Ž.

return await submit("prompt", undefined, options => api.sendUserMessage(text, options));
},
answerAsk: unavailable("ask.answer"),
Expand Down
Loading
Loading