@gajae-code/coding-agent 0.16.3 → 0.16.4

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 (133) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/types/cli/args.d.ts +2 -2
  3. package/dist/types/cli/root-flags.d.ts +4 -2
  4. package/dist/types/cli.d.ts +1 -1
  5. package/dist/types/commands/launch.d.ts +1 -1
  6. package/dist/types/config/autorouting-tier-map.d.ts +3 -0
  7. package/dist/types/config/model-registry.d.ts +4 -0
  8. package/dist/types/config/model-resolver.d.ts +9 -0
  9. package/dist/types/config/settings-schema.d.ts +29 -1
  10. package/dist/types/discovery/claude-plugins.d.ts +3 -1
  11. package/dist/types/discovery/helpers.d.ts +2 -0
  12. package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
  13. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
  14. package/dist/types/extensibility/runtime-skill-discovery.d.ts +1 -1
  15. package/dist/types/extensibility/shared-events.d.ts +1 -1
  16. package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
  17. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  18. package/dist/types/modes/components/settings-selector.d.ts +6 -1
  19. package/dist/types/modes/components/status-line/command.d.ts +35 -0
  20. package/dist/types/modes/components/status-line/index.d.ts +1 -0
  21. package/dist/types/modes/components/status-line/types.d.ts +5 -0
  22. package/dist/types/modes/components/tool-status-header.d.ts +14 -0
  23. package/dist/types/modes/controllers/event-controller.d.ts +6 -0
  24. package/dist/types/modes/interactive-mode.d.ts +1 -0
  25. package/dist/types/modes/types.d.ts +1 -0
  26. package/dist/types/runtime-mcp/client.d.ts +9 -1
  27. package/dist/types/runtime-mcp/manager.d.ts +7 -1
  28. package/dist/types/runtime-mcp/pool.d.ts +22 -3
  29. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  30. package/dist/types/runtime-mcp/types.d.ts +22 -0
  31. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
  32. package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
  33. package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
  34. package/dist/types/sdk/host/session-runtime.d.ts +23 -0
  35. package/dist/types/sdk/session-api-key.d.ts +26 -0
  36. package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
  37. package/dist/types/session/agent-session.d.ts +9 -0
  38. package/dist/types/setup/paseo/announce.d.ts +111 -0
  39. package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
  40. package/dist/types/system-prompt.d.ts +2 -0
  41. package/dist/types/task/executor.d.ts +2 -1
  42. package/dist/types/tools/fetch.d.ts +7 -0
  43. package/dist/types/tools/index.d.ts +8 -0
  44. package/dist/types/tools/path-utils.d.ts +2 -0
  45. package/dist/types/tools/search.d.ts +6 -0
  46. package/package.json +7 -7
  47. package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
  48. package/scripts/generate-sdk-operation-inventory.ts +9 -0
  49. package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
  50. package/src/cli/args.ts +20 -9
  51. package/src/cli/root-flags.ts +5 -3
  52. package/src/config/autorouting-tier-map.ts +1 -0
  53. package/src/config/file-lock.ts +181 -17
  54. package/src/config/model-preset-registry.ts +5 -3
  55. package/src/config/model-profiles.ts +40 -0
  56. package/src/config/model-registry.ts +57 -11
  57. package/src/config/model-resolver.ts +48 -0
  58. package/src/config/settings-schema.ts +33 -1
  59. package/src/config/settings.ts +1 -0
  60. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
  61. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
  62. package/src/discovery/builtin.ts +13 -4
  63. package/src/discovery/claude-plugins.ts +12 -4
  64. package/src/discovery/helpers.ts +10 -2
  65. package/src/extensibility/gjc-plugins/compiler.ts +208 -58
  66. package/src/extensibility/gjc-plugins/installer.ts +13 -12
  67. package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
  68. package/src/extensibility/gjc-plugins/paths.ts +13 -0
  69. package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
  70. package/src/extensibility/runtime-skill-discovery.ts +77 -151
  71. package/src/extensibility/shared-events.ts +1 -1
  72. package/src/extensibility/skills.ts +44 -16
  73. package/src/gjc-runtime/repository-binding.ts +1 -1
  74. package/src/gjc-runtime/session-state-lock.ts +92 -2
  75. package/src/internal-urls/docs-index.generated.ts +7 -7
  76. package/src/main.ts +72 -34
  77. package/src/modes/DESIGN.md +54 -0
  78. package/src/modes/components/assistant-message.ts +22 -9
  79. package/src/modes/components/model-selector.ts +166 -33
  80. package/src/modes/components/settings-selector.ts +1075 -235
  81. package/src/modes/components/status-line/command.ts +190 -0
  82. package/src/modes/components/status-line/index.ts +1 -0
  83. package/src/modes/components/status-line/segments.ts +32 -5
  84. package/src/modes/components/status-line/types.ts +5 -0
  85. package/src/modes/components/tool-status-header.ts +393 -21
  86. package/src/modes/controllers/command-controller.ts +2 -0
  87. package/src/modes/controllers/event-controller.ts +148 -5
  88. package/src/modes/controllers/selector-controller.ts +21 -0
  89. package/src/modes/interactive-mode.ts +47 -1
  90. package/src/modes/types.ts +1 -0
  91. package/src/modes/utils/ui-helpers.ts +1 -0
  92. package/src/prompts/system/system-prompt.md +8 -0
  93. package/src/prompts/tools/search.md +2 -0
  94. package/src/runtime-mcp/client.ts +60 -17
  95. package/src/runtime-mcp/manager.ts +171 -14
  96. package/src/runtime-mcp/pool.test.ts +209 -0
  97. package/src/runtime-mcp/pool.ts +224 -23
  98. package/src/runtime-mcp/transports/stdio.ts +158 -24
  99. package/src/runtime-mcp/types.ts +24 -0
  100. package/src/sdk/broker/broker.ts +5 -4
  101. package/src/sdk/bus/chat-daemon-control.ts +1 -1
  102. package/src/sdk/bus/chat-daemon-runtime.ts +37 -1
  103. package/src/sdk/bus/slack-daemon.ts +92 -2
  104. package/src/sdk/bus/slack-live-provider.ts +12 -0
  105. package/src/sdk/bus/slack-provider.ts +5 -0
  106. package/src/sdk/cli/session-cli.ts +9 -6
  107. package/src/sdk/host/session-runtime.test.ts +2 -2
  108. package/src/sdk/host/session-runtime.ts +275 -43
  109. package/src/sdk/protocol/operation-inventory.generated.json +66 -0
  110. package/src/sdk/session-api-key.ts +94 -0
  111. package/src/sdk/session.ts +73 -29
  112. package/src/sdk/transport/serve-cli.ts +3 -3
  113. package/src/session/agent-session.ts +123 -38
  114. package/src/session/contribution-prep.ts +214 -21
  115. package/src/setup/paseo/announce.ts +450 -0
  116. package/src/setup/paseo/setup-deps.ts +15 -3
  117. package/src/slash-commands/builtin-registry.ts +1 -0
  118. package/src/system-prompt.ts +4 -0
  119. package/src/task/executor.ts +2 -1
  120. package/src/task/index.ts +17 -4
  121. package/src/tools/bash.ts +35 -8
  122. package/src/tools/cron.ts +1 -1
  123. package/src/tools/fetch.ts +29 -1
  124. package/src/tools/index.ts +8 -0
  125. package/src/tools/job.ts +8 -2
  126. package/src/tools/monitor.ts +8 -2
  127. package/src/tools/path-utils.ts +7 -0
  128. package/src/tools/search.ts +116 -58
  129. package/src/tools/skill.ts +22 -2
  130. package/src/tools/subagent.ts +3 -1
  131. package/src/tools/tool-catalog.generated.ts +8 -1
  132. package/src/web/insane/bridge.ts +113 -23
  133. 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,28 @@ 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(connectionIds: Iterable<string>, frame: SdkFrame): void {
431
+ for (const connectionId of connectionIds) {
432
+ try {
433
+ const result = this.transport.sendFrame(connectionId, frame);
434
+ if (result instanceof Promise) result.catch(() => undefined);
435
+ } catch {
436
+ // A dead connection is reaped by the transport's own close handling.
437
+ }
438
+ }
439
+ }
440
+
398
441
  publish(frame: SdkFrame): void {
399
442
  this.emitEvent(frame);
400
443
  }
@@ -577,6 +620,8 @@ function canonicalTerminalOutcome(
577
620
  if (candidate.kind === "stopped") {
578
621
  if (candidate.reason === "cancelled" && candidate.provenance === "client_cancel")
579
622
  return { kind: "stopped", reason: "cancelled", provenance: "client_cancel" };
623
+ if (candidate.reason === "end_turn" && candidate.provenance === "agent")
624
+ return { kind: "stopped", reason: "end_turn", provenance: "agent" };
580
625
  return undefined;
581
626
  }
582
627
  if (candidate.kind === "failed")
@@ -2239,6 +2284,7 @@ function createControlSurface(
2239
2284
  canResolveGate: () => boolean = () => true,
2240
2285
  trackGateResolution: <T>(resolution: Promise<T>) => Promise<T> = async resolution => await resolution,
2241
2286
  onInvocationCompletionReconciledForTests?: (kind: InvocationKind, correlation: InvocationCorrelation) => void,
2287
+ publishLifecycleFrame?: (frame: SdkFrame) => void,
2242
2288
  ): ControlSurface {
2243
2289
  const normalizePromptImages = (value: unknown): ImageContent[] => {
2244
2290
  if (!Array.isArray(value)) return [];
@@ -2376,6 +2422,24 @@ function createControlSurface(
2376
2422
  const correlation = newCorrelation();
2377
2423
  const sdkRunToken = `${correlation.commandId}:${correlation.turnId}`;
2378
2424
  const sdkRunCapability = createSdkRunCapability(sdkRunToken);
2425
+ const publishTerminal = (outcome: InvocationOutcome): void => {
2426
+ publishLifecycleFrame?.({
2427
+ type: "agent_end",
2428
+ sessionId: ctx.sessionManager.getSessionId(),
2429
+ ...correlation,
2430
+ outcome,
2431
+ });
2432
+ };
2433
+ const publishFailure = (error: unknown): void => {
2434
+ const failure = sanitizePromptFailure(error);
2435
+ publishLifecycleFrame?.({
2436
+ type: "agent_failed",
2437
+ sessionId: ctx.sessionManager.getSessionId(),
2438
+ ...correlation,
2439
+ error: failure,
2440
+ });
2441
+ publishTerminal(canonicalFailedOutcome(failure));
2442
+ };
2379
2443
  const preflight = Promise.withResolvers<void>();
2380
2444
  let accepted = false;
2381
2445
  let settled = false;
@@ -2484,11 +2548,25 @@ function createControlSurface(
2484
2548
  retirePendingOwner?.(kind, correlation);
2485
2549
  const attemptTerminalization = async (remaining: number): Promise<void> => {
2486
2550
  try {
2551
+ const before = reconciliation.lookup(kind, correlation) as {
2552
+ status?: string;
2553
+ };
2554
+ const alreadyTerminal = before.status === "terminal_ok" || before.status === "failed";
2487
2555
  await reconciliation.noteTransition(kind, correlation, {
2488
2556
  type: "agent_end",
2489
2557
  ...(typeof result === "string" ? { content: sanitizeTurnResultContent(result) } : {}),
2490
2558
  });
2491
2559
  onInvocationCompletionReconciledForTests?.(kind, correlation);
2560
+ if (!alreadyTerminal) {
2561
+ const settledRecord = reconciliation.lookup(kind, correlation) as {
2562
+ status?: string;
2563
+ outcome?: unknown;
2564
+ error?: { code?: unknown; message?: unknown };
2565
+ };
2566
+ const outcome = canonicalTerminalOutcome(settledRecord.outcome, settledRecord.error);
2567
+ if (outcome?.kind === "failed") publishFailure(settledRecord.error ?? outcome);
2568
+ else if (outcome) publishTerminal(outcome);
2569
+ }
2492
2570
  retirePendingOwner?.(kind, correlation, "always");
2493
2571
  } catch (transitionError) {
2494
2572
  if (kind === "prompt" || kind === "skill") {
@@ -2585,6 +2663,7 @@ function createControlSurface(
2585
2663
  }
2586
2664
  try {
2587
2665
  await reconciliation.noteTransition(kind, correlation, { type: "agent_end" });
2666
+ publishFailure(error);
2588
2667
  } catch (transitionError) {
2589
2668
  if (kind === "prompt" || kind === "skill") {
2590
2669
  retirePendingOwner?.(
@@ -3940,7 +4019,11 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
3940
4019
  waitForGateResolutionQuiescence: () => Promise<void>;
3941
4020
  activeInvocation?: { kind: InvocationKind; correlation: InvocationCorrelation };
3942
4021
  drainedInvocations?: Array<{ kind: InvocationKind; correlation: InvocationCorrelation }>;
3943
- attachedInvocations?: Array<{ kind: InvocationKind; correlation: InvocationCorrelation }>;
4022
+ attachedInvocations?: Array<{
4023
+ kind: InvocationKind;
4024
+ correlation: InvocationCorrelation;
4025
+ connectionId: string | undefined;
4026
+ }>;
3944
4027
  openLifecycleBatches: Array<{
3945
4028
  epoch: number;
3946
4029
  invocations: Array<{
@@ -3949,7 +4032,11 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
3949
4032
  connectionId: string | undefined;
3950
4033
  sdkRunToken: string;
3951
4034
  }>;
3952
- attachedInvocations: Array<{ kind: InvocationKind; correlation: InvocationCorrelation }>;
4035
+ attachedInvocations: Array<{
4036
+ kind: InvocationKind;
4037
+ correlation: InvocationCorrelation;
4038
+ connectionId: string | undefined;
4039
+ }>;
3953
4040
  }>;
3954
4041
  disposeGate?: () => void;
3955
4042
  lifecycleActive: boolean;
@@ -4142,6 +4229,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4142
4229
  terminalContent?: TurnResultContent,
4143
4230
  terminalHasActivity = false,
4144
4231
  terminalOutcome?: InvocationOutcome,
4232
+ stopReason?: AgentEndEvent["stopReason"],
4233
+ startToken?: string,
4145
4234
  ): Promise<void> => {
4146
4235
  const current = lifecycleOwner?.state ?? active;
4147
4236
  if (!current) return;
@@ -4216,19 +4305,44 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4216
4305
  // turn (queued-while-streaming submissions never push), so a mid-prompt
4217
4306
  // continuation agent_start with an empty queue leaves the current owner
4218
4307
  // untouched (review thread P1).
4219
- const drained = current.pending
4220
- .splice(0)
4308
+ const matchingPending =
4309
+ typeof startToken === "string" && startToken.length > 0
4310
+ ? current.pending.filter(entry => entry.sdkRunToken === startToken)
4311
+ : current.pending.slice();
4312
+ const matchingKeys = new Set(matchingPending.map(entry => entry.sdkRunToken));
4313
+ const pendingSnapshot = current.pending.splice(0);
4314
+ const drained = pendingSnapshot
4315
+ .filter(entry => matchingKeys.has(entry.sdkRunToken))
4221
4316
  .filter(entry => entry.kind !== "prompt" || !current.deadlineManager.isExpiring(entry.correlation));
4317
+ current.pending.push(...pendingSnapshot.filter(entry => !matchingKeys.has(entry.sdkRunToken)));
4318
+ const existingTokenBatch =
4319
+ typeof startToken === "string" && startToken.length > 0
4320
+ ? lifecycleRunOwners.get(startToken)?.batch
4321
+ : undefined;
4222
4322
  if (drained.length > 0) {
4223
- current.openLifecycleBatches.push({
4323
+ const batch: LifecycleBatch = {
4224
4324
  epoch: current.lifecycleEpoch,
4225
4325
  invocations: drained,
4226
4326
  attachedInvocations: [],
4227
- });
4327
+ };
4328
+ current.openLifecycleBatches.push(batch);
4329
+ if (typeof startToken === "string" && startToken.length > 0) {
4330
+ const entry = drained.find(item => item.sdkRunToken === startToken);
4331
+ lifecycleRunOwners.set(startToken, {
4332
+ state: current,
4333
+ batch,
4334
+ ...(entry ? { correlationKey: lifecycleCorrelationKey(entry.correlation) } : {}),
4335
+ });
4336
+ }
4228
4337
  adoptLifecycleBatch(drained);
4229
4338
  if (current.activeInvocation?.kind === "prompt") {
4230
4339
  current.deadlineManager.onAccepted(current.activeInvocation.correlation);
4231
4340
  }
4341
+ } else if (existingTokenBatch && current.openLifecycleBatches.includes(existingTokenBatch)) {
4342
+ // A continuation within the same SDK-owned run carries the same token
4343
+ // but has no newly pending invocation. Preserve the established owner so
4344
+ // later chunks and the final terminal remain directed to its submitters.
4345
+ adoptLifecycleBatch(existingTokenBatch.invocations);
4232
4346
  } else {
4233
4347
  // An empty drain is an agent-initiated successor run. Clear the
4234
4348
  // predecessor's SDK owner and active invocation so abort ownership and
@@ -4240,7 +4354,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4240
4354
  // Pair this agent_end with the oldest unmatched start. A delayed
4241
4355
  // aborted-turn end that lands after a successor agent_start must
4242
4356
  // terminalize the aborted invocation, never the successor.
4243
- const ended = current.openLifecycleBatches[0];
4357
+ const ended = failureBatch ?? current.openLifecycleBatches[0];
4244
4358
  const baseTransitions = ended
4245
4359
  ? ended.invocations.map(({ kind, correlation }) => ({ kind, correlation }))
4246
4360
  : current.activeInvocation
@@ -4364,7 +4478,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4364
4478
  };
4365
4479
  void attempt().finally(() => skillTerminalRecoveryControllers.delete(recoveryKey));
4366
4480
  };
4367
- if (type === "agent_end" && maintenanceOutcome !== undefined && maintenanceOutcome !== "aborted") {
4481
+ if (type === "agent_end" && isContinuingMidRunMaintenanceOutcome(maintenanceOutcome)) {
4368
4482
  try {
4369
4483
  current.runtime.emitEvent({ type, sessionId: ctx.sessionManager.getSessionId() });
4370
4484
  } catch {
@@ -4485,7 +4599,31 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4485
4599
  observed = false;
4486
4600
  }
4487
4601
  }
4602
+ } else if (type === "agent_end" && transitions.length > 0) {
4603
+ // An invocation-owned terminal must carry the identity it was accepted
4604
+ // under and the normalized outcome. A client that submitted this turn
4605
+ // matches the terminal against its own acknowledged correlation and
4606
+ // refuses an unowned one, so the bare `{ type, sessionId }` boundary left
4607
+ // every externally submitted prompt reporting `working` forever even
4608
+ // though the turn had finished -- ACP dropped it as
4609
+ // `incomplete_correlation`, and the notifications runtime has always
4610
+ // stamped both fields for exactly this reason. One frame per invocation,
4611
+ // matching the agent_failed shape above, also lets a shared run attribute
4612
+ // the boundary to each owner instead of one signal nobody can claim. A
4613
+ // continuing maintenance checkpoint never reaches this branch: it returned
4614
+ // above with the one uncorrelated frame, so it cannot settle a live prompt.
4615
+ const outcome = terminalOutcome ?? terminalStoppedOutcome(stopReason, maintenanceOutcome);
4616
+ for (const invocation of transitions) {
4617
+ try {
4618
+ current.runtime.emitEvent({ type, sessionId, ...invocation.correlation, outcome });
4619
+ } catch {
4620
+ observed = false;
4621
+ }
4622
+ }
4488
4623
  } else {
4624
+ // Everything that does not terminalize an owned invocation keeps the one
4625
+ // uncorrelated frame: `agent_start` and a host-originated run (autonomous
4626
+ // continuation, cron, monitor) that owns no invocation.
4489
4627
  try {
4490
4628
  current.runtime.emitEvent({ type, sessionId });
4491
4629
  } catch {
@@ -4495,7 +4633,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4495
4633
  } catch {
4496
4634
  observed = false;
4497
4635
  }
4498
- if (type === "agent_end" && maintenanceOutcome !== undefined && maintenanceOutcome !== "aborted") return;
4636
+ if (type === "agent_end" && isContinuingMidRunMaintenanceOutcome(maintenanceOutcome)) return;
4499
4637
  if (type === "agent_end") {
4500
4638
  if (current.lifecycleEpoch !== eventLifecycleEpoch) {
4501
4639
  // A successor agent_start won the lifecycle race while this event's
@@ -4553,29 +4691,22 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4553
4691
  };
4554
4692
  api.on("agent_start", (event, ctx) => {
4555
4693
  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);
4694
+ return trackLifecycle(
4695
+ async () =>
4696
+ emitLifecycle(
4697
+ "agent_start",
4698
+ ctx,
4699
+ undefined,
4700
+ undefined,
4701
+ owner ? { state: owner, sessionId: owner.sessionId } : undefined,
4702
+ undefined,
4703
+ false,
4704
+ undefined,
4705
+ undefined,
4706
+ typeof event.sdkRunToken === "string" ? event.sdkRunToken : undefined,
4707
+ ),
4708
+ owner,
4709
+ );
4579
4710
  });
4580
4711
  api.on("agent_end", (event, ctx) => {
4581
4712
  const tokenBinding =
@@ -4608,11 +4739,20 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4608
4739
  const failureAlreadyPublished = failure === undefined || (hasExistingFailure && genericFailureKeys.length === 0);
4609
4740
  const terminalEvidence = promptTerminalEvidenceFromAgentEnd(event);
4610
4741
  const terminalOutcome =
4611
- event.stopReason === "cancelled"
4612
- ? ({ kind: "stopped", reason: "cancelled", provenance: "client_cancel" } as const)
4613
- : failure === undefined
4614
- ? undefined
4615
- : canonicalFailedOutcome(failure);
4742
+ event.stopReason === "cancelled" ||
4743
+ (event.stopReason === "maintenance" && event.maintenanceOutcome === "aborted")
4744
+ ? terminalStoppedOutcome(
4745
+ event.stopReason,
4746
+ event.stopReason === "maintenance" ? event.maintenanceOutcome : undefined,
4747
+ )
4748
+ : failure !== undefined
4749
+ ? canonicalFailedOutcome(failure)
4750
+ : terminalEvidence.content?.text.trim() || terminalEvidence.hasActivity
4751
+ ? terminalStoppedOutcome(
4752
+ event.stopReason,
4753
+ event.stopReason === "maintenance" ? event.maintenanceOutcome : undefined,
4754
+ )
4755
+ : canonicalFailedOutcome(EMPTY_PROMPT_FAILURE);
4616
4756
  return trackLifecycle(async () => {
4617
4757
  if (failure && !failureAlreadyPublished) {
4618
4758
  for (const key of genericFailureKeys) owner?.failureDiagnosticKeys.delete(key);
@@ -4627,6 +4767,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4627
4767
  terminalEvidence.content,
4628
4768
  terminalEvidence.hasActivity,
4629
4769
  terminalOutcome,
4770
+ event.stopReason,
4630
4771
  );
4631
4772
  }, owner).finally(() => {
4632
4773
  if (typeof event.sdkRunToken === "string" && lifecycleRunOwners.get(event.sdkRunToken)?.state === owner)
@@ -4716,11 +4857,84 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4716
4857
  current.deadlineManager.onAttributableEvent(invocation.correlation, eventType);
4717
4858
  }
4718
4859
  };
4719
- api.on("tool_execution_start", async (_event, ctx) => {
4860
+ /**
4861
+ * Stream one turn event to the connections that submitted the running turn.
4862
+ *
4863
+ * This is the same frame the notifications runtime sends for an ACP-launched
4864
+ * session (`{ type: "event", kind, payload: { event_type, event }, ...correlation }`),
4865
+ * built from the same shared producer, so an ACP client attached to a live
4866
+ * interactive session renders assistant text and tool calls exactly as it does
4867
+ * for a session it started itself. Without it an attached client could drive
4868
+ * the session and see the turn settle but never receive a single word of the
4869
+ * answer.
4870
+ *
4871
+ * Scoped to the owning invocations of the batch that is actually running: a
4872
+ * turn nobody submitted over the SDK (an ordinary terminal prompt, an
4873
+ * autonomous continuation, cron, monitor) has no owner connection and streams
4874
+ * nothing at all, so a session with no attached client pays one map lookup.
4875
+ */
4876
+ const STREAMED_TURN_EVENT_TYPES: ReadonlySet<string> = new Set([
4877
+ "message_update",
4878
+ "message_end",
4879
+ "tool_execution_start",
4880
+ "tool_execution_update",
4881
+ "tool_execution_end",
4882
+ ]);
4883
+ const streamTurnEvent = (event: { type?: unknown } | null | undefined, ctx: ExtensionContext): void => {
4884
+ const current = lifecycleStateForContext(ctx, "agent_start");
4885
+ const activeInvocation = current?.activeInvocation;
4886
+ if (!current) return;
4887
+ const batch = activeInvocation
4888
+ ? current.openLifecycleBatches.find(candidate =>
4889
+ candidate.invocations.some(
4890
+ entry =>
4891
+ entry.correlation.commandId === activeInvocation.correlation.commandId &&
4892
+ entry.correlation.turnId === activeInvocation.correlation.turnId,
4893
+ ),
4894
+ )
4895
+ : undefined;
4896
+ const invocations = batch
4897
+ ? [...batch.invocations, ...batch.attachedInvocations]
4898
+ : current.lifecycleActive
4899
+ ? (current.attachedInvocations ?? [])
4900
+ : [];
4901
+ if (invocations.length === 0) return;
4902
+ // The handlers below also renew prompt deadlines, so streaming must never
4903
+ // throw out of them: a frame the wire producer does not recognize is dropped.
4904
+ if (!event || typeof event.type !== "string" || !STREAMED_TURN_EVENT_TYPES.has(event.type)) return;
4905
+ try {
4906
+ const payload = toAgentWireEventPayload(event as AgentSessionEvent);
4907
+ // One frame per owning invocation, each carrying its own correlation, so a
4908
+ // shared run lets every submitter attribute the content to its own prompt.
4909
+ for (const invocation of invocations) {
4910
+ if (invocation.connectionId === undefined) continue;
4911
+ current.runtime.sendFrameTo([invocation.connectionId], {
4912
+ type: "event",
4913
+ kind: event.type,
4914
+ payload,
4915
+ ...invocation.correlation,
4916
+ });
4917
+ }
4918
+ } catch {
4919
+ // Streamed content is best-effort; the turn producing it is authoritative.
4920
+ }
4921
+ };
4922
+ api.on("tool_execution_start", async (event, ctx) => {
4720
4923
  renewAttributableProgress("tool_execution_start", ctx);
4924
+ streamTurnEvent(event, ctx);
4925
+ });
4926
+ api.on("tool_execution_update", async (event, ctx) => {
4927
+ streamTurnEvent(event, ctx);
4721
4928
  });
4722
- api.on("tool_execution_end", async (_event, ctx) => {
4929
+ api.on("tool_execution_end", async (event, ctx) => {
4723
4930
  renewAttributableProgress("tool_execution_end", ctx);
4931
+ streamTurnEvent(event, ctx);
4932
+ });
4933
+ api.on("message_update", async (event, ctx) => {
4934
+ streamTurnEvent(event, ctx);
4935
+ });
4936
+ api.on("message_end", async (event, ctx) => {
4937
+ streamTurnEvent(event, ctx);
4724
4938
  });
4725
4939
  const errorCode = (error: unknown): string | undefined =>
4726
4940
  typeof error === "object" &&
@@ -4784,7 +4998,11 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4784
4998
  connectionId: string | undefined;
4785
4999
  sdkRunToken: string;
4786
5000
  }>;
4787
- attachedInvocations: Array<{ kind: InvocationKind; correlation: InvocationCorrelation }>;
5001
+ attachedInvocations: Array<{
5002
+ kind: InvocationKind;
5003
+ correlation: InvocationCorrelation;
5004
+ connectionId: string | undefined;
5005
+ }>;
4788
5006
  }> = [];
4789
5007
  const configRevision = { current: 0 };
4790
5008
  let acceptingGateResolutions = true;
@@ -4913,6 +5131,19 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4913
5131
  // 2. Terminal boundary. The durable error from step 1 makes this
4914
5132
  // classify as failed, never terminal_ok.
4915
5133
  await reconciliation.noteTransition(kind, correlation, { type: "agent_end" });
5134
+ const failure = sanitizePromptFailure(Object.assign(new Error(error.message), { code: error.code }));
5135
+ runtime.emitEvent({
5136
+ type: "agent_failed",
5137
+ sessionId,
5138
+ ...correlation,
5139
+ error: failure,
5140
+ });
5141
+ runtime.emitEvent({
5142
+ type: "agent_end",
5143
+ sessionId,
5144
+ ...correlation,
5145
+ outcome: canonicalFailedOutcome(failure),
5146
+ });
4916
5147
  // 3. Release recovery ownership ONLY after durable terminalization.
4917
5148
  deadlineManager.clear(correlation);
4918
5149
  } catch (transitionError) {
@@ -5065,7 +5296,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5065
5296
  entry.correlation.turnId === correlation.turnId,
5066
5297
  )
5067
5298
  )
5068
- current.attachedInvocations.push({ kind, correlation });
5299
+ current.attachedInvocations.push({ kind, correlation, connectionId });
5069
5300
  if (
5070
5301
  activeBatch &&
5071
5302
  !activeBatch.attachedInvocations.some(
@@ -5074,7 +5305,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5074
5305
  entry.correlation.turnId === correlation.turnId,
5075
5306
  )
5076
5307
  )
5077
- activeBatch.attachedInvocations.push({ kind, correlation });
5308
+ activeBatch.attachedInvocations.push({ kind, correlation, connectionId });
5078
5309
  bindPromotedToken(activeBatch);
5079
5310
  return;
5080
5311
  }
@@ -5157,6 +5388,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5157
5388
  () => acceptingGateResolutions,
5158
5389
  trackGateResolution,
5159
5390
  options.onInvocationCompletionReconciledForTests,
5391
+ frame => runtime.emitEvent(frame),
5160
5392
  );
5161
5393
  const installProviderDefinitions = (capability: string, definitions: unknown): void => {
5162
5394
  if (capability === "permission") {
@@ -2841,6 +2841,50 @@
2841
2841
  "testIds": "not_applicable"
2842
2842
  }
2843
2843
  },
2844
+ {
2845
+ "sourceId": "agent_session:awaitDisposeCompletion",
2846
+ "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
2847
+ "sourceKind": "agent_session",
2848
+ "decision": "exclude",
2849
+ "rationale": "test-only disposal completion seam, not a user-facing SDK control seam",
2850
+ "exclusionMetadata": {
2851
+ "adapterMappings": "not_applicable",
2852
+ "testIds": "not_applicable"
2853
+ }
2854
+ },
2855
+ {
2856
+ "sourceId": "agent_session:setDisposeTimeoutForTests",
2857
+ "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
2858
+ "sourceKind": "agent_session",
2859
+ "decision": "exclude",
2860
+ "rationale": "test-only disposal timeout seam, not a user-facing SDK control seam",
2861
+ "exclusionMetadata": {
2862
+ "adapterMappings": "not_applicable",
2863
+ "testIds": "not_applicable"
2864
+ }
2865
+ },
2866
+ {
2867
+ "sourceId": "agent_session:trackPostPromptTaskForTests",
2868
+ "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
2869
+ "sourceKind": "agent_session",
2870
+ "decision": "exclude",
2871
+ "rationale": "test-only post-prompt task seam, not a user-facing SDK control seam",
2872
+ "exclusionMetadata": {
2873
+ "adapterMappings": "not_applicable",
2874
+ "testIds": "not_applicable"
2875
+ }
2876
+ },
2877
+ {
2878
+ "sourceId": "agent_session:requestWorkerIntegrationForTests",
2879
+ "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
2880
+ "sourceKind": "agent_session",
2881
+ "decision": "exclude",
2882
+ "rationale": "test-only worker integration seam, not a user-facing SDK control seam",
2883
+ "exclusionMetadata": {
2884
+ "adapterMappings": "not_applicable",
2885
+ "testIds": "not_applicable"
2886
+ }
2887
+ },
2844
2888
  {
2845
2889
  "sourceId": "agent_session:closeWriterStrict",
2846
2890
  "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
@@ -2885,6 +2929,17 @@
2885
2929
  "testIds": "not_applicable"
2886
2930
  }
2887
2931
  },
2932
+ {
2933
+ "sourceId": "agent_session:runWithPromptAdmissionForTests",
2934
+ "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
2935
+ "sourceKind": "agent_session",
2936
+ "decision": "exclude",
2937
+ "rationale": "test-only prompt admission seam, not a user-facing SDK control seam",
2938
+ "exclusionMetadata": {
2939
+ "adapterMappings": "not_applicable",
2940
+ "testIds": "not_applicable"
2941
+ }
2942
+ },
2888
2943
  {
2889
2944
  "sourceId": "agent_session:queueCoordinatorRuntimeStatePersistForTests",
2890
2945
  "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
@@ -3912,6 +3967,17 @@
3912
3967
  "testIds": "not_applicable"
3913
3968
  }
3914
3969
  },
3970
+ {
3971
+ "sourceId": "agent_session:reloadSkills",
3972
+ "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
3973
+ "sourceKind": "agent_session",
3974
+ "decision": "exclude",
3975
+ "rationale": "internal marketplace lifecycle refresh for the session skill catalog; not an independent SDK ingress operation",
3976
+ "exclusionMetadata": {
3977
+ "adapterMappings": "not_applicable",
3978
+ "testIds": "not_applicable"
3979
+ }
3980
+ },
3915
3981
  {
3916
3982
  "sourceId": "agent_session:retireWorkspaceTreeForRescope",
3917
3983
  "sourceFile": "packages/coding-agent/src/session/agent-session.ts",