@gajae-code/coding-agent 0.12.0 → 0.12.2

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.
Files changed (247) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/dist/types/async/job-manager.d.ts +17 -3
  3. package/dist/types/config/settings-schema.d.ts +56 -2
  4. package/dist/types/exa/mcp-client.d.ts +10 -1
  5. package/dist/types/exec/bash-executor.d.ts +45 -12
  6. package/dist/types/extensibility/extensions/types.d.ts +16 -2
  7. package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
  8. package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
  9. package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
  10. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
  11. package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
  12. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
  13. package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
  14. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  15. package/dist/types/gjc-runtime/ralplan-review-conflicts.d.ts +114 -0
  16. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +23 -3
  17. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  18. package/dist/types/gjc-runtime/team-runtime.d.ts +25 -0
  19. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
  20. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +10 -0
  21. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +37 -80
  22. package/dist/types/modes/acp/acp-agent.d.ts +2 -19
  23. package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
  24. package/dist/types/modes/components/irc-sidebar.d.ts +16 -0
  25. package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
  26. package/dist/types/modes/components/session-observer-overlay.d.ts +18 -0
  27. package/dist/types/modes/components/settings-selector.d.ts +9 -0
  28. package/dist/types/modes/components/status-line/gh.d.ts +5 -0
  29. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
  30. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  31. package/dist/types/modes/controllers/event-controller.d.ts +1 -1
  32. package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
  33. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  34. package/dist/types/modes/interactive-mode.d.ts +9 -0
  35. package/dist/types/modes/theme/theme.d.ts +3 -1
  36. package/dist/types/modes/types.d.ts +32 -0
  37. package/dist/types/reminders/star-reminder.d.ts +3 -13
  38. package/dist/types/runtime-mcp/manager.d.ts +1 -0
  39. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  40. package/dist/types/sdk/broker/transport.d.ts +2 -0
  41. package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
  42. package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -1
  43. package/dist/types/sdk/bus/config.d.ts +6 -2
  44. package/dist/types/sdk/bus/index.d.ts +29 -0
  45. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +8 -1
  46. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  47. package/dist/types/sdk/bus/reconciliation-store.d.ts +7 -3
  48. package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
  49. package/dist/types/sdk/bus/rich-render.d.ts +2 -0
  50. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
  51. package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
  52. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -4
  53. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
  54. package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
  55. package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
  56. package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
  57. package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
  58. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  59. package/dist/types/sdk/prompt-status.d.ts +23 -3
  60. package/dist/types/sdk/session.d.ts +6 -0
  61. package/dist/types/session/agent-session.d.ts +40 -2
  62. package/dist/types/session/artifacts.d.ts +8 -0
  63. package/dist/types/session/blob-store.d.ts +57 -4
  64. package/dist/types/session/internal/managed-session-storage.d.ts +30 -4
  65. package/dist/types/session/session-manager.d.ts +23 -6
  66. package/dist/types/session/session-storage.d.ts +4 -0
  67. package/dist/types/session/streaming-output.d.ts +36 -7
  68. package/dist/types/setup/credential-import.d.ts +12 -0
  69. package/dist/types/skill-state/active-state.d.ts +12 -0
  70. package/dist/types/skill-state/workflow-hud.d.ts +6 -0
  71. package/dist/types/task/executor.d.ts +3 -2
  72. package/dist/types/task/index.d.ts +7 -1
  73. package/dist/types/task/output-manager.d.ts +1 -0
  74. package/dist/types/task/provider-retry-status.d.ts +8 -0
  75. package/dist/types/task/receipt.d.ts +9 -1
  76. package/dist/types/task/types.d.ts +23 -0
  77. package/dist/types/tools/ask-answer-registry.d.ts +2 -2
  78. package/dist/types/tools/bash-interactive.d.ts +4 -1
  79. package/dist/types/tools/bash.d.ts +3 -1
  80. package/dist/types/tools/image-gen.d.ts +4 -0
  81. package/dist/types/tools/index.d.ts +6 -0
  82. package/dist/types/tools/output-meta.d.ts +23 -2
  83. package/dist/types/tools/path-utils.d.ts +1 -0
  84. package/dist/types/tools/read.d.ts +0 -6
  85. package/dist/types/tools/review.d.ts +5 -7
  86. package/dist/types/tools/subagent.d.ts +2 -0
  87. package/dist/types/utils/gh.d.ts +13 -0
  88. package/dist/types/utils/image-resize.d.ts +1 -0
  89. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  90. package/dist/types/web/search/providers/searxng.d.ts +7 -0
  91. package/dist/types/web/search/providers/xai.d.ts +2 -0
  92. package/package.json +10 -8
  93. package/scripts/acp-conformance-agent.ts +218 -0
  94. package/scripts/capture-gjc-bundle-settings.ts +306 -0
  95. package/scripts/dogfood-ralplan-review-conflicts.ts +272 -0
  96. package/scripts/generate-sdk-operation-inventory.ts +5 -0
  97. package/scripts/resident-memory-bench.ts +1341 -0
  98. package/scripts/run-acp-conformance.ts +290 -0
  99. package/src/async/job-manager.ts +99 -39
  100. package/src/cli/fast-help.ts +1 -1
  101. package/src/cli/plugin-cli.ts +176 -18
  102. package/src/commands/team.ts +19 -1
  103. package/src/config/file-lock.ts +11 -2
  104. package/src/config/settings-schema.ts +53 -2
  105. package/src/coordinator-mcp/question-gate-codec.ts +1 -1
  106. package/src/coordinator-mcp/server.ts +5 -12
  107. package/src/debug/index.ts +42 -17
  108. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -0
  109. package/src/defaults/gjc/skills/ralplan/SKILL.md +25 -20
  110. package/src/defaults/gjc/skills/team/SKILL.md +2 -0
  111. package/src/defaults/gjc/skills/ultragoal/SKILL.md +39 -26
  112. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
  113. package/src/defaults/gjc-defaults.ts +4 -4
  114. package/src/exa/mcp-client.ts +12 -3
  115. package/src/exec/bash-executor.ts +144 -27
  116. package/src/extensibility/extensions/runner.ts +15 -0
  117. package/src/extensibility/extensions/types.ts +18 -1
  118. package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
  119. package/src/extensibility/gjc-plugins/index.ts +19 -2
  120. package/src/extensibility/gjc-plugins/installer.ts +189 -50
  121. package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
  122. package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
  123. package/src/extensibility/gjc-plugins/observability.ts +3 -3
  124. package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
  125. package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
  126. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
  127. package/src/extensibility/gjc-plugins/schema.ts +84 -12
  128. package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
  129. package/src/extensibility/gjc-plugins/types.ts +141 -0
  130. package/src/extensibility/gjc-plugins/validation.ts +9 -0
  131. package/src/gjc-runtime/launch-tmux.ts +25 -6
  132. package/src/gjc-runtime/launch-worktree.ts +103 -1
  133. package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
  134. package/src/gjc-runtime/managed-owner-admission.ts +1 -1
  135. package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
  136. package/src/gjc-runtime/ralplan-runtime.ts +491 -27
  137. package/src/gjc-runtime/team-launch.ts +8 -0
  138. package/src/gjc-runtime/team-runtime.ts +140 -32
  139. package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
  140. package/src/gjc-runtime/tmux-owner-isolation.ts +10 -0
  141. package/src/gjc-runtime/tmux-sessions.ts +58 -13
  142. package/src/gjc-runtime/ultragoal-evidence.ts +6 -74
  143. package/src/gjc-runtime/ultragoal-runtime.ts +635 -612
  144. package/src/gjc-runtime/workflow-manifest.generated.json +37 -99
  145. package/src/gjc-runtime/workflow-manifest.ts +21 -70
  146. package/src/internal-urls/docs-index.generated.ts +16 -16
  147. package/src/modes/DESIGN.md +104 -0
  148. package/src/modes/acp/acp-agent.ts +298 -186
  149. package/src/modes/components/gjc-bundle-settings.ts +508 -0
  150. package/src/modes/components/irc-sidebar.ts +30 -1
  151. package/src/modes/components/notifications-settings-editor.ts +12 -1
  152. package/src/modes/components/oauth-selector.ts +68 -30
  153. package/src/modes/components/session-observer-overlay.ts +305 -86
  154. package/src/modes/components/settings-selector.ts +56 -5
  155. package/src/modes/components/status-line/gh.ts +16 -0
  156. package/src/modes/components/tool-status-header.ts +3 -15
  157. package/src/modes/components/transcript-viewer-overlay.ts +99 -24
  158. package/src/modes/controllers/command-controller.ts +42 -34
  159. package/src/modes/controllers/event-controller.ts +27 -16
  160. package/src/modes/controllers/extension-ui-controller.ts +193 -34
  161. package/src/modes/controllers/selector-controller.ts +160 -22
  162. package/src/modes/interactive-mode.ts +189 -62
  163. package/src/modes/jobs-observer.ts +1 -1
  164. package/src/modes/runtime-init.ts +3 -0
  165. package/src/modes/theme/theme.ts +11 -9
  166. package/src/modes/types.ts +88 -0
  167. package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
  168. package/src/prompts/system/eager-todo.md +8 -4
  169. package/src/prompts/system/system-prompt.md +11 -4
  170. package/src/prompts/tools/bash.md +5 -5
  171. package/src/prompts/tools/search-tool-bm25.md +5 -0
  172. package/src/prompts/tools/task.md +1 -1
  173. package/src/reminders/star-reminder.ts +4 -42
  174. package/src/runtime-mcp/manager.ts +19 -4
  175. package/src/sdk/acp/adapter.ts +22 -6
  176. package/src/sdk/broker/lifecycle.ts +18 -13
  177. package/src/sdk/broker/transport.ts +35 -3
  178. package/src/sdk/bus/attachment-registry.ts +5 -1
  179. package/src/sdk/bus/chat-daemon-cli.ts +4 -39
  180. package/src/sdk/bus/chat-daemon-control.ts +7 -3
  181. package/src/sdk/bus/config.ts +18 -2
  182. package/src/sdk/bus/index.ts +661 -126
  183. package/src/sdk/bus/kind-aware-reconciliation.ts +148 -73
  184. package/src/sdk/bus/lifecycle-commands.ts +4 -1
  185. package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
  186. package/src/sdk/bus/reconciliation-store.ts +95 -16
  187. package/src/sdk/bus/rich-draft.ts +1 -1
  188. package/src/sdk/bus/rich-render.ts +2 -2
  189. package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
  190. package/src/sdk/bus/telegram-cli.ts +33 -8
  191. package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
  192. package/src/sdk/bus/telegram-daemon-contract.ts +12 -4
  193. package/src/sdk/bus/telegram-daemon.ts +916 -23
  194. package/src/sdk/bus/telegram-reference.ts +40 -3
  195. package/src/sdk/bus/threaded-render.ts +48 -3
  196. package/src/sdk/bus/topic-registry.ts +20 -0
  197. package/src/sdk/host/query/revision-store.ts +36 -2
  198. package/src/sdk/host/reverse-leases.ts +44 -7
  199. package/src/sdk/prompt-status.ts +16 -3
  200. package/src/sdk/protocol/operation-inventory.generated.json +33 -0
  201. package/src/sdk/session.ts +89 -9
  202. package/src/session/agent-session.ts +844 -173
  203. package/src/session/artifacts.ts +22 -0
  204. package/src/session/auth-broker-config.ts +10 -3
  205. package/src/session/blob-store.ts +915 -24
  206. package/src/session/internal/managed-session-scope.ts +200 -111
  207. package/src/session/internal/managed-session-storage.ts +492 -62
  208. package/src/session/session-manager.ts +1614 -435
  209. package/src/session/session-storage.ts +38 -45
  210. package/src/session/streaming-output.ts +238 -68
  211. package/src/setup/credential-import.ts +75 -13
  212. package/src/skill-state/active-state.ts +32 -0
  213. package/src/skill-state/workflow-hud.ts +17 -0
  214. package/src/skill-state/workflow-mutation-guard.ts +496 -2
  215. package/src/task/executor.ts +122 -39
  216. package/src/task/index.ts +280 -45
  217. package/src/task/output-manager.ts +24 -14
  218. package/src/task/provider-retry-status.ts +16 -0
  219. package/src/task/receipt.ts +34 -8
  220. package/src/task/render.ts +6 -2
  221. package/src/task/types.ts +108 -0
  222. package/src/tools/ask-answer-registry.ts +12 -6
  223. package/src/tools/ask.ts +86 -55
  224. package/src/tools/ast-edit.ts +1 -0
  225. package/src/tools/ast-grep.ts +1 -0
  226. package/src/tools/bash-interactive.ts +8 -3
  227. package/src/tools/bash.ts +503 -60
  228. package/src/tools/find.ts +1 -0
  229. package/src/tools/image-gen.ts +29 -5
  230. package/src/tools/index.ts +6 -0
  231. package/src/tools/output-meta.ts +108 -14
  232. package/src/tools/path-utils.ts +3 -2
  233. package/src/tools/read.ts +18 -0
  234. package/src/tools/review.ts +5 -7
  235. package/src/tools/search.ts +1 -0
  236. package/src/tools/subagent-render.ts +6 -2
  237. package/src/tools/subagent.ts +6 -0
  238. package/src/tools/telegram-send.ts +6 -3
  239. package/src/utils/gh.ts +39 -0
  240. package/src/utils/image-resize.ts +3 -0
  241. package/src/web/search/providers/anthropic.ts +4 -4
  242. package/src/web/search/providers/kimi.ts +15 -5
  243. package/src/web/search/providers/searxng.ts +47 -4
  244. package/src/web/search/providers/xai.ts +7 -2
  245. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
  246. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
  247. package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
@@ -47,6 +47,7 @@ import { resolveAcpFinalText } from "../../sdk/acp/final-text";
47
47
  import { ACP_MCP_LIFECYCLE_TIMEOUT_MS, type SessionLifecycleMcpServer } from "../../sdk/acp/mcp";
48
48
  import { ensureBroker } from "../../sdk/broker/ensure";
49
49
  import { readSdkBrokerDiscovery, SdkClient, SdkClientError } from "../../sdk/client";
50
+ import type { SdkPromptTerminalOutcome } from "../../sdk/prompt-status";
50
51
  import {
51
52
  buildToolCallStartUpdate,
52
53
  mapAgentSessionEventToAcpSessionUpdates,
@@ -66,26 +67,21 @@ const THINKING_CONFIG_ID = "thinking";
66
67
  const SESSION_PAGE_SIZE = 50;
67
68
  export const ACP_BOOTSTRAP_RACE_GUARD_MS = 50;
68
69
  const MAX_ACP_REPLAY_PAGES = 10_000;
70
+ /** Bounded retention of settled prompt correlations so late duplicates stay closed. */
71
+ const SETTLED_PROMPT_CORRELATION_RETENTION = 16;
69
72
 
70
73
  type JsonObject = Record<string, unknown>;
71
- /**
72
- * ACP prompt completion is tied to lifecycle activity observed after dispatch.
73
- * A fast host may stream and finish a turn before acknowledging `turn.prompt`,
74
- * so pre-ack activity is retained but cannot resolve the waiter until the
75
- * acknowledgement supplies its command/turn correlation.
76
- */
77
74
  interface PromptWaiter {
78
- cancelRequested: boolean;
79
75
  acknowledged: boolean;
80
- activityObserved: boolean;
81
- /** The prompt was accepted while the host was already busy, so its next valid idle ends the steer. */
82
- steeringAtAcknowledgement: boolean;
83
76
  /** Highest inbound frame sequence already observed when the prompt was acknowledged. */
84
77
  boundary: number;
85
78
  correlation: PromptCorrelation;
86
79
  messageProgress?: { textEmitted: boolean; thoughtEmitted: boolean };
87
80
  emittedAssistantText: string;
88
- pendingTerminal?: PromptCorrelation;
81
+ settled: boolean;
82
+ terminal?: { outcome: SdkPromptTerminalOutcome; correlation: PromptCorrelation };
83
+ /** Frames for an already-settled correlation held until acknowledgement resolves ownership. */
84
+ deferredFrames: JsonObject[];
89
85
  resolve: (response: PromptResponse) => void;
90
86
  reject: (error: Error) => void;
91
87
  }
@@ -110,6 +106,9 @@ type SessionRecord = {
110
106
  /** Start/update args retained because tool_execution_end does not carry them. */
111
107
  toolArgs: Map<string, unknown>;
112
108
  /** Actionable model-profile authentication failure detected before prompt dispatch. */
109
+ connectionId?: string;
110
+ /** Bounded set of correlations already settled; they stay closed for publication. */
111
+ settledPromptCorrelations: PromptCorrelation[];
113
112
  authFailure?: string;
114
113
  activePrompt?: PromptWaiter;
115
114
  };
@@ -257,31 +256,97 @@ function correlationFrom(...values: unknown[]): PromptCorrelation {
257
256
  return correlation;
258
257
  }
259
258
 
260
- function correlationsConflict(expected: PromptCorrelation, actual: PromptCorrelation): boolean {
259
+ function hasCorrelation(correlation: PromptCorrelation): boolean {
260
+ return correlation.commandId !== undefined || correlation.turnId !== undefined;
261
+ }
262
+ function correlationsMatch(expected: PromptCorrelation, actual: PromptCorrelation): boolean {
261
263
  return (
262
- (expected.commandId !== undefined && actual.commandId !== undefined && expected.commandId !== actual.commandId) ||
263
- (expected.turnId !== undefined && actual.turnId !== undefined && expected.turnId !== actual.turnId)
264
+ hasCorrelation(actual) &&
265
+ (expected.commandId === undefined || expected.commandId === actual.commandId) &&
266
+ (expected.turnId === undefined || expected.turnId === actual.turnId)
264
267
  );
265
268
  }
266
269
 
267
- function correlationsMatch(expected: PromptCorrelation, actual: PromptCorrelation): boolean {
270
+ function hasCompleteCorrelation(correlation: PromptCorrelation): correlation is { commandId: string; turnId: string } {
268
271
  return (
269
- (expected.commandId !== undefined && expected.commandId === actual.commandId) ||
270
- (expected.turnId !== undefined && expected.turnId === actual.turnId)
272
+ typeof correlation.commandId === "string" &&
273
+ correlation.commandId.trim().length > 0 &&
274
+ typeof correlation.turnId === "string" &&
275
+ correlation.turnId.trim().length > 0
271
276
  );
272
277
  }
273
278
 
274
- function isPromptActivity(eventType: string): boolean {
275
- return [
276
- "agent_start",
277
- "turn_start",
278
- "message_start",
279
- "message_update",
280
- "message_end",
281
- "tool_execution_start",
282
- "tool_execution_update",
283
- "tool_execution_end",
284
- ].includes(eventType);
279
+ function correlationsExactlyMatch(expected: PromptCorrelation, actual: PromptCorrelation): boolean {
280
+ return (
281
+ hasCompleteCorrelation(expected) &&
282
+ hasCompleteCorrelation(actual) &&
283
+ expected.commandId === actual.commandId &&
284
+ expected.turnId === actual.turnId
285
+ );
286
+ }
287
+
288
+ function promptAcknowledgement(value: unknown): PromptCorrelation | undefined {
289
+ const candidate = object(value);
290
+ if (
291
+ !candidate ||
292
+ candidate.ok === false ||
293
+ candidate.error !== undefined ||
294
+ (candidate.accepted !== undefined && candidate.accepted !== true)
295
+ )
296
+ return undefined;
297
+ const payload = object(candidate.result) ?? candidate;
298
+ if (payload.accepted !== true) return undefined;
299
+ if (typeof payload.commandId !== "string" || payload.commandId.trim().length === 0) return undefined;
300
+ if (typeof payload.turnId !== "string" || payload.turnId.trim().length === 0) return undefined;
301
+ return { commandId: payload.commandId, turnId: payload.turnId };
302
+ }
303
+ function strictCorrelationFrom(...values: unknown[]): PromptCorrelation | undefined {
304
+ const correlation: PromptCorrelation = {};
305
+ let malformed = false;
306
+ for (const value of values) {
307
+ const candidate = object(value);
308
+ if (!candidate) continue;
309
+ for (const [field, aliases] of [
310
+ ["commandId", ["commandId", "command_id"]],
311
+ ["turnId", ["turnId", "turn_id"]],
312
+ ] as const) {
313
+ for (const alias of aliases) {
314
+ if (!Object.hasOwn(candidate, alias)) continue;
315
+ const identity = candidate[alias];
316
+ if (typeof identity !== "string" || identity.trim().length === 0) {
317
+ malformed = true;
318
+ continue;
319
+ }
320
+ const previous = correlation[field];
321
+ if (previous !== undefined && previous !== identity) malformed = true;
322
+ correlation[field] = identity;
323
+ }
324
+ }
325
+ }
326
+ return malformed ? undefined : correlation;
327
+ }
328
+
329
+ function terminalOutcome(event: JsonObject): SdkPromptTerminalOutcome | undefined {
330
+ const outcome = object(event.outcome);
331
+ if (!outcome) return undefined;
332
+ if (
333
+ outcome.kind === "stopped" &&
334
+ (outcome.reason === "end_turn" ||
335
+ outcome.reason === "max_tokens" ||
336
+ outcome.reason === "max_turn_requests" ||
337
+ outcome.reason === "refusal" ||
338
+ outcome.reason === "cancelled") &&
339
+ (outcome.provenance === "agent" || outcome.provenance === "client_cancel")
340
+ )
341
+ return outcome as SdkPromptTerminalOutcome;
342
+ if (
343
+ outcome.kind === "failed" &&
344
+ (outcome.code === "prompt_failed" || outcome.code === "prompt_deadline_exceeded") &&
345
+ typeof outcome.message === "string" &&
346
+ (outcome.provenance === "agent_failed" || outcome.provenance === "deadline")
347
+ )
348
+ return outcome as SdkPromptTerminalOutcome;
349
+ return undefined;
285
350
  }
286
351
 
287
352
  export type TranscriptReplayBlock = { type: "text"; text: string } | { type: "image"; data: string; mimeType: string };
@@ -322,11 +387,9 @@ type ReceivedSdkEvent = {
322
387
  * without treating notification-specific frames as agent lifecycle truth.
323
388
  */
324
389
  function receivedSdkEvent(frame: JsonObject): ReceivedSdkEvent | undefined {
325
- if (frame.type === "activity") {
326
- const type = frame.state === "busy" ? "agent_start" : frame.state === "idle" ? "agent_end" : undefined;
327
- return type ? { event: { type, ...correlationFrom(frame) } } : undefined;
328
- }
329
- if (frame.type === "agent_start" || frame.type === "agent_end") return { event: frame };
390
+ if (frame.type === "activity") return undefined;
391
+ if (frame.type === "agent_start" || frame.type === "agent_end" || frame.type === "agent_failed")
392
+ return { event: frame };
330
393
  if (frame.type !== "event") return undefined;
331
394
  const payload = object(frame.payload);
332
395
  if (!payload) return undefined;
@@ -547,8 +610,10 @@ export function acpRequestFailure(error: unknown): unknown {
547
610
  switch (code) {
548
611
  case "authentication_failed":
549
612
  return RequestError.authRequired({ code, details: message }, message);
550
- case "not_found":
551
- return RequestError.resourceNotFound(message);
613
+ // `not_found` stays -32603 with its discriminator in `data`: ACP's
614
+ // `resourceNotFound` (-32002) is a URI-addressed resource error, and an unknown
615
+ // session id is not a resource URI. Pinned ACP core-v1 conformance also requires
616
+ // -32603/-32000 for a prompt against an unknown session.
552
617
  case "invalid_input":
553
618
  case "unsupported":
554
619
  case "unsupported_content":
@@ -567,17 +632,59 @@ export function acpProviderRegistrations(
567
632
  env: NodeJS.ProcessEnv = process.env,
568
633
  ): AcpProviderRegistration[] {
569
634
  return [
635
+ // `fs.readTextFile` and `fs.writeTextFile` are independently optional, so the
636
+ // advertised methods travel with the lease instead of being inferred as both.
570
637
  ...(capabilities?.fs?.readTextFile || capabilities?.fs?.writeTextFile
571
- ? [{ capability: "fs", definitions: [] }]
638
+ ? [
639
+ {
640
+ capability: "fs",
641
+ definitions: [
642
+ ...(capabilities.fs.readTextFile ? [{ name: "fs.readTextFile" }] : []),
643
+ ...(capabilities.fs.writeTextFile ? [{ name: "fs.writeTextFile" }] : []),
644
+ ],
645
+ },
646
+ ]
572
647
  : []),
573
648
  ...(capabilities?.terminal ? [{ capability: "terminal", definitions: [] }] : []),
574
649
  ...(resolveAcpPermissionMode(capabilities, env) === "prompt"
575
650
  ? [{ capability: "permission", definitions: [] }]
576
651
  : []),
577
- { capability: "ui", definitions: [] },
652
+ ...(capabilities?.elicitation?.form ? [{ capability: "ui", definitions: [] }] : []),
578
653
  ];
579
654
  }
580
655
 
656
+ export function createAcpReverseConnection(connection: AgentSideConnection, sessionId: string): AcpReverseConnection {
657
+ const methods: Record<string, string> = {
658
+ request: "session/request_permission",
659
+ "permission.request": "session/request_permission",
660
+ "fs.readTextFile": "fs/read_text_file",
661
+ "fs.writeTextFile": "fs/write_text_file",
662
+ "terminal.create": "terminal/create",
663
+ "ui.elicit": "elicitation/create",
664
+ };
665
+ return {
666
+ request: async (
667
+ method: string,
668
+ params: JsonObject,
669
+ options?: { cancellationSignal?: AbortSignal },
670
+ ): Promise<unknown> => {
671
+ const name = methods[method];
672
+ if (!name)
673
+ throw new AcpSdkAdapterError("acp_reverse_unavailable", `ACP reverse method is unavailable: ${method}`);
674
+ const rawRequest = (connection as unknown as Record<string, unknown>).request;
675
+ if (typeof rawRequest !== "function")
676
+ throw new AcpSdkAdapterError("acp_reverse_unavailable", "ACP reverse request surface is unavailable.");
677
+ return await (
678
+ rawRequest as (
679
+ method: string,
680
+ input: JsonObject,
681
+ options?: { cancellationSignal?: AbortSignal },
682
+ ) => Promise<unknown>
683
+ ).call(connection, name, { ...params, sessionId }, options);
684
+ },
685
+ };
686
+ }
687
+
581
688
  /** Maps ACP permission handling to the session's canonical SDK policy. */
582
689
  export async function applyAcpPermissionMode(
583
690
  adapter: Pick<AcpSdkAdapter, "control">,
@@ -597,82 +704,6 @@ export async function applyAcpStartupOptions(
597
704
  if (options?.thinkingLevel) await adapter.control("thinking.set", { level: options.thinkingLevel });
598
705
  }
599
706
 
600
- /** ACP form elicitation uses the client-facing reverse surface without owning a session runtime. */
601
- export function createAcpExtensionUiContext(
602
- connection: AgentSideConnection,
603
- getSessionId: () => string,
604
- capabilities: ClientCapabilities | undefined,
605
- ): {
606
- select: (
607
- message: string,
608
- options: string[],
609
- dialog?: { signal?: AbortSignal; timeout?: number; onTimeout?: () => void },
610
- ) => Promise<string | undefined>;
611
- confirm: (
612
- message: string,
613
- detail?: string,
614
- dialog?: { signal?: AbortSignal; timeout?: number; onTimeout?: () => void },
615
- ) => Promise<boolean>;
616
- input: (
617
- message: string,
618
- placeholder?: string,
619
- dialog?: { signal?: AbortSignal; timeout?: number; onTimeout?: () => void },
620
- ) => Promise<string | undefined>;
621
- } {
622
- const elicit = async (
623
- kind: "select" | "confirm" | "input",
624
- message: string,
625
- options: string[] | undefined,
626
- dialog: { signal?: AbortSignal; timeout?: number; onTimeout?: () => void } | undefined,
627
- ): Promise<unknown> => {
628
- if (!capabilities?.elicitation?.form || dialog?.signal?.aborted) return undefined;
629
- const request = (
630
- connection as unknown as { unstable_createElicitation(input: JsonObject): Promise<JsonObject> }
631
- ).unstable_createElicitation({
632
- sessionId: getSessionId(),
633
- message,
634
- requestedSchema: {
635
- type: "object",
636
- properties: {
637
- value:
638
- kind === "confirm" ? { type: "boolean" } : { type: "string", ...(options ? { enum: options } : {}) },
639
- },
640
- required: ["value"],
641
- },
642
- });
643
- let timer: NodeJS.Timeout | undefined;
644
- const timeout =
645
- dialog?.timeout === undefined
646
- ? undefined
647
- : new Promise<undefined>(resolve => {
648
- timer = setTimeout(() => {
649
- dialog.onTimeout?.();
650
- resolve(undefined);
651
- }, dialog.timeout);
652
- });
653
- try {
654
- const response = timeout ? await Promise.race([request, timeout]) : await request;
655
- return object(object(response)?.content)?.value;
656
- } catch {
657
- return undefined;
658
- } finally {
659
- if (timer) clearTimeout(timer);
660
- }
661
- };
662
- return {
663
- select: async (message, options, dialog) => {
664
- const value = await elicit("select", message, options, dialog);
665
- return typeof value === "string" && options.includes(value) ? value : undefined;
666
- },
667
- confirm: async (message, detail, dialog) =>
668
- (await elicit("confirm", detail ? `${message}\n\n${detail}` : message, undefined, dialog)) === true,
669
- input: async (message, placeholder, dialog) => {
670
- const value = await elicit("input", placeholder ? `${message}\n\n${placeholder}` : message, undefined, dialog);
671
- return typeof value === "string" ? value : undefined;
672
- },
673
- };
674
- }
675
-
676
707
  /**
677
708
  * ACP is a pure SDK client. Session processes are created and resumed by the
678
709
  * broker, while all per-session operations use that session's authenticated SDK
@@ -962,20 +993,19 @@ export class AcpAgent implements Agent {
962
993
 
963
994
  async prompt(params: PromptRequest): Promise<PromptResponse> {
964
995
  const record = this.#sessions.get(params.sessionId);
965
- if (!record) throw new AcpSdkAdapterError("not_found", `Unsupported ACP session: ${params.sessionId}`);
996
+ if (!record) throw new AcpSdkAdapterError("not_found", `Unknown session, not found: ${params.sessionId}`);
966
997
  if (record.activePrompt) throw new AcpSdkAdapterError("conflict", "ACP session already has an active prompt.");
967
998
  if (record.authFailure) throw new AcpSdkAdapterError("authentication_failed", record.authFailure);
968
999
  const payload = acpPromptPayload(params.prompt);
969
1000
  let waiter!: PromptWaiter;
970
1001
  const response = new Promise<PromptResponse>((resolve, reject) => {
971
1002
  waiter = {
972
- cancelRequested: false,
973
1003
  acknowledged: false,
974
- activityObserved: false,
975
- steeringAtAcknowledgement: record.busy,
976
1004
  boundary: record.inboundSequence,
977
1005
  correlation: {},
978
1006
  emittedAssistantText: "",
1007
+ settled: false,
1008
+ deferredFrames: [],
979
1009
  resolve,
980
1010
  reject,
981
1011
  };
@@ -986,14 +1016,29 @@ export class AcpAgent implements Agent {
986
1016
  text: payload.text,
987
1017
  ...(payload.images.length ? { images: payload.images } : {}),
988
1018
  });
989
- waiter.steeringAtAcknowledgement = record.busy;
990
- // Capture the ingress boundary after the command acknowledgement. Frames
991
- // queued before this point are stale with respect to this ACP prompt.
1019
+ const acknowledgementCorrelation = promptAcknowledgement(acknowledgement);
1020
+ if (!acknowledgementCorrelation)
1021
+ throw new AcpSdkAdapterError(
1022
+ "invalid_prompt_acknowledgement",
1023
+ "SDK prompt acknowledgement must accept the prompt and include commandId and turnId.",
1024
+ );
1025
+ // Retain the acknowledgement ingress boundary with its complete correlation.
992
1026
  waiter.boundary = record.inboundSequence;
993
- waiter.correlation = correlationFrom(acknowledgement);
1027
+ waiter.correlation = acknowledgementCorrelation;
994
1028
  waiter.acknowledged = true;
1029
+ // Frames held while ownership was unknown belong to this prompt only when the
1030
+ // acknowledgement proves their complete correlation matches exactly.
1031
+ const deferred = waiter.deferredFrames.splice(0);
1032
+ for (const deferredFrame of deferred)
1033
+ if (correlationsExactlyMatch(waiter.correlation, correlationFrom(deferredFrame)))
1034
+ record.frameTail = record.frameTail.then(
1035
+ async () => await this.#handleSdkFrame(params.sessionId, record.adapter, deferredFrame),
1036
+ );
995
1037
  this.#settlePrompt(record, waiter);
996
1038
  } catch (error) {
1039
+ waiter.deferredFrames.length = 0;
1040
+ waiter.terminal = undefined;
1041
+ waiter.settled = true;
997
1042
  if (record.activePrompt === waiter) record.activePrompt = undefined;
998
1043
  throw error;
999
1044
  }
@@ -1002,8 +1047,7 @@ export class AcpAgent implements Agent {
1002
1047
 
1003
1048
  async cancel(params: { sessionId: string }): Promise<void> {
1004
1049
  const record = this.#sessions.get(params.sessionId);
1005
- if (!record) throw new AcpSdkAdapterError("not_found", `Unsupported ACP session: ${params.sessionId}`);
1006
- const waiter = record.activePrompt;
1050
+ if (!record) throw new AcpSdkAdapterError("not_found", `Unknown session, not found: ${params.sessionId}`);
1007
1051
  const acknowledgement = await record.adapter.cancel();
1008
1052
  const result = object(object(acknowledgement)?.result) ?? object(acknowledgement);
1009
1053
  if (result?.aborted !== true)
@@ -1011,9 +1055,6 @@ export class AcpAgent implements Agent {
1011
1055
  "abort_unacknowledged",
1012
1056
  "SDK did not acknowledge cancellation of the active prompt.",
1013
1057
  );
1014
- // Do not retroactively mark a waiter that already settled while the abort
1015
- // request was in flight. A cancelled response means the abort itself won.
1016
- if (waiter && record.activePrompt === waiter) waiter.cancelRequested = true;
1017
1058
  }
1018
1059
 
1019
1060
  async extMethod(method: string, params: JsonObject): Promise<JsonObject> {
@@ -1110,8 +1151,9 @@ export class AcpAgent implements Agent {
1110
1151
  if (attached) {
1111
1152
  if (path.resolve(attached.cwd) !== path.resolve(cwd))
1112
1153
  throw new AcpSdkAdapterError("conflict", `ACP session ${id} has conflicting cwd authority.`);
1113
- if (mcpServers.length > 0)
1114
- throw new AcpSdkAdapterError("conflict", `ACP session ${id} already has immutable MCP configuration.`);
1154
+ // ACP clients replay their declared MCP servers when reconnecting. The live
1155
+ // session host remains authoritative for its immutable configuration, so
1156
+ // attachment must not reinterpret the replay as a mutation request.
1115
1157
  return;
1116
1158
  }
1117
1159
  const knownCwd = this.#knownSessionCwds.get(id);
@@ -1149,8 +1191,8 @@ export class AcpAgent implements Agent {
1149
1191
  const indexed = await this.#scopedBrokerSession(id, cwd);
1150
1192
  this.#assertSessionEpoch(id, epoch);
1151
1193
  if (indexed?.live) {
1152
- if (mcpServers.length > 0)
1153
- throw new AcpSdkAdapterError("conflict", `ACP session ${id} already has immutable MCP configuration.`);
1194
+ // A reconnect may repeat the client's MCP declaration. Attaching to the
1195
+ // existing endpoint preserves the live host's immutable configuration.
1154
1196
  const result = await this.#brokerEndpoint(id, indexed.endpointGeneration);
1155
1197
  this.#assertSessionEpoch(id, epoch);
1156
1198
  await this.#attach(id, cwd, endpoint(result), epoch);
@@ -1229,6 +1271,19 @@ export class AcpAgent implements Agent {
1229
1271
  connection: this.#reverseConnection(id),
1230
1272
  providers: this.#providers(),
1231
1273
  });
1274
+ let capabilities: JsonObject | undefined;
1275
+ try {
1276
+ const response = object(await adapter.query("runtime.capabilities"));
1277
+ const result = object(response?.result) ?? response;
1278
+ // Q18 is a paged query surface: the capability object arrives as the single
1279
+ // page item, so fall back to the envelope only for direct-result hosts.
1280
+ capabilities = object(pageItems(result)[0]) ?? result;
1281
+ } catch {}
1282
+ if (capabilities?.promptTerminalOutcomeVersion !== 1)
1283
+ throw new AcpSdkAdapterError(
1284
+ "unavailable",
1285
+ "This ACP client requires a newer GJC SDK session; restart the session.",
1286
+ );
1232
1287
  this.#assertSessionEpoch(id, epoch);
1233
1288
  const record: SessionRecord = {
1234
1289
  cwd,
@@ -1237,7 +1292,9 @@ export class AcpAgent implements Agent {
1237
1292
  unsubscribe: () => {},
1238
1293
  reconnectUnsubscribe: () => {},
1239
1294
  frameTail: Promise.resolve(),
1295
+ settledPromptCorrelations: [],
1240
1296
  inboundSequence: 0,
1297
+ connectionId: adapter.connectionId,
1241
1298
  busy: false,
1242
1299
  toolArgs: new Map(),
1243
1300
  };
@@ -1414,7 +1471,7 @@ export class AcpAgent implements Agent {
1414
1471
 
1415
1472
  #adapter(id: string): AcpSdkAdapter {
1416
1473
  const record = this.#sessions.get(id);
1417
- if (!record) throw new AcpSdkAdapterError("not_found", `Unsupported ACP session: ${id}`);
1474
+ if (!record) throw new AcpSdkAdapterError("not_found", `Unknown session, not found: ${id}`);
1418
1475
  return record.adapter;
1419
1476
  }
1420
1477
 
@@ -1434,29 +1491,18 @@ export class AcpAgent implements Agent {
1434
1491
  }
1435
1492
 
1436
1493
  #reverseConnection(sessionId: string): AcpReverseConnection {
1437
- const methods: Record<string, string> = {
1438
- "fs.readTextFile": "readTextFile",
1439
- "fs.writeTextFile": "writeTextFile",
1440
- "terminal.create": "createTerminal",
1441
- "permission.request": "requestPermission",
1442
- "ui.elicit": "unstable_createElicitation",
1443
- };
1444
- return {
1445
- request: async (method: string, params: JsonObject): Promise<unknown> => {
1446
- const name = methods[method] ?? method;
1447
- const target = (this.#connection as unknown as Record<string, unknown>)[name];
1448
- if (typeof target !== "function")
1449
- throw new AcpSdkAdapterError("acp_reverse_unavailable", `ACP reverse method is unavailable: ${method}`);
1450
- const request = method === "permission.request" ? { ...params, sessionId } : params;
1451
- return await (target as (input: JsonObject) => Promise<unknown>)(request);
1452
- },
1453
- };
1494
+ return createAcpReverseConnection(this.#connection, sessionId);
1454
1495
  }
1455
1496
 
1456
1497
  #observeSessionActivity(record: SessionRecord, frame: JsonObject): void {
1498
+ if (frame.type === "activity") {
1499
+ if (frame.state === "busy") record.busy = true;
1500
+ else if (frame.state === "idle") record.busy = false;
1501
+ return;
1502
+ }
1457
1503
  const event = receivedSdkEvent(frame)?.event;
1458
1504
  if (event?.type === "agent_start") record.busy = true;
1459
- else if (event?.type === "agent_end") record.busy = false;
1505
+ else if (event?.type === "agent_end" || event?.type === "agent_failed") record.busy = false;
1460
1506
  }
1461
1507
 
1462
1508
  #frameProcessingFailure(error: unknown): AcpSdkAdapterError {
@@ -1468,22 +1514,72 @@ export class AcpAgent implements Agent {
1468
1514
  #enqueueSdkFrame(id: string, adapter: AcpSdkAdapter, frame: JsonObject): void {
1469
1515
  const record = this.#sessions.get(id);
1470
1516
  if (!record || record.adapter !== adapter) return;
1471
- // Sequence and busy state are captured at ingress, before queued work begins.
1472
- // A frame received before acknowledgement stays before that prompt's boundary.
1517
+ // Ingress ordering is recorded before queued work begins.
1473
1518
  this.#observeSessionActivity(record, frame);
1474
- const sequence = ++record.inboundSequence;
1475
- const task = record.frameTail.then(async () => await this.#handleSdkFrame(id, adapter, frame, sequence));
1519
+ ++record.inboundSequence;
1520
+ const task = record.frameTail.then(async () => await this.#handleSdkFrame(id, adapter, frame));
1476
1521
  record.frameTail = task.catch(
1477
1522
  async error => await this.#failSession(id, adapter, this.#frameProcessingFailure(error)),
1478
1523
  );
1479
1524
  }
1480
1525
 
1481
- async #handleSdkFrame(id: string, adapter: AcpSdkAdapter, frame: JsonObject, sequence: number): Promise<void> {
1526
+ async #handleSdkFrame(id: string, adapter: AcpSdkAdapter, frame: JsonObject): Promise<void> {
1482
1527
  const record = this.#sessions.get(id);
1483
1528
  if (!record || record.adapter !== adapter) return;
1529
+ if ((frame.type === "hello" || frame.type === "server_hello") && typeof frame.connectionId === "string") {
1530
+ const reconnected = record.connectionId !== undefined && record.connectionId !== frame.connectionId;
1531
+ record.connectionId = frame.connectionId;
1532
+ if (reconnected) {
1533
+ const waiter = record.activePrompt;
1534
+ if (waiter && !waiter.settled && !waiter.terminal) {
1535
+ record.activePrompt = undefined;
1536
+ waiter.settled = true;
1537
+ if (hasCorrelation(waiter.correlation)) {
1538
+ record.settledPromptCorrelations.push(waiter.correlation);
1539
+ while (record.settledPromptCorrelations.length > SETTLED_PROMPT_CORRELATION_RETENTION)
1540
+ record.settledPromptCorrelations.shift();
1541
+ }
1542
+ waiter.reject(
1543
+ new AcpSdkAdapterError(
1544
+ "connection_closed",
1545
+ "The prompt owner connection was lost before completion.",
1546
+ ),
1547
+ );
1548
+ }
1549
+ }
1550
+ return;
1551
+ }
1484
1552
  const received = receivedSdkEvent(frame);
1485
1553
  if (!received) return;
1486
1554
  const { event, wirePayload } = received;
1555
+ const isTerminal = event.type === "agent_end" || event.type === "agent_failed";
1556
+ const correlation = (isTerminal ? strictCorrelationFrom(frame, event) : correlationFrom(frame, event)) ?? {};
1557
+ const activePrompt = record.activePrompt;
1558
+ const outcome = isTerminal ? terminalOutcome(event) : undefined;
1559
+ if (isTerminal) {
1560
+ // Terminal ownership requires a complete identity. Unowned, partial, and
1561
+ // duplicate terminals are never allowed to publish or query anything.
1562
+ if (!hasCompleteCorrelation(correlation) || !activePrompt || activePrompt.settled) return;
1563
+ if (!activePrompt.acknowledged) {
1564
+ // Hold the entire frame until the prompt acknowledgement proves ownership.
1565
+ activePrompt.deferredFrames.push(frame);
1566
+ return;
1567
+ }
1568
+ if (!correlationsExactlyMatch(activePrompt.correlation, correlation) || activePrompt.terminal) return;
1569
+ if (!outcome) {
1570
+ const detail =
1571
+ typeof (event as { error?: { message?: unknown } }).error?.message === "string"
1572
+ ? (event as { error: { message: string } }).error.message
1573
+ : "the prompt terminal omitted a valid normalized outcome";
1574
+ this.#rejectPrompt(
1575
+ record,
1576
+ activePrompt,
1577
+ new AcpSdkAdapterError("connection_closed", `ACP prompt terminal was invalid: ${detail}`),
1578
+ );
1579
+ return;
1580
+ }
1581
+ activePrompt.terminal = { outcome, correlation };
1582
+ }
1487
1583
  const toolCallId = typeof event.toolCallId === "string" ? event.toolCallId : undefined;
1488
1584
  if (
1489
1585
  toolCallId &&
@@ -1492,31 +1588,21 @@ export class AcpAgent implements Agent {
1492
1588
  ) {
1493
1589
  record.toolArgs.set(toolCallId, event.args);
1494
1590
  }
1495
- const correlation = correlationFrom(frame, event);
1496
- const activePrompt = record.activePrompt;
1497
- // Prompt ownership is updated before publishing client notifications, but a
1498
- // terminal waiter is resolved only after acknowledgement and publication
1499
- // succeed. Fast hosts may emit the whole turn before acknowledging the
1500
- // control request; retaining that activity prevents an idle ACP prompt from
1501
- // remaining open forever.
1502
- if (
1503
- activePrompt &&
1504
- sequence > activePrompt.boundary &&
1505
- !correlationsConflict(activePrompt.correlation, correlation)
1506
- ) {
1507
- if (isPromptActivity(String(event.type))) {
1508
- activePrompt.activityObserved = true;
1509
- } else if (event.type === "agent_end") {
1510
- if (
1511
- correlationsMatch(activePrompt.correlation, correlation) ||
1512
- correlation.commandId !== undefined ||
1513
- correlation.turnId !== undefined ||
1514
- activePrompt.activityObserved ||
1515
- activePrompt.steeringAtAcknowledgement
1516
- )
1517
- activePrompt.pendingTerminal = correlation;
1591
+ const settledCorrelation = record.settledPromptCorrelations.some(settled =>
1592
+ correlationsMatch(settled, correlation),
1593
+ );
1594
+ if (settledCorrelation) {
1595
+ // Frames for an already-settled correlation stay closed until an active prompt
1596
+ // acknowledges the exact same identity.
1597
+ if (activePrompt && !activePrompt.settled && !activePrompt.acknowledged) {
1598
+ activePrompt.deferredFrames.push(frame);
1599
+ return;
1518
1600
  }
1601
+ if (!activePrompt || activePrompt.settled || !correlationsMatch(activePrompt.correlation, correlation)) return;
1519
1602
  }
1603
+ // After a correlated settlement with no active prompt, correlationless wire frames
1604
+ // have no prompt to belong to and must not publish further updates.
1605
+ if (!record.activePrompt && record.settledPromptCorrelations.length > 0 && !hasCorrelation(correlation)) return;
1520
1606
  if (wirePayload) {
1521
1607
  for (const notification of mapAgentWireEventPayloadToAcpSessionUpdates(wirePayload as never, id, {
1522
1608
  cwd: record.cwd,
@@ -1582,21 +1668,47 @@ export class AcpAgent implements Agent {
1582
1668
  }
1583
1669
  }
1584
1670
  await this.#emitEndOfTurnUpdates(id, adapter);
1671
+ } else if (event.type === "agent_failed") {
1672
+ await this.#emitEndOfTurnUpdates(id, adapter);
1585
1673
  }
1586
1674
  if (activePrompt) this.#settlePrompt(record, activePrompt);
1587
1675
  }
1588
1676
 
1677
+ #rejectPrompt(record: SessionRecord, waiter: PromptWaiter, error: AcpSdkAdapterError): void {
1678
+ if (record.activePrompt !== waiter || waiter.settled) return;
1679
+ record.activePrompt = undefined;
1680
+ waiter.settled = true;
1681
+ waiter.deferredFrames.length = 0;
1682
+ waiter.terminal = undefined;
1683
+ if (hasCompleteCorrelation(waiter.correlation)) {
1684
+ record.settledPromptCorrelations.push(waiter.correlation);
1685
+ while (record.settledPromptCorrelations.length > SETTLED_PROMPT_CORRELATION_RETENTION)
1686
+ record.settledPromptCorrelations.shift();
1687
+ }
1688
+ waiter.reject(error);
1689
+ }
1690
+
1589
1691
  #settlePrompt(record: SessionRecord, waiter: PromptWaiter): void {
1590
- if (record.activePrompt !== waiter || !waiter.acknowledged || !waiter.pendingTerminal) return;
1591
- if (correlationsConflict(waiter.correlation, waiter.pendingTerminal)) return;
1592
- if (
1593
- !correlationsMatch(waiter.correlation, waiter.pendingTerminal) &&
1594
- !waiter.activityObserved &&
1595
- !waiter.steeringAtAcknowledgement
1596
- )
1692
+ if (record.activePrompt !== waiter || waiter.settled || !waiter.acknowledged || !waiter.terminal) return;
1693
+ // A terminal captured before acknowledgement is only this prompt's terminal when the
1694
+ // eventual acknowledgement correlates with it; otherwise it belonged to an earlier prompt.
1695
+ if (!correlationsExactlyMatch(waiter.correlation, waiter.terminal.correlation)) {
1696
+ waiter.terminal = undefined;
1597
1697
  return;
1698
+ }
1598
1699
  record.activePrompt = undefined;
1599
- waiter.resolve({ stopReason: waiter.cancelRequested ? "cancelled" : "end_turn" });
1700
+ waiter.settled = true;
1701
+ if (hasCorrelation(waiter.correlation)) {
1702
+ record.settledPromptCorrelations.push(waiter.correlation);
1703
+ while (record.settledPromptCorrelations.length > SETTLED_PROMPT_CORRELATION_RETENTION)
1704
+ record.settledPromptCorrelations.shift();
1705
+ }
1706
+ const { outcome } = waiter.terminal;
1707
+ if (outcome.kind === "stopped") {
1708
+ waiter.resolve({ stopReason: outcome.reason });
1709
+ return;
1710
+ }
1711
+ waiter.reject(new AcpSdkAdapterError(outcome.code, outcome.message));
1600
1712
  }
1601
1713
 
1602
1714
  async #emitEndOfTurnUpdates(id: string, adapter: AcpSdkAdapter): Promise<void> {
@@ -1670,7 +1782,7 @@ export class AcpAgent implements Agent {
1670
1782
  rejectUnavailableStartupPreset = false,
1671
1783
  ): Promise<Pick<NewSessionResponse, "configOptions" | "modes">> {
1672
1784
  const record = this.#sessions.get(id);
1673
- if (!record) throw new AcpSdkAdapterError("not_found", `Unsupported ACP session: ${id}`);
1785
+ if (!record) throw new AcpSdkAdapterError("not_found", `Unknown session, not found: ${id}`);
1674
1786
  const modelPreset = this.#startupOptions?.modelPreset;
1675
1787
  const [config, modelCatalog] = await Promise.all([
1676
1788
  record.adapter.query("config.list/get"),