@gajae-code/coding-agent 0.16.3 → 0.16.6

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 (277) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/dist/types/capability/types.d.ts +4 -0
  3. package/dist/types/cli/args.d.ts +2 -2
  4. package/dist/types/cli/customize-doctor.d.ts +22 -0
  5. package/dist/types/cli/root-flags.d.ts +4 -2
  6. package/dist/types/cli.d.ts +1 -1
  7. package/dist/types/commands/launch.d.ts +1 -1
  8. package/dist/types/config/autorouting-tier-map.d.ts +3 -0
  9. package/dist/types/config/file-lock.d.ts +31 -3
  10. package/dist/types/config/model-registry.d.ts +4 -0
  11. package/dist/types/config/model-resolver.d.ts +9 -0
  12. package/dist/types/config/settings-schema.d.ts +29 -1
  13. package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
  14. package/dist/types/coordinator-mcp/server.d.ts +7 -0
  15. package/dist/types/discovery/claude-plugins.d.ts +3 -1
  16. package/dist/types/discovery/helpers.d.ts +35 -0
  17. package/dist/types/exec/bash-executor.d.ts +17 -1
  18. package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
  19. package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
  20. package/dist/types/exec/bash-shell-signals.d.ts +2 -0
  21. package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
  22. package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
  23. package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
  24. package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
  25. package/dist/types/exec/bash-shell-worker.d.ts +1 -0
  26. package/dist/types/exec/isolated-shell.d.ts +19 -0
  27. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
  28. package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
  29. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  30. package/dist/types/extensibility/extensions/runner.d.ts +46 -6
  31. package/dist/types/extensibility/extensions/types.d.ts +16 -0
  32. package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
  33. package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
  34. package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
  35. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
  36. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  37. package/dist/types/extensibility/hooks/types.d.ts +4 -0
  38. package/dist/types/extensibility/runtime-skill-discovery.d.ts +5 -2
  39. package/dist/types/extensibility/shared-events.d.ts +1 -1
  40. package/dist/types/extensibility/skill-management.d.ts +9 -4
  41. package/dist/types/extensibility/skills.d.ts +2 -0
  42. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
  43. package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
  44. package/dist/types/gjc-runtime/session-state-lock.d.ts +14 -0
  45. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
  46. package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
  47. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  48. package/dist/types/modes/components/bash-execution.d.ts +1 -0
  49. package/dist/types/modes/components/execution-shared.d.ts +1 -0
  50. package/dist/types/modes/components/settings-selector.d.ts +6 -1
  51. package/dist/types/modes/components/status-line/command.d.ts +35 -0
  52. package/dist/types/modes/components/status-line/index.d.ts +1 -0
  53. package/dist/types/modes/components/status-line/types.d.ts +5 -0
  54. package/dist/types/modes/components/tool-status-header.d.ts +14 -0
  55. package/dist/types/modes/controllers/event-controller.d.ts +6 -0
  56. package/dist/types/modes/interactive-mode.d.ts +3 -0
  57. package/dist/types/modes/types.d.ts +7 -0
  58. package/dist/types/runtime-mcp/client.d.ts +9 -1
  59. package/dist/types/runtime-mcp/manager.d.ts +7 -1
  60. package/dist/types/runtime-mcp/pool.d.ts +22 -3
  61. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  62. package/dist/types/runtime-mcp/types.d.ts +22 -0
  63. package/dist/types/sdk/broker/ensure.d.ts +32 -0
  64. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
  65. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  66. package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
  67. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
  68. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
  69. package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
  70. package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
  71. package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
  72. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  73. package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
  74. package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
  75. package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
  76. package/dist/types/sdk/client/client.d.ts +6 -0
  77. package/dist/types/sdk/host/host.d.ts +3 -1
  78. package/dist/types/sdk/host/session-runtime.d.ts +26 -1
  79. package/dist/types/sdk/lifecycle/service.d.ts +1 -0
  80. package/dist/types/sdk/router/session-router.d.ts +10 -2
  81. package/dist/types/sdk/session-api-key.d.ts +26 -0
  82. package/dist/types/sdk/session.d.ts +5 -1
  83. package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
  84. package/dist/types/session/agent-session.d.ts +11 -0
  85. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  86. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  87. package/dist/types/session/messages.d.ts +1 -0
  88. package/dist/types/session/session-manager.d.ts +3 -1
  89. package/dist/types/setup/paseo/announce.d.ts +112 -0
  90. package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
  91. package/dist/types/skill-state/canonical-skills.d.ts +4 -0
  92. package/dist/types/system-prompt.d.ts +10 -0
  93. package/dist/types/task/executor.d.ts +5 -1
  94. package/dist/types/tools/bash.d.ts +1 -0
  95. package/dist/types/tools/fetch.d.ts +7 -0
  96. package/dist/types/tools/github-cache.d.ts +2 -1
  97. package/dist/types/tools/index.d.ts +10 -0
  98. package/dist/types/tools/path-utils.d.ts +2 -0
  99. package/dist/types/tools/search.d.ts +6 -0
  100. package/package.json +9 -7
  101. package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
  102. package/scripts/generate-sdk-operation-inventory.ts +11 -0
  103. package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
  104. package/src/capability/index.ts +14 -2
  105. package/src/capability/types.ts +4 -0
  106. package/src/cli/args.ts +20 -9
  107. package/src/cli/customize-doctor.ts +155 -12
  108. package/src/cli/root-flags.ts +5 -3
  109. package/src/cli.ts +22 -0
  110. package/src/commands/sdk.ts +74 -31
  111. package/src/config/autorouting-tier-map.ts +1 -0
  112. package/src/config/file-lock-gc.ts +40 -2
  113. package/src/config/file-lock.ts +379 -31
  114. package/src/config/model-preset-registry.ts +120 -19
  115. package/src/config/model-profiles.ts +40 -0
  116. package/src/config/model-registry.ts +73 -23
  117. package/src/config/model-resolver.ts +48 -0
  118. package/src/config/settings-schema.ts +33 -1
  119. package/src/config/settings.ts +1 -0
  120. package/src/coordinator-mcp/question-state.ts +33 -2
  121. package/src/coordinator-mcp/server.ts +477 -101
  122. package/src/crash/sanitize.ts +1 -1
  123. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
  124. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
  125. package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
  126. package/src/discovery/builtin.ts +52 -55
  127. package/src/discovery/claude-plugins.ts +12 -4
  128. package/src/discovery/helpers.ts +307 -34
  129. package/src/exec/bash-executor.ts +330 -50
  130. package/src/exec/bash-shell-guardian.ts +388 -0
  131. package/src/exec/bash-shell-runtime-entry.ts +5 -0
  132. package/src/exec/bash-shell-signals.ts +29 -0
  133. package/src/exec/bash-shell-supervisor-entry.ts +3 -0
  134. package/src/exec/bash-shell-supervisor.ts +359 -0
  135. package/src/exec/bash-shell-worker-entry.ts +5 -0
  136. package/src/exec/bash-shell-worker-protocol.ts +36 -0
  137. package/src/exec/bash-shell-worker.ts +116 -0
  138. package/src/exec/isolated-shell.ts +393 -0
  139. package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
  140. package/src/extensibility/extensions/function-hooks.ts +959 -0
  141. package/src/extensibility/extensions/index.ts +22 -0
  142. package/src/extensibility/extensions/loader.ts +53 -2
  143. package/src/extensibility/extensions/runner.ts +895 -264
  144. package/src/extensibility/extensions/types.ts +42 -0
  145. package/src/extensibility/extensions/wrapper.ts +56 -15
  146. package/src/extensibility/gjc-plugins/compiler.ts +228 -60
  147. package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
  148. package/src/extensibility/gjc-plugins/installer.ts +13 -12
  149. package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
  150. package/src/extensibility/gjc-plugins/paths.ts +13 -0
  151. package/src/extensibility/gjc-plugins/registry.ts +113 -1
  152. package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
  153. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
  154. package/src/extensibility/gjc-plugins/schema.ts +121 -4
  155. package/src/extensibility/gjc-plugins/types.ts +25 -0
  156. package/src/extensibility/hooks/loader.ts +133 -16
  157. package/src/extensibility/hooks/types.ts +28 -0
  158. package/src/extensibility/runtime-skill-discovery.ts +149 -190
  159. package/src/extensibility/shared-events.ts +1 -1
  160. package/src/extensibility/skill-management.ts +200 -52
  161. package/src/extensibility/skills.ts +73 -25
  162. package/src/gjc-runtime/empty-delete-gc.ts +94 -4
  163. package/src/gjc-runtime/gc-runtime.ts +29 -0
  164. package/src/gjc-runtime/linux-proc.ts +14 -0
  165. package/src/gjc-runtime/repository-binding.ts +1 -1
  166. package/src/gjc-runtime/session-state-lock.ts +143 -10
  167. package/src/gjc-runtime/session-state-sidecar.ts +27 -11
  168. package/src/gjc-runtime/state-writer.ts +30 -29
  169. package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
  170. package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
  171. package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
  172. package/src/gjc-runtime/workflow-manifest.ts +16 -0
  173. package/src/harness-control-plane/session-lease.ts +2 -2
  174. package/src/internal-urls/docs-index.generated.ts +12 -12
  175. package/src/lsp/config.ts +10 -3
  176. package/src/lsp/index.ts +1 -1
  177. package/src/lsp/path-trust.ts +69 -14
  178. package/src/main.ts +78 -35
  179. package/src/modes/DESIGN.md +54 -0
  180. package/src/modes/acp/acp-agent.ts +16 -3
  181. package/src/modes/components/assistant-message.ts +22 -9
  182. package/src/modes/components/bash-execution.ts +4 -1
  183. package/src/modes/components/execution-shared.ts +4 -1
  184. package/src/modes/components/model-selector.ts +166 -33
  185. package/src/modes/components/session-observer-overlay.ts +1 -1
  186. package/src/modes/components/settings-selector.ts +1075 -235
  187. package/src/modes/components/status-line/command.ts +190 -0
  188. package/src/modes/components/status-line/index.ts +1 -0
  189. package/src/modes/components/status-line/segments.ts +32 -5
  190. package/src/modes/components/status-line/types.ts +5 -0
  191. package/src/modes/components/tool-status-header.ts +396 -22
  192. package/src/modes/controllers/command-controller.ts +3 -0
  193. package/src/modes/controllers/event-controller.ts +151 -7
  194. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  195. package/src/modes/controllers/input-controller.ts +6 -3
  196. package/src/modes/controllers/selector-controller.ts +21 -0
  197. package/src/modes/interactive-mode.ts +53 -6
  198. package/src/modes/print-mode.ts +5 -1
  199. package/src/modes/prompt-action-autocomplete.ts +54 -27
  200. package/src/modes/runtime-init.ts +2 -0
  201. package/src/modes/types.ts +17 -3
  202. package/src/modes/utils/ui-helpers.ts +2 -0
  203. package/src/prompts/system/system-prompt.md +8 -0
  204. package/src/prompts/tools/search.md +2 -0
  205. package/src/prompts/tools/subagent.md +1 -0
  206. package/src/runtime-mcp/client.ts +60 -17
  207. package/src/runtime-mcp/manager.ts +189 -14
  208. package/src/runtime-mcp/pool.test.ts +218 -0
  209. package/src/runtime-mcp/pool.ts +224 -23
  210. package/src/runtime-mcp/transports/stdio.ts +158 -24
  211. package/src/runtime-mcp/types.ts +24 -0
  212. package/src/sdk/broker/broker.ts +124 -32
  213. package/src/sdk/broker/ensure.ts +248 -41
  214. package/src/sdk/broker/lifecycle-ledger.ts +7 -17
  215. package/src/sdk/broker/lifecycle.ts +56 -6
  216. package/src/sdk/broker/startup-failure.ts +15 -6
  217. package/src/sdk/bus/chat-daemon-control.ts +3 -1
  218. package/src/sdk/bus/chat-daemon-runtime.ts +72 -6
  219. package/src/sdk/bus/chat-effect-journal.ts +132 -7
  220. package/src/sdk/bus/conversation-store.ts +29 -0
  221. package/src/sdk/bus/lifecycle-commands.ts +17 -11
  222. package/src/sdk/bus/slack-conversation.ts +1 -0
  223. package/src/sdk/bus/slack-daemon.ts +478 -49
  224. package/src/sdk/bus/slack-live-provider.ts +12 -0
  225. package/src/sdk/bus/slack-provider.ts +5 -0
  226. package/src/sdk/bus/telegram-daemon.ts +16 -2
  227. package/src/sdk/bus/topic-registry.ts +19 -0
  228. package/src/sdk/cli/master-cli.ts +1 -1
  229. package/src/sdk/cli/session-cli.ts +11 -8
  230. package/src/sdk/client/client.ts +66 -17
  231. package/src/sdk/host/host.ts +29 -9
  232. package/src/sdk/host/query/revision-store.ts +19 -13
  233. package/src/sdk/host/reverse-leases.ts +7 -4
  234. package/src/sdk/host/session-runtime.test.ts +52 -10
  235. package/src/sdk/host/session-runtime.ts +383 -44
  236. package/src/sdk/lifecycle/service.ts +4 -0
  237. package/src/sdk/protocol/operation-inventory.generated.json +77 -0
  238. package/src/sdk/router/session-router.ts +89 -38
  239. package/src/sdk/session-api-key.ts +94 -0
  240. package/src/sdk/session.ts +228 -55
  241. package/src/sdk/transport/relay.ts +35 -13
  242. package/src/sdk/transport/serve-cli.ts +3 -3
  243. package/src/session/agent-session.ts +240 -90
  244. package/src/session/contribution-prep.ts +214 -21
  245. package/src/session/fallback-chain-controller.ts +38 -0
  246. package/src/session/internal/managed-session-storage.ts +19 -5
  247. package/src/session/messages.ts +5 -0
  248. package/src/session/session-manager.ts +29 -5
  249. package/src/session-import/redact.ts +5 -1
  250. package/src/setup/paseo/announce.ts +456 -0
  251. package/src/setup/paseo/setup-deps.ts +15 -3
  252. package/src/skill-state/canonical-skills.ts +12 -0
  253. package/src/skills/skills.test.ts +41 -21
  254. package/src/slash-commands/builtin-registry.ts +1 -0
  255. package/src/system-prompt.ts +29 -3
  256. package/src/task/executor.ts +7 -1
  257. package/src/task/index.ts +21 -4
  258. package/src/tools/bash.ts +63 -19
  259. package/src/tools/cron.ts +1 -1
  260. package/src/tools/fetch.ts +29 -1
  261. package/src/tools/gh.ts +26 -3
  262. package/src/tools/github-cache.ts +68 -16
  263. package/src/tools/index.ts +10 -0
  264. package/src/tools/job.ts +21 -4
  265. package/src/tools/monitor.ts +8 -2
  266. package/src/tools/path-utils.ts +7 -0
  267. package/src/tools/read.ts +14 -2
  268. package/src/tools/render-utils.ts +1 -3
  269. package/src/tools/resource-gc.ts +28 -8
  270. package/src/tools/search.ts +116 -58
  271. package/src/tools/skill-discovery.ts +5 -0
  272. package/src/tools/skill.ts +37 -2
  273. package/src/tools/subagent.ts +6 -3
  274. package/src/tools/tool-catalog.generated.ts +10 -3
  275. package/src/tools/ultragoal-ask-guard.ts +2 -2
  276. package/src/web/insane/bridge.ts +113 -23
  277. package/src/web/search/providers/perplexity.ts +102 -41
@@ -4,7 +4,7 @@ import * as crypto from "node:crypto";
4
4
  import * as fs from "node:fs/promises";
5
5
  import * as path from "node:path";
6
6
  import { promisify } from "node:util";
7
- import { ThinkingLevel } from "@gajae-code/agent-core";
7
+ import { isContinuingMidRunMaintenanceOutcome, ThinkingLevel } from "@gajae-code/agent-core";
8
8
  import type { Api, ImageContent, Model } from "@gajae-code/ai/core";
9
9
  import { logger } from "@gajae-code/utils";
10
10
  import { AsyncJobManager } from "../../async";
@@ -27,7 +27,10 @@ import { resolveModelChainWithAuth, splitSelectorThinkingSuffix } from "../../co
27
27
  import { type ModelSelectorValue, normalizeModelSelectorValue } from "../../config/model-selector-value";
28
28
  import { type Settings, validateSettingPatch } from "../../config/settings";
29
29
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../../extensibility/extensions";
30
+ import type { AgentEndEvent } from "../../extensibility/shared-events";
30
31
  import { normalizeGoal } from "../../goals/state";
32
+ import { toAgentWireEventPayload } from "../../modes/shared/agent-wire/event-envelope";
33
+ import type { AgentSessionEvent } from "../../session/agent-session";
31
34
  import type { SdkRunCapability } from "../../session/sdk-run-capability";
32
35
  import {
33
36
  boundTerminalRetentionState,
@@ -192,6 +195,24 @@ class DiffQueryError extends Error {
192
195
  }
193
196
  }
194
197
 
198
+ /**
199
+ * Normalize an agent-loop stop reason into the published terminal outcome.
200
+ *
201
+ * Only reached for a boundary that actually terminalizes an owned invocation,
202
+ * so every result is a `stopped` disposition: a failed run publishes its own
203
+ * correlated `agent_failed` frame with the sanitized cause instead.
204
+ */
205
+ export function terminalStoppedOutcome(
206
+ stopReason: AgentEndEvent["stopReason"],
207
+ maintenanceOutcome: string | undefined,
208
+ ): Extract<SdkPromptTerminalOutcome, { kind: "stopped" }> {
209
+ // An aborted maintenance checkpoint is the one maintenance path that really
210
+ // ends the run, and it ends it the same way an explicit stop does.
211
+ return stopReason === "cancelled" || maintenanceOutcome === "aborted"
212
+ ? { kind: "stopped", reason: "cancelled", provenance: "client_cancel" }
213
+ : { kind: "stopped", reason: "end_turn", provenance: "agent" };
214
+ }
215
+
195
216
  /** Transport-neutral endpoint contract consumed by the SDK session runtime. */
196
217
  export interface SessionSdkTransport {
197
218
  readonly sessionId: string;
@@ -395,6 +416,26 @@ export class SessionSdkSessionRuntime {
395
416
  this.transport.broadcastFrame?.(event);
396
417
  }
397
418
 
419
+ /**
420
+ * Deliver a frame to specific connections without recording it.
421
+ *
422
+ * Deliberately not `emitEvent`: that assigns a ring position and broadcasts to
423
+ * every authenticated socket. Streamed turn content is high-frequency and
424
+ * unbounded in size, so a count-bounded ring would evict the lifecycle events
425
+ * clients depend on -- including the `agent_end` that settles their prompt --
426
+ * and broadcasting would hand every subscriber a stream only the submitter
427
+ * asked for. Delivery failure is ignored: a disconnected consumer must never
428
+ * disturb the turn producing the content.
429
+ */
430
+ sendFrameTo(connectionId: string, frame: SdkFrame): void {
431
+ try {
432
+ const result = this.transport.sendFrame(connectionId, frame);
433
+ if (result instanceof Promise) result.catch(() => undefined);
434
+ } catch {
435
+ // A dead connection is reaped by the transport's own close handling.
436
+ }
437
+ }
438
+
398
439
  publish(frame: SdkFrame): void {
399
440
  this.emitEvent(frame);
400
441
  }
@@ -448,13 +489,13 @@ export class SessionSdkSessionRuntime {
448
489
  }
449
490
  }
450
491
 
451
- async stop(): Promise<void> {
492
+ async stop(options: { allowLockContention?: boolean } = {}): Promise<void> {
452
493
  this.#connectionDisposer?.();
453
494
  this.#capabilitiesDisposer?.();
454
495
  this.#malformedDisposer?.();
455
496
  let hostError: unknown;
456
497
  try {
457
- await this.host.stop();
498
+ await this.host.stop(options);
458
499
  } catch (error) {
459
500
  hostError = error;
460
501
  } finally {
@@ -577,6 +618,8 @@ function canonicalTerminalOutcome(
577
618
  if (candidate.kind === "stopped") {
578
619
  if (candidate.reason === "cancelled" && candidate.provenance === "client_cancel")
579
620
  return { kind: "stopped", reason: "cancelled", provenance: "client_cancel" };
621
+ if (candidate.reason === "end_turn" && candidate.provenance === "agent")
622
+ return { kind: "stopped", reason: "end_turn", provenance: "agent" };
580
623
  return undefined;
581
624
  }
582
625
  if (candidate.kind === "failed")
@@ -2239,6 +2282,7 @@ function createControlSurface(
2239
2282
  canResolveGate: () => boolean = () => true,
2240
2283
  trackGateResolution: <T>(resolution: Promise<T>) => Promise<T> = async resolution => await resolution,
2241
2284
  onInvocationCompletionReconciledForTests?: (kind: InvocationKind, correlation: InvocationCorrelation) => void,
2285
+ publishLifecycleFrame?: (frame: SdkFrame) => void,
2242
2286
  ): ControlSurface {
2243
2287
  const normalizePromptImages = (value: unknown): ImageContent[] => {
2244
2288
  if (!Array.isArray(value)) return [];
@@ -2376,6 +2420,24 @@ function createControlSurface(
2376
2420
  const correlation = newCorrelation();
2377
2421
  const sdkRunToken = `${correlation.commandId}:${correlation.turnId}`;
2378
2422
  const sdkRunCapability = createSdkRunCapability(sdkRunToken);
2423
+ const publishTerminal = (outcome: InvocationOutcome): void => {
2424
+ publishLifecycleFrame?.({
2425
+ type: "agent_end",
2426
+ sessionId: ctx.sessionManager.getSessionId(),
2427
+ ...correlation,
2428
+ outcome,
2429
+ });
2430
+ };
2431
+ const publishFailure = (error: unknown): void => {
2432
+ const failure = sanitizePromptFailure(error);
2433
+ publishLifecycleFrame?.({
2434
+ type: "agent_failed",
2435
+ sessionId: ctx.sessionManager.getSessionId(),
2436
+ ...correlation,
2437
+ error: failure,
2438
+ });
2439
+ publishTerminal(canonicalFailedOutcome(failure));
2440
+ };
2379
2441
  const preflight = Promise.withResolvers<void>();
2380
2442
  let accepted = false;
2381
2443
  let settled = false;
@@ -2484,11 +2546,25 @@ function createControlSurface(
2484
2546
  retirePendingOwner?.(kind, correlation);
2485
2547
  const attemptTerminalization = async (remaining: number): Promise<void> => {
2486
2548
  try {
2549
+ const before = reconciliation.lookup(kind, correlation) as {
2550
+ status?: string;
2551
+ };
2552
+ const alreadyTerminal = before.status === "terminal_ok" || before.status === "failed";
2487
2553
  await reconciliation.noteTransition(kind, correlation, {
2488
2554
  type: "agent_end",
2489
2555
  ...(typeof result === "string" ? { content: sanitizeTurnResultContent(result) } : {}),
2490
2556
  });
2491
2557
  onInvocationCompletionReconciledForTests?.(kind, correlation);
2558
+ if (!alreadyTerminal) {
2559
+ const settledRecord = reconciliation.lookup(kind, correlation) as {
2560
+ status?: string;
2561
+ outcome?: unknown;
2562
+ error?: { code?: unknown; message?: unknown };
2563
+ };
2564
+ const outcome = canonicalTerminalOutcome(settledRecord.outcome, settledRecord.error);
2565
+ if (outcome?.kind === "failed") publishFailure(settledRecord.error ?? outcome);
2566
+ else if (outcome) publishTerminal(outcome);
2567
+ }
2492
2568
  retirePendingOwner?.(kind, correlation, "always");
2493
2569
  } catch (transitionError) {
2494
2570
  if (kind === "prompt" || kind === "skill") {
@@ -2550,6 +2626,19 @@ function createControlSurface(
2550
2626
  // so their recovery owner is this bounded in-place retry loop: the same
2551
2627
  // order, retried, until both writes land or the budget is spent.
2552
2628
  const attemptRejectionTerminalization = async (remaining: number): Promise<void> => {
2629
+ // AgentSession can reject cleanup after its durable terminal was published.
2630
+ // That rejection is diagnostic, not another outcome for this correlation.
2631
+ const record = reconciliation.lookup(kind, correlation) as { status?: string };
2632
+ if (record.status === "terminal_ok" || record.status === "failed") {
2633
+ logger.warn("SDK submission cleanup failed after terminal publication", {
2634
+ kind,
2635
+ commandId: correlation.commandId,
2636
+ turnId: correlation.turnId,
2637
+ error: sanitizePromptFailure(error),
2638
+ });
2639
+ retirePendingOwner?.(kind, correlation, "always");
2640
+ return;
2641
+ }
2553
2642
  try {
2554
2643
  await reconciliation.noteTransition(kind, correlation, { type: "agent_failed", error });
2555
2644
  } catch {
@@ -2585,6 +2674,7 @@ function createControlSurface(
2585
2674
  }
2586
2675
  try {
2587
2676
  await reconciliation.noteTransition(kind, correlation, { type: "agent_end" });
2677
+ publishFailure(error);
2588
2678
  } catch (transitionError) {
2589
2679
  if (kind === "prompt" || kind === "skill") {
2590
2680
  retirePendingOwner?.(
@@ -3940,7 +4030,11 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
3940
4030
  waitForGateResolutionQuiescence: () => Promise<void>;
3941
4031
  activeInvocation?: { kind: InvocationKind; correlation: InvocationCorrelation };
3942
4032
  drainedInvocations?: Array<{ kind: InvocationKind; correlation: InvocationCorrelation }>;
3943
- attachedInvocations?: Array<{ kind: InvocationKind; correlation: InvocationCorrelation }>;
4033
+ attachedInvocations?: Array<{
4034
+ kind: InvocationKind;
4035
+ correlation: InvocationCorrelation;
4036
+ connectionId: string | undefined;
4037
+ }>;
3944
4038
  openLifecycleBatches: Array<{
3945
4039
  epoch: number;
3946
4040
  invocations: Array<{
@@ -3949,7 +4043,27 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
3949
4043
  connectionId: string | undefined;
3950
4044
  sdkRunToken: string;
3951
4045
  }>;
3952
- attachedInvocations: Array<{ kind: InvocationKind; correlation: InvocationCorrelation }>;
4046
+ attachedInvocations: Array<{
4047
+ kind: InvocationKind;
4048
+ correlation: InvocationCorrelation;
4049
+ connectionId: string | undefined;
4050
+ }>;
4051
+ /**
4052
+ * Content frames observed before this batch's correlated agent_start
4053
+ * reached the wire. The synchronous session subscription can see a
4054
+ * message_update before emitLifecycle finishes awaiting durable start
4055
+ * persistence; holding them here keeps the producer's start/content
4056
+ * boundary for every SDK consumer. Each entry carries the recipients
4057
+ * that owned the run when the event was produced: an owner attached
4058
+ * later must not receive content from before its attachment.
4059
+ */
4060
+ startPublished: boolean;
4061
+ /** Set once the hold bound dropped content for this batch; gates the single warning. */
4062
+ heldContentDropped: boolean;
4063
+ heldContent: Array<{
4064
+ event: AgentSessionEvent;
4065
+ recipients: Array<{ correlation: InvocationCorrelation; connectionId: string | undefined }>;
4066
+ }>;
3953
4067
  }>;
3954
4068
  disposeGate?: () => void;
3955
4069
  lifecycleActive: boolean;
@@ -4133,6 +4247,56 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4133
4247
  owner.lifecycleTasks.add(task);
4134
4248
  return task;
4135
4249
  };
4250
+ /**
4251
+ * Publish one content frame per owning invocation, each carrying its own
4252
+ * correlation, so a shared run lets every submitter attribute the content to
4253
+ * its own prompt. Content is best-effort and bypasses the lifecycle replay
4254
+ * ring; the turn producing it is authoritative.
4255
+ */
4256
+ const publishContentFrames = (
4257
+ current: RuntimeState,
4258
+ event: AgentSessionEvent,
4259
+ invocations: ReadonlyArray<{ correlation: InvocationCorrelation; connectionId: string | undefined }>,
4260
+ ): void => {
4261
+ try {
4262
+ const payload = toAgentWireEventPayload(event);
4263
+ for (const invocation of invocations) {
4264
+ if (invocation.connectionId === undefined) continue;
4265
+ current.runtime.sendFrameTo(invocation.connectionId, {
4266
+ type: "event",
4267
+ kind: event.type,
4268
+ payload,
4269
+ ...invocation.correlation,
4270
+ });
4271
+ }
4272
+ } catch {
4273
+ // Streamed content is best-effort; the turn producing it is authoritative.
4274
+ }
4275
+ };
4276
+ /**
4277
+ * Upper bound on content events held for one batch while its correlated
4278
+ * agent_start is still being durably recorded. A start write normally settles
4279
+ * in milliseconds, so this is only reached when persistence is wedged. Past
4280
+ * the bound the OLDEST held content is dropped (with one warning per batch):
4281
+ * content is never published before its start, so a wedged write degrades
4282
+ * to a gap in the stream, not an inverted start/content boundary, and it
4283
+ * can never accumulate an entire response in memory. The turn's terminal
4284
+ * result remains authoritative for anything a consumer did not see.
4285
+ */
4286
+ const MAX_HELD_CONTENT_EVENTS = 256;
4287
+ /**
4288
+ * Release content that arrived before the batch's correlated agent_start
4289
+ * reached the wire. Called once per batch, after the start frames are
4290
+ * published (or after the start publication path gave up), so SDK consumers
4291
+ * observe start before any content, in producer order.
4292
+ */
4293
+ const releaseHeldContent = (current: RuntimeState, batch: LifecycleBatch): void => {
4294
+ if (batch.startPublished) return;
4295
+ batch.startPublished = true;
4296
+ const held = batch.heldContent;
4297
+ batch.heldContent = [];
4298
+ for (const { event, recipients } of held) publishContentFrames(current, event, recipients);
4299
+ };
4136
4300
  const emitLifecycle = async (
4137
4301
  type: "agent_start" | "agent_end" | "agent_failed",
4138
4302
  ctx: ExtensionContext,
@@ -4142,6 +4306,9 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4142
4306
  terminalContent?: TurnResultContent,
4143
4307
  terminalHasActivity = false,
4144
4308
  terminalOutcome?: InvocationOutcome,
4309
+ stopReason?: AgentEndEvent["stopReason"],
4310
+ startToken?: string,
4311
+ startTokens?: string[],
4145
4312
  ): Promise<void> => {
4146
4313
  const current = lifecycleOwner?.state ?? active;
4147
4314
  if (!current) return;
@@ -4216,19 +4383,46 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4216
4383
  // turn (queued-while-streaming submissions never push), so a mid-prompt
4217
4384
  // continuation agent_start with an empty queue leaves the current owner
4218
4385
  // untouched (review thread P1).
4219
- const drained = current.pending
4220
- .splice(0)
4386
+ const cohort = new Set(startTokens);
4387
+ if (startToken) cohort.add(startToken);
4388
+ const matchingPending =
4389
+ cohort.size > 0 ? current.pending.filter(entry => cohort.has(entry.sdkRunToken)) : current.pending.slice();
4390
+ const matchingKeys = new Set(matchingPending.map(entry => entry.sdkRunToken));
4391
+ const pendingSnapshot = current.pending.splice(0);
4392
+ const drained = pendingSnapshot
4393
+ .filter(entry => matchingKeys.has(entry.sdkRunToken))
4221
4394
  .filter(entry => entry.kind !== "prompt" || !current.deadlineManager.isExpiring(entry.correlation));
4395
+ current.pending.push(...pendingSnapshot.filter(entry => !matchingKeys.has(entry.sdkRunToken)));
4396
+ const existingTokenBatch =
4397
+ typeof startToken === "string" && startToken.length > 0
4398
+ ? lifecycleRunOwners.get(startToken)?.batch
4399
+ : undefined;
4222
4400
  if (drained.length > 0) {
4223
- current.openLifecycleBatches.push({
4401
+ const batch: LifecycleBatch = {
4224
4402
  epoch: current.lifecycleEpoch,
4225
4403
  invocations: drained,
4226
4404
  attachedInvocations: [],
4227
- });
4405
+ startPublished: false,
4406
+ heldContentDropped: false,
4407
+ heldContent: [],
4408
+ };
4409
+ current.openLifecycleBatches.push(batch);
4410
+ for (const entry of drained) {
4411
+ lifecycleRunOwners.set(entry.sdkRunToken, {
4412
+ state: current,
4413
+ batch,
4414
+ correlationKey: lifecycleCorrelationKey(entry.correlation),
4415
+ });
4416
+ }
4228
4417
  adoptLifecycleBatch(drained);
4229
4418
  if (current.activeInvocation?.kind === "prompt") {
4230
4419
  current.deadlineManager.onAccepted(current.activeInvocation.correlation);
4231
4420
  }
4421
+ } else if (existingTokenBatch && current.openLifecycleBatches.includes(existingTokenBatch)) {
4422
+ // A continuation within the same SDK-owned run carries the same token
4423
+ // but has no newly pending invocation. Preserve the established owner so
4424
+ // later chunks and the final terminal remain directed to its submitters.
4425
+ adoptLifecycleBatch(existingTokenBatch.invocations);
4232
4426
  } else {
4233
4427
  // An empty drain is an agent-initiated successor run. Clear the
4234
4428
  // predecessor's SDK owner and active invocation so abort ownership and
@@ -4240,7 +4434,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4240
4434
  // Pair this agent_end with the oldest unmatched start. A delayed
4241
4435
  // aborted-turn end that lands after a successor agent_start must
4242
4436
  // terminalize the aborted invocation, never the successor.
4243
- const ended = current.openLifecycleBatches[0];
4437
+ const ended = failureBatch ?? current.openLifecycleBatches[0];
4244
4438
  const baseTransitions = ended
4245
4439
  ? ended.invocations.map(({ kind, correlation }) => ({ kind, correlation }))
4246
4440
  : current.activeInvocation
@@ -4364,7 +4558,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4364
4558
  };
4365
4559
  void attempt().finally(() => skillTerminalRecoveryControllers.delete(recoveryKey));
4366
4560
  };
4367
- if (type === "agent_end" && maintenanceOutcome !== undefined && maintenanceOutcome !== "aborted") {
4561
+ if (type === "agent_end" && isContinuingMidRunMaintenanceOutcome(maintenanceOutcome)) {
4368
4562
  try {
4369
4563
  current.runtime.emitEvent({ type, sessionId: ctx.sessionManager.getSessionId() });
4370
4564
  } catch {
@@ -4485,7 +4679,50 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4485
4679
  observed = false;
4486
4680
  }
4487
4681
  }
4682
+ } else if (type === "agent_start" && transitions.length > 0) {
4683
+ for (const invocation of transitions) {
4684
+ try {
4685
+ current.runtime.emitEvent({ type, sessionId, ...invocation.correlation });
4686
+ } catch {
4687
+ observed = false;
4688
+ }
4689
+ }
4690
+ // The start frames are on the wire (or definitively failed); content
4691
+ // that the synchronous session subscription observed meanwhile must
4692
+ // follow them, never precede them.
4693
+ const started = current.openLifecycleBatches.find(candidate =>
4694
+ candidate.invocations.some(entry =>
4695
+ transitions.some(
4696
+ ({ correlation }) =>
4697
+ correlation.commandId === entry.correlation.commandId &&
4698
+ correlation.turnId === entry.correlation.turnId,
4699
+ ),
4700
+ ),
4701
+ );
4702
+ if (started) releaseHeldContent(current, started);
4703
+ } else if (type === "agent_end" && transitions.length > 0) {
4704
+ // An invocation-owned terminal must carry the identity it was accepted
4705
+ // under and the normalized outcome. A client that submitted this turn
4706
+ // matches the terminal against its own acknowledged correlation and
4707
+ // refuses an unowned one, so the bare `{ type, sessionId }` boundary left
4708
+ // every externally submitted prompt reporting `working` forever even
4709
+ // though the turn had finished -- ACP dropped it as
4710
+ // `incomplete_correlation`, and the notifications runtime has always
4711
+ // stamped both fields for exactly this reason. One frame per invocation,
4712
+ // matching the agent_failed shape above, also lets a shared run attribute
4713
+ // the boundary to each owner instead of one signal nobody can claim. A
4714
+ // continuing maintenance checkpoint never reaches this branch: it returned
4715
+ // above with the one uncorrelated frame, so it cannot settle a live prompt.
4716
+ const outcome = terminalOutcome ?? terminalStoppedOutcome(stopReason, maintenanceOutcome);
4717
+ for (const invocation of transitions) {
4718
+ try {
4719
+ current.runtime.emitEvent({ type, sessionId, ...invocation.correlation, outcome });
4720
+ } catch {
4721
+ observed = false;
4722
+ }
4723
+ }
4488
4724
  } else {
4725
+ // Host-originated runs without an invocation keep an uncorrelated frame.
4489
4726
  try {
4490
4727
  current.runtime.emitEvent({ type, sessionId });
4491
4728
  } catch {
@@ -4495,7 +4732,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4495
4732
  } catch {
4496
4733
  observed = false;
4497
4734
  }
4498
- if (type === "agent_end" && maintenanceOutcome !== undefined && maintenanceOutcome !== "aborted") return;
4735
+ if (type === "agent_end" && isContinuingMidRunMaintenanceOutcome(maintenanceOutcome)) return;
4499
4736
  if (type === "agent_end") {
4500
4737
  if (current.lifecycleEpoch !== eventLifecycleEpoch) {
4501
4738
  // A successor agent_start won the lifecycle race while this event's
@@ -4553,29 +4790,23 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4553
4790
  };
4554
4791
  api.on("agent_start", (event, ctx) => {
4555
4792
  const owner = lifecycleStateForEvent(ctx, "agent_start", event.sdkRunToken);
4556
- return trackLifecycle(async () => {
4557
- await emitLifecycle(
4558
- "agent_start",
4559
- ctx,
4560
- undefined,
4561
- undefined,
4562
- owner ? { state: owner, sessionId: owner.sessionId } : undefined,
4563
- );
4564
- if (owner && typeof event.sdkRunToken === "string" && event.sdkRunToken.length > 0) {
4565
- const batch = owner.openLifecycleBatches.at(-1);
4566
- const entry = batch?.invocations.find(item => item.sdkRunToken === event.sdkRunToken);
4567
- const previous = lifecycleRunOwners.get(event.sdkRunToken);
4568
- lifecycleRunOwners.set(event.sdkRunToken, {
4569
- state: owner,
4570
- batch,
4571
- ...(previous?.correlationKey !== undefined
4572
- ? { correlationKey: previous.correlationKey }
4573
- : entry
4574
- ? { correlationKey: lifecycleCorrelationKey(entry.correlation) }
4575
- : {}),
4576
- });
4577
- }
4578
- }, owner);
4793
+ return trackLifecycle(
4794
+ async () =>
4795
+ emitLifecycle(
4796
+ "agent_start",
4797
+ ctx,
4798
+ undefined,
4799
+ undefined,
4800
+ owner ? { state: owner, sessionId: owner.sessionId } : undefined,
4801
+ undefined,
4802
+ false,
4803
+ undefined,
4804
+ undefined,
4805
+ typeof event.sdkRunToken === "string" ? event.sdkRunToken : undefined,
4806
+ event.sdkRunTokens,
4807
+ ),
4808
+ owner,
4809
+ );
4579
4810
  });
4580
4811
  api.on("agent_end", (event, ctx) => {
4581
4812
  const tokenBinding =
@@ -4608,11 +4839,20 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4608
4839
  const failureAlreadyPublished = failure === undefined || (hasExistingFailure && genericFailureKeys.length === 0);
4609
4840
  const terminalEvidence = promptTerminalEvidenceFromAgentEnd(event);
4610
4841
  const terminalOutcome =
4611
- event.stopReason === "cancelled"
4612
- ? ({ kind: "stopped", reason: "cancelled", provenance: "client_cancel" } as const)
4613
- : failure === undefined
4614
- ? undefined
4615
- : canonicalFailedOutcome(failure);
4842
+ event.stopReason === "cancelled" ||
4843
+ (event.stopReason === "maintenance" && event.maintenanceOutcome === "aborted")
4844
+ ? terminalStoppedOutcome(
4845
+ event.stopReason,
4846
+ event.stopReason === "maintenance" ? event.maintenanceOutcome : undefined,
4847
+ )
4848
+ : failure !== undefined
4849
+ ? canonicalFailedOutcome(failure)
4850
+ : terminalEvidence.content?.text.trim() || terminalEvidence.hasActivity
4851
+ ? terminalStoppedOutcome(
4852
+ event.stopReason,
4853
+ event.stopReason === "maintenance" ? event.maintenanceOutcome : undefined,
4854
+ )
4855
+ : canonicalFailedOutcome(EMPTY_PROMPT_FAILURE);
4616
4856
  return trackLifecycle(async () => {
4617
4857
  if (failure && !failureAlreadyPublished) {
4618
4858
  for (const key of genericFailureKeys) owner?.failureDiagnosticKeys.delete(key);
@@ -4627,6 +4867,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4627
4867
  terminalEvidence.content,
4628
4868
  terminalEvidence.hasActivity,
4629
4869
  terminalOutcome,
4870
+ event.stopReason,
4630
4871
  );
4631
4872
  }, owner).finally(() => {
4632
4873
  if (typeof event.sdkRunToken === "string" && lifecycleRunOwners.get(event.sdkRunToken)?.state === owner)
@@ -4716,6 +4957,74 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4716
4957
  current.deadlineManager.onAttributableEvent(invocation.correlation, eventType);
4717
4958
  }
4718
4959
  };
4960
+ /**
4961
+ * Stream one turn event to the connections that submitted the running turn.
4962
+ *
4963
+ * This is the same frame the notifications runtime sends for an ACP-launched
4964
+ * session (`{ type: "event", kind, payload: { event_type, event }, ...correlation }`),
4965
+ * built from the same shared producer, so an ACP client attached to a live
4966
+ * interactive session renders assistant text and tool calls exactly as it does
4967
+ * for a session it started itself. Without it an attached client could drive
4968
+ * the session and see the turn settle but never receive a single word of the
4969
+ * answer.
4970
+ *
4971
+ * Scoped to the owning invocations of the batch that is actually running: a
4972
+ * turn nobody submitted over the SDK (an ordinary terminal prompt, an
4973
+ * autonomous continuation, cron, monitor) has no owner connection and streams
4974
+ * nothing at all, so a session with no attached client pays one map lookup.
4975
+ */
4976
+ const STREAMED_TURN_EVENT_TYPES: ReadonlySet<string> = new Set([
4977
+ "message_update",
4978
+ "message_end",
4979
+ "tool_execution_start",
4980
+ "tool_execution_update",
4981
+ "tool_execution_end",
4982
+ ]);
4983
+ const streamTurnEvent = (event: AgentSessionEvent | null | undefined, ctx: ExtensionContext): void => {
4984
+ const current = lifecycleStateForContext(ctx, "agent_start");
4985
+ const activeInvocation = current?.activeInvocation;
4986
+ if (!current) return;
4987
+ const batch = activeInvocation
4988
+ ? current.openLifecycleBatches.find(candidate =>
4989
+ candidate.invocations.some(
4990
+ entry =>
4991
+ entry.correlation.commandId === activeInvocation.correlation.commandId &&
4992
+ entry.correlation.turnId === activeInvocation.correlation.turnId,
4993
+ ),
4994
+ )
4995
+ : undefined;
4996
+ const invocations = batch
4997
+ ? [...batch.invocations, ...batch.attachedInvocations]
4998
+ : current.lifecycleActive
4999
+ ? (current.attachedInvocations ?? [])
5000
+ : [];
5001
+ if (invocations.length === 0) return;
5002
+ // Content bypasses the lifecycle replay ring and must never interrupt its producer.
5003
+ if (!event || typeof event.type !== "string" || !STREAMED_TURN_EVENT_TYPES.has(event.type)) return;
5004
+ // emitLifecycle("agent_start") awaits durable persistence before it
5005
+ // publishes the correlated start frame, while this subscription is
5006
+ // synchronous. Hold content until that start is on the wire so no consumer
5007
+ // sees turn content before the turn it belongs to.
5008
+ if (batch && !batch.startPublished) {
5009
+ batch.heldContent.push({ event: event as AgentSessionEvent, recipients: invocations });
5010
+ if (batch.heldContent.length > MAX_HELD_CONTENT_EVENTS) {
5011
+ if (!batch.heldContentDropped) {
5012
+ batch.heldContentDropped = true;
5013
+ logger.warn(
5014
+ "sdk: agent_start persistence still pending; dropping oldest held turn content to bound memory",
5015
+ {
5016
+ epoch: batch.epoch,
5017
+ bound: MAX_HELD_CONTENT_EVENTS,
5018
+ invocations: batch.invocations.length,
5019
+ },
5020
+ );
5021
+ }
5022
+ batch.heldContent.splice(0, batch.heldContent.length - MAX_HELD_CONTENT_EVENTS);
5023
+ }
5024
+ return;
5025
+ }
5026
+ publishContentFrames(current, event as AgentSessionEvent, invocations);
5027
+ };
4719
5028
  api.on("tool_execution_start", async (_event, ctx) => {
4720
5029
  renewAttributableProgress("tool_execution_start", ctx);
4721
5030
  });
@@ -4784,7 +5093,17 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4784
5093
  connectionId: string | undefined;
4785
5094
  sdkRunToken: string;
4786
5095
  }>;
4787
- attachedInvocations: Array<{ kind: InvocationKind; correlation: InvocationCorrelation }>;
5096
+ attachedInvocations: Array<{
5097
+ kind: InvocationKind;
5098
+ correlation: InvocationCorrelation;
5099
+ connectionId: string | undefined;
5100
+ }>;
5101
+ startPublished: boolean;
5102
+ heldContentDropped: boolean;
5103
+ heldContent: Array<{
5104
+ event: AgentSessionEvent;
5105
+ recipients: Array<{ correlation: InvocationCorrelation; connectionId: string | undefined }>;
5106
+ }>;
4788
5107
  }> = [];
4789
5108
  const configRevision = { current: 0 };
4790
5109
  let acceptingGateResolutions = true;
@@ -4913,6 +5232,19 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4913
5232
  // 2. Terminal boundary. The durable error from step 1 makes this
4914
5233
  // classify as failed, never terminal_ok.
4915
5234
  await reconciliation.noteTransition(kind, correlation, { type: "agent_end" });
5235
+ const failure = sanitizePromptFailure(Object.assign(new Error(error.message), { code: error.code }));
5236
+ runtime.emitEvent({
5237
+ type: "agent_failed",
5238
+ sessionId,
5239
+ ...correlation,
5240
+ error: failure,
5241
+ });
5242
+ runtime.emitEvent({
5243
+ type: "agent_end",
5244
+ sessionId,
5245
+ ...correlation,
5246
+ outcome: canonicalFailedOutcome(failure),
5247
+ });
4916
5248
  // 3. Release recovery ownership ONLY after durable terminalization.
4917
5249
  deadlineManager.clear(correlation);
4918
5250
  } catch (transitionError) {
@@ -5065,7 +5397,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5065
5397
  entry.correlation.turnId === correlation.turnId,
5066
5398
  )
5067
5399
  )
5068
- current.attachedInvocations.push({ kind, correlation });
5400
+ current.attachedInvocations.push({ kind, correlation, connectionId });
5069
5401
  if (
5070
5402
  activeBatch &&
5071
5403
  !activeBatch.attachedInvocations.some(
@@ -5074,7 +5406,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5074
5406
  entry.correlation.turnId === correlation.turnId,
5075
5407
  )
5076
5408
  )
5077
- activeBatch.attachedInvocations.push({ kind, correlation });
5409
+ activeBatch.attachedInvocations.push({ kind, correlation, connectionId });
5078
5410
  bindPromotedToken(activeBatch);
5079
5411
  return;
5080
5412
  }
@@ -5157,6 +5489,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5157
5489
  () => acceptingGateResolutions,
5158
5490
  trackGateResolution,
5159
5491
  options.onInvocationCompletionReconciledForTests,
5492
+ frame => runtime.emitEvent(frame),
5160
5493
  );
5161
5494
  const installProviderDefinitions = (capability: string, definitions: unknown): void => {
5162
5495
  if (capability === "permission") {
@@ -5366,9 +5699,15 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5366
5699
  });
5367
5700
  }
5368
5701
  });
5702
+ // Extension message_update hooks are queued while final/tool hooks are not.
5703
+ // Use only the synchronous session boundary so those paths cannot reorder or duplicate content.
5704
+ const disposeStream = ctx.onSessionEvent?.(event => {
5705
+ if (STREAMED_TURN_EVENT_TYPES.has(event.type)) streamTurnEvent(event, ctx);
5706
+ });
5369
5707
  const disposeGate = (): void => {
5370
5708
  disposeWorkflowGate?.();
5371
5709
  disposeFold?.();
5710
+ disposeStream?.();
5372
5711
  };
5373
5712
  let publishedEndpointUrl: string | undefined;
5374
5713
  let brokerRegistered = false;
@@ -5579,7 +5918,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5579
5918
  retiredLifecycleOwnerTimers.set(current, timer);
5580
5919
  } else current.deadlineManager.clearAll();
5581
5920
  current.disposeGate?.();
5582
- await current.runtime.stop();
5921
+ await current.runtime.stop({ allowLockContention: cancelSkillRecovery });
5583
5922
  } catch (error) {
5584
5923
  // Keep the immutable owner available for a retry when transport teardown
5585
5924
  // fails after quiescing. Clearing `active` before stop prevents a second
@@ -200,6 +200,7 @@ export interface SessionLifecycleSessionResult {
200
200
  readonly sessionId: string;
201
201
  readonly cwd?: string;
202
202
  readonly endpointGeneration?: number;
203
+ readonly endpointIncarnation?: string;
203
204
  readonly reused?: boolean;
204
205
  readonly note?: string;
205
206
  }
@@ -511,6 +512,7 @@ function sessionResult(value: unknown, expectedSessionId?: string): SessionLifec
511
512
  sessionId: string;
512
513
  cwd?: string;
513
514
  endpointGeneration?: number;
515
+ endpointIncarnation?: string;
514
516
  reused?: boolean;
515
517
  note?: string;
516
518
  } = { sessionId };
@@ -518,6 +520,8 @@ function sessionResult(value: unknown, expectedSessionId?: string): SessionLifec
518
520
  const endpointGeneration = record.endpointGeneration;
519
521
  if (typeof endpointGeneration === "number" && Number.isSafeInteger(endpointGeneration) && endpointGeneration > 0)
520
522
  result.endpointGeneration = endpointGeneration;
523
+ if (typeof record.endpointIncarnation === "string" && /^[a-f0-9]{64}$/.test(record.endpointIncarnation))
524
+ result.endpointIncarnation = record.endpointIncarnation;
521
525
  if (typeof record.reused === "boolean") result.reused = record.reused;
522
526
  if (typeof record.note === "string") result.note = record.note;
523
527
  return result;