@gajae-code/coding-agent 0.16.1 → 0.16.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -338,6 +338,8 @@ type AttachedSession = {
338
338
  replayPending: boolean;
339
339
  replayPendingSince?: number;
340
340
  replayPendingToken?: object;
341
+ drainingHeldSeq?: number;
342
+ drainingHeldBatch?: HeldFrame[];
341
343
  readyTail: Promise<void>;
342
344
  readonly publication: { promise: Promise<void>; resolve: () => void; reject: (reason?: unknown) => void };
343
345
  dispose: () => void;
@@ -1899,44 +1901,53 @@ export class SessionRouter {
1899
1901
  }
1900
1902
 
1901
1903
  async #reinitializeAttachment(attached: AttachedSession): Promise<void> {
1902
- const previous = attached.readyTail;
1904
+ // A prior ready tail may be waiting for publication work carried by the dead
1905
+ // socket. Chaining the reconnect behind it prevents the replacement from even
1906
+ // issuing replay. Replace its hold-array identity to fence that tail, but carry
1907
+ // forward frames it had not started draining; the active draining sequence is
1908
+ // already represented by the independent frame tail.
1909
+ if (attached.barrier.held) {
1910
+ const carried = [...(attached.drainingHeldBatch ?? []), ...attached.barrier.held];
1911
+ attached.drainingHeldBatch = undefined;
1912
+ attached.barrier.held = carried
1913
+ .filter(entry => entry.seq !== attached.drainingHeldSeq)
1914
+ .filter((entry, index, entries) => entries.findIndex(candidate => candidate.seq === entry.seq) === index);
1915
+ }
1903
1916
  const replayToken = {};
1904
1917
  attached.replayPending = true;
1905
1918
  attached.replayPendingSince = Date.now();
1906
1919
  attached.replayPendingToken = replayToken;
1907
- const current = previous
1908
- .catch(() => undefined)
1909
- .then(async () => {
1910
- if (!this.#attachmentLive(attached)) return;
1911
- const proven = await this.#readProvenEndpoint(attached.indexed);
1912
- if (
1913
- !proven ||
1914
- proven.endpoint.url !== attached.endpoint.url ||
1915
- proven.endpoint.token !== attached.endpoint.token ||
1916
- proven.endpoint.pid !== attached.pid ||
1917
- !sameEndpointIdentity(attached.endpointIdentity, proven.identity)
1918
- ) {
1919
- await this.#retireAttachment(attached, proven ? "replaced_same_generation" : undefined);
1920
- return;
1921
- }
1922
- attached.initializingPublication = true;
1923
- try {
1924
- void Promise.resolve()
1925
- .then(() => this.#deps.onNotificationSubscriptionReady?.(attached.notificationSubscription))
1926
- .catch(error => {
1927
- this.#detachNotification(attached, "cancelled");
1928
- logger.warn(`SDK notification subscription reconnect hook failed locally: ${String(error)}`);
1929
- });
1930
- await this.#deps.onAttachmentReady?.(attached.capability);
1931
- await this.#deps.onReplayReady?.();
1932
- } catch {
1933
- await this.#retireAttachment(attached);
1934
- return;
1935
- } finally {
1936
- attached.initializingPublication = false;
1937
- }
1938
- if (this.#attachmentLive(attached)) await this.#replayAttachment(attached, attached.cursor.seq);
1939
- });
1920
+ const current = Promise.resolve().then(async () => {
1921
+ if (!this.#attachmentLive(attached)) return;
1922
+ const proven = await this.#readProvenEndpoint(attached.indexed);
1923
+ if (
1924
+ !proven ||
1925
+ proven.endpoint.url !== attached.endpoint.url ||
1926
+ proven.endpoint.token !== attached.endpoint.token ||
1927
+ proven.endpoint.pid !== attached.pid ||
1928
+ !sameEndpointIdentity(attached.endpointIdentity, proven.identity)
1929
+ ) {
1930
+ await this.#retireAttachment(attached, proven ? "replaced_same_generation" : undefined);
1931
+ return;
1932
+ }
1933
+ attached.initializingPublication = true;
1934
+ try {
1935
+ void Promise.resolve()
1936
+ .then(() => this.#deps.onNotificationSubscriptionReady?.(attached.notificationSubscription))
1937
+ .catch(error => {
1938
+ this.#detachNotification(attached, "cancelled");
1939
+ logger.warn(`SDK notification subscription reconnect hook failed locally: ${String(error)}`);
1940
+ });
1941
+ await this.#deps.onAttachmentReady?.(attached.capability);
1942
+ await this.#deps.onReplayReady?.();
1943
+ } catch {
1944
+ await this.#retireAttachment(attached);
1945
+ return;
1946
+ } finally {
1947
+ attached.initializingPublication = false;
1948
+ }
1949
+ if (this.#attachmentLive(attached)) await this.#replayAttachment(attached, attached.cursor.seq);
1950
+ });
1940
1951
  attached.readyTail = current.finally(() => {
1941
1952
  if (attached.replayPendingToken === replayToken) {
1942
1953
  attached.replayPending = false;
@@ -2294,7 +2305,19 @@ export class SessionRouter {
2294
2305
  return;
2295
2306
  }
2296
2307
  const batch = held.splice(0, held.length).sort((left, right) => left.seq - right.seq);
2297
- for (const entry of batch) await this.#enqueueFrame(attached, entry.frame, "ordered");
2308
+ attached.drainingHeldBatch = batch;
2309
+ for (;;) {
2310
+ const entry = batch.shift();
2311
+ if (!entry) break;
2312
+ attached.drainingHeldSeq = entry.seq;
2313
+ try {
2314
+ await this.#enqueueFrame(attached, entry.frame, "ordered");
2315
+ } finally {
2316
+ if (attached.drainingHeldSeq === entry.seq) attached.drainingHeldSeq = undefined;
2317
+ }
2318
+ if (attached.barrier.held !== held) return;
2319
+ }
2320
+ if (attached.drainingHeldBatch === batch) attached.drainingHeldBatch = undefined;
2298
2321
  }
2299
2322
  }
2300
2323
 
@@ -2374,7 +2397,7 @@ export class SessionRouter {
2374
2397
  this.#failBarrier(attached, "replay response contained a malformed event");
2375
2398
  return;
2376
2399
  }
2377
- let previousSeq = sinceSeq;
2400
+ let previousSeq = -1;
2378
2401
  for (const event of events) {
2379
2402
  if (event.type !== "event") {
2380
2403
  this.#failBarrier(attached, "replay response contained a non-event frame");
@@ -2411,7 +2434,7 @@ export class SessionRouter {
2411
2434
  }
2412
2435
  const retained = events
2413
2436
  .map(event => readSequence(event.seq))
2414
- .find(seq => seq !== undefined && seq <= gap.toSeq);
2437
+ .find(seq => seq !== undefined && seq >= gap.fromSeq && seq <= gap.toSeq);
2415
2438
  if (retained !== undefined) {
2416
2439
  this.#failBarrier(
2417
2440
  attached,
@@ -2720,7 +2720,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2720
2720
  hasForegroundBashBackgroundRequestHandler: () => session?.hasForegroundBashBackgroundRequestHandler() ?? false,
2721
2721
  requestForegroundBashBackground: (reason, adapter) =>
2722
2722
  Promise.resolve(session?.requestForegroundBashBackground(reason, adapter) ?? false),
2723
- getToolInterruptPolicy: () => agent?.getToolInterruptPolicy() ?? "abort_tools",
2724
2723
 
2725
2724
  getCredentialSessionId: () => session?.credentialSessionId ?? credentialSessionId,
2726
2725
  getMcpManager: () => mcpManager ?? options.inheritedMcpManager,
package/src/tools/bash.ts CHANGED
@@ -1139,20 +1139,22 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
1139
1139
 
1140
1140
  /**
1141
1141
  * Whether a queued user steer may fold the running foreground wait. Mirrors
1142
- * the loop's own steer admission: `busyPromptMode=queue` never admits a
1143
- * steer into the busy run, and `toolInterruptPolicy=finish_tools` explicitly
1144
- * lets the tool finish, so neither may fold. The
1145
- * auto-background setting is deliberately not consulted: like the chord,
1146
- * a steer fold is a user action.
1142
+ * the loop's steer admission: `busyPromptMode=queue` never admits a steer
1143
+ * into the busy run, so it never folds. `toolInterruptPolicy` is
1144
+ * deliberately NOT a gate: `finish_tools` means "do not kill the batch to
1145
+ * deliver a steer", and a fold kills nothing. It is the one way to deliver
1146
+ * the steer now AND let the command finish, so it applies under both
1147
+ * policies; the policy only decides what the loop does with sibling tools
1148
+ * after the fold returns. The auto-background setting is likewise not
1149
+ * consulted: like the chord, a steer fold is a user action.
1147
1150
  *
1148
- * Fails closed: a session that cannot prove its tool interrupt policy or
1149
- * report newly admitted steering is not steer-foldable.
1151
+ * Fails closed: a session that cannot report newly admitted steering or
1152
+ * accept a fold request is not steer-foldable.
1150
1153
  */
1151
1154
  #steerFoldEnabled(): boolean {
1152
- const { waitForUserSteering, getToolInterruptPolicy, requestForegroundBashBackground } = this.session;
1153
- if (!waitForUserSteering || !getToolInterruptPolicy || !requestForegroundBashBackground) return false;
1154
- if (this.session.settings.get("busyPromptMode") !== "steer") return false;
1155
- return getToolInterruptPolicy() === "abort_tools";
1155
+ const { waitForUserSteering, requestForegroundBashBackground } = this.session;
1156
+ if (!waitForUserSteering || !requestForegroundBashBackground) return false;
1157
+ return this.session.settings.get("busyPromptMode") === "steer";
1156
1158
  }
1157
1159
 
1158
1160
  /**
@@ -1161,8 +1163,8 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
1161
1163
  * when the wait starts, or one that arrives inside the grace window, never
1162
1164
  * folds: the command finishes normally and that steer is consumed at the
1163
1165
  * ordinary tool boundary. The watcher keeps observing so a later qualifying
1164
- * steer still folds, and re-checks the gates at that moment so a
1165
- * `busyPromptMode`/`toolInterruptPolicy` change during the command is honored.
1166
+ * steer still folds, and re-checks the gate at that moment so a
1167
+ * `busyPromptMode` change during the command is honored.
1166
1168
  * Returns a stop function; call it when the wait settles. No-op when steer
1167
1169
  * folding is gated off at start.
1168
1170
  */
@@ -242,8 +242,6 @@ export interface ToolSession {
242
242
  hasForegroundBashBackgroundRequestHandler?: () => boolean;
243
243
  /** Request that a foreground wait fold into a background job, if supported. `reason` names the trigger; `adapter` targets a specific wait instead of the newest registration. */
244
244
  requestForegroundBashBackground?: (reason?: FoldReason, adapter?: FoldAdapter) => Promise<boolean>;
245
- /** Current tool interrupt policy; `finish_tools` lets the running tool finish, so a steer must not fold it. */
246
- getToolInterruptPolicy?: () => "abort_tools" | "finish_tools";
247
245
 
248
246
  /** Get the session-owned or inherited async job manager. */
249
247
  getAsyncJobManager?: () => AsyncJobManager | undefined;
@@ -2319,7 +2319,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
2319
2319
  "job": {
2320
2320
  "name": "job",
2321
2321
  "label": "Job",
2322
- "description": "Inspects, waits, or cancels async jobs.\n\nBackground job results are delivered automatically when complete. Jobs that back task subagents should be controlled via the `subagent` tool when it is available; use `job` for non-subagent jobs (async bash, monitors) and as a compatibility fallback. Running job output stays quiet by default to avoid flooding the conversation; use `tail` when you explicitly want to show/reopen retained output. Reach for this tool only when you need to inspect or intervene.\n\nIn the interactive TUI, a running foreground command can be folded into a background job by pressing the fold chord twice (default `Alt+Shift+B`, plus `Cmd+B` on macOS; remappable via `app.tool.backgroundFold`), or automatically when the user sends a steer after the command has run for at least 2 seconds (with `busyPromptMode=steer` and `toolInterruptPolicy=abort_tools`). This covers managed non-PTY bash, editor/ACP client-terminal commands, and PTY-mode commands. A folded job keeps its original deadline, is never killed or restarted by folding, and its result wakes a later turn automatically; a folded PTY continues output-only and no longer accepts input. Job listings expose `foldReason` (`steer`, `chord`, `timer`, or `sdk_control`) beside `backgrounded` so you can tell why a job left the foreground. `task`/`subagent` waits are not foldable. Raw shell `Ctrl+Z`/`bg` is not the supported path inside GJC because it bypasses job ownership and output-routing contracts.\n\n# Operations\n\n## `list: true`\nUse to inspect what's running.\n\n## `tail: [id, …]`\nShow the retained output buffer for one or more background jobs without waiting.\n- Use this to reopen/tail a backgrounded long-running bash/tool output after folding it away.\n- Output is bounded by the manager retention window; stale cursors may report that only the retained tail is available.\n- Prefer `tail` over polling when you only need to peek at progress, so the conversation can continue without flooding the TUI.\n\n## `poll: [id, …]`\nBlock until the specified jobs finish or the wait window (~30 s, not configurable) elapses.\n- Use when you are genuinely blocked on a result and have no other work to do.\n- Returns the current snapshot when the timer elapses; running jobs remain running.\n- Completed jobs include their final output in the returned snapshot.\n\n## `cancel: [id, …]`\nStop running jobs.\n- Use when a job is stalled, hung, or no longer needed.\n- Returns immediately after cancelling.",
2322
+ "description": "Inspects, waits, or cancels async jobs.\n\nBackground job results are delivered automatically when complete. Jobs that back task subagents should be controlled via the `subagent` tool when it is available; use `job` for non-subagent jobs (async bash, monitors) and as a compatibility fallback. Running job output stays quiet by default to avoid flooding the conversation; use `tail` when you explicitly want to show/reopen retained output. Reach for this tool only when you need to inspect or intervene.\n\nIn the interactive TUI, a running foreground command can be folded into a background job by pressing the fold chord twice (default `Alt+Shift+B`, plus `Cmd+B` on macOS; remappable via `app.tool.backgroundFold`), or automatically when the user sends a steer after the command has run for at least 2 seconds (with `busyPromptMode=steer` (`toolInterruptPolicy` is not a gate)). This covers managed non-PTY bash, editor/ACP client-terminal commands, and PTY-mode commands. A folded job keeps its original deadline, is never killed or restarted by folding, and its result wakes a later turn automatically; a folded PTY continues output-only and no longer accepts input. Job listings expose `foldReason` (`steer`, `chord`, `timer`, or `sdk_control`) beside `backgrounded` so you can tell why a job left the foreground. `task`/`subagent` waits are not foldable. Raw shell `Ctrl+Z`/`bg` is not the supported path inside GJC because it bypasses job ownership and output-routing contracts.\n\n# Operations\n\n## `list: true`\nUse to inspect what's running.\n\n## `tail: [id, …]`\nShow the retained output buffer for one or more background jobs without waiting.\n- Use this to reopen/tail a backgrounded long-running bash/tool output after folding it away.\n- Output is bounded by the manager retention window; stale cursors may report that only the retained tail is available.\n- Prefer `tail` over polling when you only need to peek at progress, so the conversation can continue without flooding the TUI.\n\n## `poll: [id, …]`\nBlock until the specified jobs finish or the wait window (~30 s, not configurable) elapses.\n- Use when you are genuinely blocked on a result and have no other work to do.\n- Returns the current snapshot when the timer elapses; running jobs remain running.\n- Completed jobs include their final output in the returned snapshot.\n\n## `cancel: [id, …]`\nStop running jobs.\n- Use when a job is stalled, hung, or no longer needed.\n- Returns immediately after cancelling.",
2323
2323
  "parameters": {
2324
2324
  "type": "object",
2325
2325
  "properties": {
@@ -65,8 +65,22 @@ const ENABLE_ECHO_INPUT = 0x0004;
65
65
  const ENABLE_WINDOW_INPUT = 0x0008;
66
66
  const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200;
67
67
 
68
- function prepareWindowsConsoleForExternalEditor(): void {
69
- if (process.platform !== "win32") return;
68
+ interface RawModeInput {
69
+ setRawMode?: (mode: boolean) => unknown;
70
+ }
71
+
72
+ export function prepareWindowsConsoleForExternalEditor(
73
+ platform: NodeJS.Platform = process.platform,
74
+ input: RawModeInput = process.stdin,
75
+ ): void {
76
+ if (platform !== "win32") return;
77
+ // Native Windows terminals can expose stdin through a ConPTY pipe rather than
78
+ // a console input handle. In that case GetConsoleMode/SetConsoleMode below
79
+ // cannot re-arm the input stream, but the runtime's TTY implementation can.
80
+ try {
81
+ input.setRawMode?.(true);
82
+ } catch {}
83
+
70
84
  try {
71
85
  const kernel32 = dlopen("kernel32.dll", {
72
86
  GetStdHandle: { args: [FFIType.i32], returns: FFIType.ptr },