@gajae-code/coding-agent 0.15.5 → 0.15.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 (155) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/dist/types/async/job-manager.d.ts +83 -0
  3. package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
  4. package/dist/types/cli/model-presets-cli.d.ts +9 -0
  5. package/dist/types/cli/update-cli.d.ts +2 -0
  6. package/dist/types/commands/auth-gateway.d.ts +3 -0
  7. package/dist/types/commands/gc.d.ts +13 -0
  8. package/dist/types/commands/model-presets.d.ts +21 -0
  9. package/dist/types/config/autorouting-tier-map.d.ts +96 -0
  10. package/dist/types/config/file-lock.d.ts +49 -2
  11. package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
  12. package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
  13. package/dist/types/config/keybindings.d.ts +3 -2
  14. package/dist/types/config/model-preset-registry.d.ts +368 -0
  15. package/dist/types/config/model-profile-activation.d.ts +39 -0
  16. package/dist/types/config/model-profiles.d.ts +5 -3
  17. package/dist/types/config/model-registry.d.ts +11 -5
  18. package/dist/types/config/settings-schema.d.ts +9 -0
  19. package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
  20. package/dist/types/coordinator-mcp/projection-scan.d.ts +46 -0
  21. package/dist/types/extensibility/extensions/types.d.ts +2 -5
  22. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
  23. package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
  24. package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
  25. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +6 -0
  26. package/dist/types/index.d.ts +1 -0
  27. package/dist/types/main.d.ts +3 -1
  28. package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
  29. package/dist/types/modes/jobs-observer.d.ts +36 -0
  30. package/dist/types/sdk/broker/broker.d.ts +3 -0
  31. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
  32. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
  33. package/dist/types/sdk/client/discovery.d.ts +1 -0
  34. package/dist/types/sdk/host/model-pin.d.ts +7 -2
  35. package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
  36. package/dist/types/sdk/host/session-runtime.d.ts +15 -1
  37. package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
  38. package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
  39. package/dist/types/sdk/router/session-router.d.ts +9 -0
  40. package/dist/types/session/account-inventory.d.ts +1 -0
  41. package/dist/types/session/agent-session.d.ts +19 -7
  42. package/dist/types/session/fold-coordinator.d.ts +201 -0
  43. package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
  44. package/dist/types/session/sdk-run-capability.d.ts +2 -0
  45. package/dist/types/telemetry/control.d.ts +11 -0
  46. package/dist/types/telemetry/events.d.ts +20 -0
  47. package/dist/types/telemetry/index.d.ts +3 -0
  48. package/dist/types/telemetry/transport.d.ts +22 -0
  49. package/dist/types/tools/bash-interactive.d.ts +23 -0
  50. package/dist/types/tools/index.d.ts +6 -5
  51. package/dist/types/web/search/providers/base.d.ts +9 -4
  52. package/dist/types/web/search/providers/xai.d.ts +2 -1
  53. package/dist/types/web/search/types.d.ts +14 -0
  54. package/package.json +10 -7
  55. package/scripts/generate-hotkeys-docs.ts +4 -0
  56. package/scripts/generate-sdk-operation-inventory.ts +3 -3
  57. package/src/async/job-manager.ts +671 -39
  58. package/src/cli/auth-broker-cli.ts +40 -12
  59. package/src/cli/auth-gateway-cli.ts +273 -97
  60. package/src/cli/config-cli.ts +2 -2
  61. package/src/cli/fast-help.ts +3 -0
  62. package/src/cli/model-presets-cli.ts +100 -0
  63. package/src/cli/update-cli.ts +40 -5
  64. package/src/cli.ts +6 -2
  65. package/src/commands/auth-gateway.ts +12 -8
  66. package/src/commands/gc.ts +19 -0
  67. package/src/commands/model-presets.ts +24 -0
  68. package/src/config/autorouting-tier-map.ts +32 -0
  69. package/src/config/file-lock-gc.ts +9 -4
  70. package/src/config/file-lock.ts +775 -59
  71. package/src/config/internal/model-preset-registry-test-state.ts +28 -0
  72. package/src/config/internal/model-preset-registry-test-support.ts +11 -0
  73. package/src/config/keybindings.ts +6 -2
  74. package/src/config/model-preset-registry.ts +2799 -0
  75. package/src/config/model-profile-activation.ts +115 -28
  76. package/src/config/model-profile-contract.ts +4 -1
  77. package/src/config/model-profiles.ts +51 -9
  78. package/src/config/model-registry.ts +1021 -122
  79. package/src/config/settings-schema.ts +11 -0
  80. package/src/config/settings.ts +6 -1
  81. package/src/coordinator-mcp/model-preset.ts +32 -17
  82. package/src/coordinator-mcp/projection-scan.ts +571 -0
  83. package/src/coordinator-mcp/server.ts +19 -24
  84. package/src/coordinator-mcp/session-reaper.ts +9 -3
  85. package/src/extensibility/extensions/loader.ts +0 -1
  86. package/src/extensibility/extensions/types.ts +2 -3
  87. package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
  88. package/src/gjc-runtime/empty-delete-gc.ts +301 -0
  89. package/src/gjc-runtime/gc-render.ts +29 -0
  90. package/src/gjc-runtime/gc-runtime.ts +174 -11
  91. package/src/gjc-runtime/session-state-lock.ts +1242 -141
  92. package/src/gjc-runtime/session-state-sidecar.ts +76 -3
  93. package/src/index.ts +1 -0
  94. package/src/internal-urls/docs-index.generated.ts +9 -8
  95. package/src/lsp/config.ts +4 -3
  96. package/src/lsp/path-trust.ts +12 -6
  97. package/src/main.ts +9 -2
  98. package/src/modes/acp/acp-agent.ts +11 -5
  99. package/src/modes/components/jobs-overlay-model.ts +99 -16
  100. package/src/modes/components/model-selector.ts +158 -18
  101. package/src/modes/components/status-line/segments.ts +6 -1
  102. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  103. package/src/modes/controllers/input-controller.ts +23 -6
  104. package/src/modes/interactive-mode.ts +14 -1
  105. package/src/modes/jobs-observer.ts +149 -16
  106. package/src/modes/runtime-init.ts +1 -1
  107. package/src/modes/utils/injected-user-submission.ts +15 -1
  108. package/src/prompts/tools/bash.md +2 -2
  109. package/src/prompts/tools/fold-receipt.md +3 -0
  110. package/src/prompts/tools/job.md +1 -1
  111. package/src/sdk/acp/adapter.ts +17 -2
  112. package/src/sdk/broker/broker.ts +48 -21
  113. package/src/sdk/broker/lifecycle.ts +32 -6
  114. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  115. package/src/sdk/bus/chat-daemon-runtime.ts +11 -1
  116. package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
  117. package/src/sdk/bus/prompt-reconciliation.ts +28 -4
  118. package/src/sdk/bus/slack-conversation.ts +5 -1
  119. package/src/sdk/cli/session-cli.ts +12 -1
  120. package/src/sdk/client/discovery.ts +3 -1
  121. package/src/sdk/host/control/dispatch.ts +3 -0
  122. package/src/sdk/host/model-pin.ts +185 -25
  123. package/src/sdk/host/sdk-run-capability.ts +1 -0
  124. package/src/sdk/host/session-runtime.test.ts +371 -37
  125. package/src/sdk/host/session-runtime.ts +364 -52
  126. package/src/sdk/prompt-deadline-manager.ts +16 -1
  127. package/src/sdk/protocol/adapter-validation.ts +27 -0
  128. package/src/sdk/protocol/operation-inventory.generated.json +2 -2
  129. package/src/sdk/protocol/operation-registry.ts +4 -1
  130. package/src/sdk/router/session-router.ts +228 -96
  131. package/src/sdk/session.ts +83 -12
  132. package/src/session/account-inventory.ts +27 -11
  133. package/src/session/agent-session.ts +678 -103
  134. package/src/session/fold-coordinator.ts +433 -0
  135. package/src/session/sdk-run-capability-internal.ts +15 -0
  136. package/src/session/sdk-run-capability.ts +2 -0
  137. package/src/session/session-manager.ts +5 -4
  138. package/src/setup/provider-presets.json +1 -1
  139. package/src/slash-commands/helpers/credential-switch.ts +6 -1
  140. package/src/task/executor.ts +19 -3
  141. package/src/telemetry/control.ts +26 -0
  142. package/src/telemetry/events.ts +142 -0
  143. package/src/telemetry/index.ts +3 -0
  144. package/src/telemetry/transport.ts +87 -0
  145. package/src/tools/bash-interactive.ts +182 -85
  146. package/src/tools/bash.ts +772 -67
  147. package/src/tools/index.ts +7 -5
  148. package/src/tools/tool-catalog.generated.ts +1 -1
  149. package/src/web/search/provider.ts +120 -11
  150. package/src/web/search/providers/anthropic.ts +89 -17
  151. package/src/web/search/providers/base.ts +9 -4
  152. package/src/web/search/providers/gemini.ts +87 -15
  153. package/src/web/search/providers/openai-compatible.ts +23 -8
  154. package/src/web/search/providers/xai.ts +98 -12
  155. package/src/web/search/types.ts +16 -0
@@ -5,16 +5,30 @@ import * as fs from "node:fs/promises";
5
5
  import * as path from "node:path";
6
6
  import { promisify } from "node:util";
7
7
  import { ThinkingLevel } from "@gajae-code/agent-core";
8
- import type { Api, Model } from "@gajae-code/ai/core";
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";
11
+ import {
12
+ getProxyRoutableProviders,
13
+ inspectProxyProviderId,
14
+ requiresQualifiedModelProfileRoleResolution,
15
+ resolveProxyMode,
16
+ rewriteSelectorForProxy,
17
+ tryResolveProxyProviderId,
18
+ } from "../../config/model-profile-activation";
11
19
  import { isModelProfileProviderAvailable, projectModelProfileCatalog } from "../../config/model-profile-contract";
12
- import { type ModelProfileDefinition, resolveProfileBindings } from "../../config/model-profiles";
20
+ import {
21
+ deriveModelProfileMappedProviders,
22
+ type ModelProfileDefinition,
23
+ resolveProfileBindings,
24
+ } from "../../config/model-profiles";
25
+ import { isAuthenticated, kNoAuth } from "../../config/model-registry";
13
26
  import { resolveModelChainWithAuth, splitSelectorThinkingSuffix } from "../../config/model-resolver";
14
27
  import { type ModelSelectorValue, normalizeModelSelectorValue } from "../../config/model-selector-value";
15
28
  import { type Settings, validateSettingPatch } from "../../config/settings";
16
29
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../../extensibility/extensions";
17
30
  import { normalizeGoal } from "../../goals/state";
31
+ import type { SdkRunCapability } from "../../session/sdk-run-capability";
18
32
  import {
19
33
  boundTerminalRetentionState,
20
34
  findOwnedRegistrationsForTurn,
@@ -26,7 +40,6 @@ import { parseThinkingLevel } from "../../thinking";
26
40
  import { ensureBroker } from "../broker/ensure";
27
41
  import { SessionIndex } from "../broker/session-index";
28
42
  import {
29
- collectAuthenticatedProfileProviders,
30
43
  parseSyntheticModelId,
31
44
  resolveSyntheticModelSelection,
32
45
  SYNTHETIC_PROVIDER_ID,
@@ -34,8 +47,9 @@ import {
34
47
  syntheticNamespaceCollision,
35
48
  } from "../model-profile-model";
36
49
  import { projectQ10Models } from "../models.js";
37
- import { PromptDeadlineManager } from "../prompt-deadline-manager";
50
+ import { PromptDeadlineManager, type PromptTerminalTransitionEvidence } from "../prompt-deadline-manager";
38
51
  import { formatPromptFailureForLocalLog, sanitizePromptFailure } from "../prompt-failure";
52
+ import { validateRequiredPromptText } from "../protocol/adapter-validation";
39
53
  import { OPERATIONS } from "../protocol/operation-registry";
40
54
  import {
41
55
  createKindAwareReconciliation,
@@ -43,11 +57,13 @@ import {
43
57
  type KindAwareReconciliation,
44
58
  resolveReconciliationSessionFile,
45
59
  } from "../reconciliation-extensions";
60
+ import { sanitizeTurnResultContent, type TurnResultContent } from "../turn-result";
46
61
  import { type ControlSurface, controlRequestFromFrame, dispatchControl } from "./control";
47
62
  import { BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD } from "./control/runtime-gate";
48
63
  import { SessionSdkHost, type SessionSdkHostOptions } from "./host";
49
64
  import { clearAutoroutingInactive, isAutoroutingInactive, markAutoroutingInactive } from "./internal-autorouting-state";
50
65
  import { CursorRegistry, QueryHandlers, RevisionStore, type SessionSurface } from "./query";
66
+ import { createSdkRunCapability } from "./sdk-run-capability";
51
67
  import {
52
68
  createSdkCapabilities,
53
69
  createSdkSurfacePolicyForContext,
@@ -420,6 +436,7 @@ export interface InvocationCorrelation {
420
436
 
421
437
  export type InvocationKind = "prompt" | "skill" | "steer";
422
438
  type InvocationStatus = "accepted" | "in_flight" | "terminal_ok" | "failed" | "uncertain";
439
+ const EMPTY_PROMPT_FAILURE = { code: "prompt_failed", message: "Prompt submission failed." } as const;
423
440
  interface InvocationRecord extends InvocationCorrelation {
424
441
  kind: InvocationKind;
425
442
  revision: number;
@@ -440,7 +457,14 @@ export interface InvocationReconciliation {
440
457
  noteTransition(
441
458
  kind: InvocationKind,
442
459
  correlation: InvocationCorrelation | undefined,
443
- frame: { type: "agent_start" | "agent_end" } | { type: "agent_failed"; error: unknown },
460
+ frame:
461
+ | {
462
+ type: "agent_start" | "agent_end";
463
+ content?: TurnResultContent;
464
+ hasActivity?: boolean;
465
+ outcome?: { kind: "stopped"; reason: "cancelled"; provenance: "client_cancel" };
466
+ }
467
+ | { type: "agent_failed"; error: unknown; content?: TurnResultContent; hasActivity?: boolean },
444
468
  ): Promise<void>;
445
469
  lookup(kind: InvocationKind, selector: { commandId?: string; turnId?: string; clientRef?: string }): unknown;
446
470
  lookupResult(kind: InvocationKind, selector: { commandId?: string; turnId?: string; clientRef?: string }): unknown;
@@ -464,7 +488,13 @@ export interface InvocationReconciliation {
464
488
  correlation: InvocationCorrelation,
465
489
  outcome?: { kind: string; code: string; message: string; provenance?: string },
466
490
  arg4?: (() => boolean) | { code: string; message: string },
491
+ // Terminal evidence accepted by finalizeOutcome (exact-head review P1/P2):
492
+ // when an explicit outcome is absent, a content-bearing, activity-bearing,
493
+ // or explicitly stopped finalization is still a successful terminal; only a
494
+ // genuinely empty, no-activity completion fails closed with the sanitized
495
+ // empty-prompt failure. Mirrors the noteTransition evidence predicate.
467
496
  arg5?: unknown,
497
+ arg6?: { content?: TurnResultContent; hasActivity?: boolean; outcomeKind?: string },
468
498
  ): Promise<void>;
469
499
  markUncertain(
470
500
  kind: InvocationKind,
@@ -699,7 +729,14 @@ export function createInvocationReconciliation(
699
729
  // so a real lifecycle event is never swallowed.
700
730
  }
701
731
  const current = records.get(recordKey);
702
- if (frame.type === "agent_end" && current === pending.finalizedRecord) {
732
+ if (
733
+ frame.type === "agent_end" &&
734
+ current === pending.finalizedRecord &&
735
+ (kind !== "prompt" ||
736
+ frame.outcome?.kind === "stopped" ||
737
+ frame.hasActivity === true ||
738
+ frame.content?.text.trim())
739
+ ) {
703
740
  // A real lifecycle end that arrived during deadline finalization is
704
741
  // stronger evidence than the synthetic deadline outcome. Upgrade the
705
742
  // durable terminal instead of treating the event as a duplicate.
@@ -734,7 +771,14 @@ export function createInvocationReconciliation(
734
771
  }
735
772
  }
736
773
  if (record.terminalAt !== undefined) {
737
- if (frame.type === "agent_end" && record.error?.code === "prompt_deadline_exceeded") {
774
+ if (
775
+ frame.type === "agent_end" &&
776
+ record.error?.code === "prompt_deadline_exceeded" &&
777
+ (kind !== "prompt" ||
778
+ frame.outcome?.kind === "stopped" ||
779
+ frame.hasActivity === true ||
780
+ frame.content?.text.trim())
781
+ ) {
738
782
  const upgraded = { ...record, revision: ++mutationRevision, status: "terminal_ok" as const };
739
783
  // The synthetic deadline error is superseded evidence; terminal_ok must not
740
784
  // surface a deadline failure for a prompt that actually completed.
@@ -795,7 +839,17 @@ export function createInvocationReconciliation(
795
839
  if (next.error === undefined || (next.error.code === "agent_failed" && failure.code !== "agent_failed"))
796
840
  next.error = failure;
797
841
  } else {
798
- next.status = next.error === undefined ? "terminal_ok" : "failed";
842
+ if (
843
+ kind === "prompt" &&
844
+ next.error === undefined &&
845
+ frame.type === "agent_end" &&
846
+ frame.outcome?.kind !== "stopped" &&
847
+ !frame.content?.text.trim() &&
848
+ !frame.hasActivity
849
+ ) {
850
+ next.status = "failed";
851
+ next.error = EMPTY_PROMPT_FAILURE;
852
+ } else next.status = next.error === undefined ? "terminal_ok" : "failed";
799
853
  next.terminalAt = Date.now();
800
854
  }
801
855
  records.set(recordKey, next);
@@ -868,6 +922,7 @@ export function createInvocationReconciliation(
868
922
  outcome,
869
923
  arg4?: (() => boolean) | { code: string; message: string },
870
924
  arg5?: unknown,
925
+ arg6?: { content?: TurnResultContent; hasActivity?: boolean; outcomeKind?: string },
871
926
  ) {
872
927
  // Normalize every supported positional form (exact-head review P1):
873
928
  // (…, isCurrent) | (…, recordError) | (…, undefined, recordError) |
@@ -880,6 +935,14 @@ export function createInvocationReconciliation(
880
935
  ? (arg5 as { code: string; message: string })
881
936
  : undefined;
882
937
  const recordError = recordErrorCandidate;
938
+ // Terminal evidence may also arrive positionally through arg5 (legacy
939
+ // callers) when it does not carry a recordError shape; normalize it so
940
+ // the empty predicate below sees every evidence channel consistently.
941
+ const legacyEvidenceCandidate =
942
+ typeof arg5 === "object" && arg5 !== null && !("code" in arg5)
943
+ ? (arg5 as { content?: TurnResultContent; hasActivity?: boolean; outcomeKind?: string })
944
+ : undefined;
945
+ const evidence = arg6 ?? legacyEvidenceCandidate;
883
946
  const recordKey = key(kind, correlation);
884
947
  const record = records.get(recordKey);
885
948
  if (!record || record.terminalAt !== undefined || record.kind !== kind) return;
@@ -896,6 +959,19 @@ export function createInvocationReconciliation(
896
959
  recordError !== undefined
897
960
  ? { code: recordError.code, message: recordError.message }
898
961
  : { code: finalOutcome.code, message: finalOutcome.message };
962
+ } else if (kind === "prompt" && finalOutcome === undefined) {
963
+ // Evidence-based empty predicate (exact-head review P1/P2): the same
964
+ // semantics as the noteTransition agent_end classifier. A
965
+ // content-bearing, activity-bearing, or explicitly stopped
966
+ // finalization without an explicit outcome is a successful terminal;
967
+ // only a genuinely empty, no-activity completion fails closed.
968
+ const terminalText = evidence?.content?.text.trim() ?? "";
969
+ if (terminalText === "" && !evidence?.hasActivity && evidence?.outcomeKind !== "stopped") {
970
+ finalizedRecord.status = "failed";
971
+ finalizedRecord.error = EMPTY_PROMPT_FAILURE;
972
+ } else {
973
+ finalizedRecord.status = "terminal_ok";
974
+ }
899
975
  } else {
900
976
  finalizedRecord.status = "terminal_ok";
901
977
  }
@@ -1057,21 +1133,50 @@ function createQuerySurface(
1057
1133
  return undefined;
1058
1134
  };
1059
1135
  const getProfileCredentialSessionId = () => ctx.credentialSessionId ?? id;
1136
+ const profileSettings = (options.settings ?? ctx.settings) as Pick<Settings, "get"> | undefined;
1137
+ const getProfileAvailableModels = (): Model<Api>[] => {
1138
+ const getAvailableForProfileActivation = ctx.modelRegistry.getAvailableForProfileActivation;
1139
+ return typeof getAvailableForProfileActivation === "function"
1140
+ ? getAvailableForProfileActivation.call(ctx.modelRegistry)
1141
+ : ctx.modelRegistry.getAvailable();
1142
+ };
1060
1143
  const resolveProfileAvailability = async (
1061
1144
  profile: ModelProfileDefinition,
1062
1145
  authenticatedProviders: ReadonlySet<string>,
1063
1146
  ): Promise<{ available: boolean; defaultModel?: Model<Api> }> => {
1147
+ if (profile.source !== "user" && inspectProxyProviderId(profileSettings).status === "invalid")
1148
+ return { available: false };
1149
+ const proxyProvider = profile.source === "user" ? undefined : tryResolveProxyProviderId(profileSettings);
1150
+ const proxyAuthenticated = proxyProvider !== undefined && authenticatedProviders.has(proxyProvider);
1151
+ const profileAuthenticated = new Set(authenticatedProviders);
1152
+ if (proxyAuthenticated) {
1153
+ for (const provider of getProxyRoutableProviders(profile)) profileAuthenticated.add(provider);
1154
+ }
1064
1155
  const rewriteSelectorProvider = (selector: string): string => {
1065
1156
  const slash = selector.indexOf("/");
1066
1157
  if (slash < 0) return selector;
1067
1158
  const provider = selector.slice(0, slash);
1068
- if (authenticatedProviders.has(provider)) return selector;
1159
+ if (profileAuthenticated.has(provider)) return selector;
1069
1160
  const group = (profile.alternativeProviderGroups ?? []).find(candidates => candidates.includes(provider));
1070
1161
  if (!group) return selector;
1071
- const replacement = group.find(candidate => authenticatedProviders.has(candidate));
1162
+ const replacement = group.find(candidate => profileAuthenticated.has(candidate));
1072
1163
  return replacement ? replacement + selector.slice(slash) : selector;
1073
1164
  };
1074
1165
  try {
1166
+ const proxyMode = profile.source === "user" ? "fallback" : resolveProxyMode(profileSettings);
1167
+ if (profile.source !== "user") {
1168
+ const configuredProviders = ctx.modelRegistry.getConfiguredProviderIds?.() ?? [];
1169
+ if (proxyProvider !== undefined && !configuredProviders.includes(proxyProvider))
1170
+ return { available: false };
1171
+ }
1172
+ if (profile.source !== "user" && proxyMode === "always") {
1173
+ if (
1174
+ proxyProvider === undefined ||
1175
+ !proxyAuthenticated ||
1176
+ !(ctx.modelRegistry.getConfiguredProviderIds?.() ?? []).includes(proxyProvider)
1177
+ )
1178
+ return { available: false };
1179
+ }
1075
1180
  const bindings = resolveProfileBindings(profile);
1076
1181
  const assignments: Array<{ value: ModelSelectorValue; isDefault: boolean }> = [];
1077
1182
  if (bindings.defaultSelector !== undefined) {
@@ -1079,23 +1184,44 @@ function createQuerySurface(
1079
1184
  }
1080
1185
  for (const value of Object.values(bindings.modelRoles)) assignments.push({ value, isDefault: false });
1081
1186
  for (const value of Object.values(bindings.agentModelOverrides)) assignments.push({ value, isDefault: false });
1187
+ const availableModels = getProfileAvailableModels();
1188
+ const resolutionRegistry = {
1189
+ ...ctx.modelRegistry,
1190
+ getAvailable: () => availableModels,
1191
+ getApiKey: (model: Model<Api>, sessionId?: string) =>
1192
+ ctx.modelRegistry.getApiKeyForProvider(model.provider, sessionId, model.baseUrl),
1193
+ resolveCanonicalModel: ctx.modelRegistry.resolveCanonicalModel?.bind(ctx.modelRegistry),
1194
+ getCanonicalVariants: ctx.modelRegistry.getCanonicalVariants?.bind(ctx.modelRegistry),
1195
+ getCanonicalId: ctx.modelRegistry.getCanonicalId?.bind(ctx.modelRegistry),
1196
+ resolveModelByLookupAlias: ctx.modelRegistry.resolveModelByLookupAlias?.bind(ctx.modelRegistry),
1197
+ lookupAliasExists: ctx.modelRegistry.lookupAliasExists?.bind(ctx.modelRegistry),
1198
+ clearCanonicalVariant: ctx.modelRegistry.clearCanonicalVariant?.bind(ctx.modelRegistry),
1199
+ };
1082
1200
  let defaultModel: Model<Api> | undefined;
1083
1201
  for (const assignment of assignments) {
1084
- const selectors = normalizeModelSelectorValue(assignment.value).map(rewriteSelectorProvider);
1202
+ let selectors = normalizeModelSelectorValue(assignment.value).map(rewriteSelectorProvider);
1203
+ if (proxyProvider !== undefined && proxyAuthenticated && profile.source !== "user") {
1204
+ selectors = selectors.map(selector =>
1205
+ rewriteSelectorForProxy(
1206
+ selector,
1207
+ proxyProvider,
1208
+ proxyMode,
1209
+ availableModels,
1210
+ new Set(authenticatedProviders),
1211
+ getProxyRoutableProviders(profile),
1212
+ ),
1213
+ );
1214
+ }
1085
1215
  const hasBareSelector = selectors.some(selector => {
1086
1216
  const suffix = splitSelectorThinkingSuffix(selector);
1087
1217
  const identity = suffix.thinkingLevel ? suffix.selector : selector;
1088
1218
  return !identity.includes("/");
1089
1219
  });
1090
- if (!assignment.isDefault && !hasBareSelector) continue;
1220
+ if (!assignment.isDefault && !requiresQualifiedModelProfileRoleResolution(profile) && !hasBareSelector)
1221
+ continue;
1091
1222
  const resolution = await resolveModelChainWithAuth(
1092
1223
  selectors,
1093
- {
1094
- ...ctx.modelRegistry,
1095
- getAvailable: () => ctx.modelRegistry.getAvailable(),
1096
- getApiKey: (model: Model<Api>, sessionId?: string) =>
1097
- ctx.modelRegistry.getApiKeyForProvider(model.provider, sessionId, model.baseUrl),
1098
- },
1224
+ resolutionRegistry,
1099
1225
  options.settings,
1100
1226
  getProfileCredentialSessionId(),
1101
1227
  {
@@ -1113,6 +1239,45 @@ function createQuerySurface(
1113
1239
  return { available: false };
1114
1240
  }
1115
1241
  };
1242
+ const collectProfileAuthentication = async (
1243
+ profiles: ReadonlyMap<string, ModelProfileDefinition>,
1244
+ ): Promise<Set<string>> => {
1245
+ const providers = new Set<string>();
1246
+ for (const profile of profiles.values()) {
1247
+ for (const provider of profile.requiredProviders) providers.add(provider);
1248
+ for (const group of profile.alternativeProviderGroups ?? []) {
1249
+ for (const provider of group) providers.add(provider);
1250
+ }
1251
+ for (const provider of deriveModelProfileMappedProviders(profile)) providers.add(provider);
1252
+ }
1253
+ const authenticated = new Set<string>();
1254
+ await Promise.all(
1255
+ [...providers].map(async provider => {
1256
+ try {
1257
+ const apiKey = await ctx.modelRegistry.getApiKeyForProvider(provider, getProfileCredentialSessionId());
1258
+ if (apiKey === kNoAuth || isAuthenticated(apiKey)) authenticated.add(provider);
1259
+ } catch {
1260
+ // A provider whose credential state cannot be read is not currently configurable.
1261
+ }
1262
+ }),
1263
+ );
1264
+ const proxyProviders = new Set<string>();
1265
+ for (const profile of profiles.values()) {
1266
+ if (profile.source === "user") continue;
1267
+ const proxyProvider = tryResolveProxyProviderId(profileSettings);
1268
+ if (proxyProvider !== undefined) proxyProviders.add(proxyProvider);
1269
+ }
1270
+ for (const proxyProvider of proxyProviders) {
1271
+ try {
1272
+ const apiKey = await ctx.modelRegistry.getApiKeyForProvider(proxyProvider, getProfileCredentialSessionId());
1273
+ if (apiKey === kNoAuth || isAuthenticated(apiKey)) authenticated.add(proxyProvider);
1274
+ } catch {
1275
+ // Passive availability must degrade to unavailable when proxy credential
1276
+ // refresh/storage fails; explicit activation retains its diagnostics.
1277
+ }
1278
+ }
1279
+ return authenticated;
1280
+ };
1116
1281
  const getDiff = async () => {
1117
1282
  try {
1118
1283
  const { stdout } = await execFileAsync("git", ["diff", "--no-ext-diff"], {
@@ -1245,9 +1410,7 @@ function createQuerySurface(
1245
1410
  if (collision) return degraded();
1246
1411
  let authenticatedProviders: ReadonlySet<string>;
1247
1412
  try {
1248
- authenticatedProviders = await collectAuthenticatedProfileProviders(profiles, provider =>
1249
- ctx.modelRegistry.getApiKeyForProvider(provider, getProfileCredentialSessionId()),
1250
- );
1413
+ authenticatedProviders = await collectProfileAuthentication(profiles);
1251
1414
  } catch {
1252
1415
  // Availability join failed: degrade only the synthetic facade,
1253
1416
  // retain concrete rows and the active marker readback.
@@ -1265,7 +1428,12 @@ function createQuerySurface(
1265
1428
  );
1266
1429
  const availableProfileIds = new Set<string>();
1267
1430
  for (const [name, profile] of profiles) {
1268
- if (!isModelProfileProviderAvailable(profile, authenticatedProviders)) continue;
1431
+ if (profile.source !== "user" && inspectProxyProviderId(profileSettings).status === "invalid") continue;
1432
+ const profileAuthenticated = new Set(authenticatedProviders);
1433
+ const proxyProvider = profile.source === "user" ? undefined : tryResolveProxyProviderId(profileSettings);
1434
+ if (proxyProvider !== undefined && profileAuthenticated.has(proxyProvider))
1435
+ for (const provider of getProxyRoutableProviders(profile)) profileAuthenticated.add(provider);
1436
+ if (!isModelProfileProviderAvailable(profile, profileAuthenticated)) continue;
1269
1437
  if (!fullyResolvedProfiles.has(name)) continue;
1270
1438
  // A profile with a default mapping is selectable only when its
1271
1439
  // default chain actually resolves to an authenticated model:
@@ -1336,18 +1504,19 @@ function createQuerySurface(
1336
1504
  (options.steerStatusLookup ?? (value => reconciliation.lookup("steer", value)))(selector),
1337
1505
  getModelProfiles: async () => {
1338
1506
  const profiles = ctx.modelRegistry.getModelProfiles();
1339
- const authenticatedProviders = await collectAuthenticatedProfileProviders(profiles, provider =>
1340
- ctx.modelRegistry.getApiKeyForProvider(provider, getProfileCredentialSessionId()),
1341
- );
1507
+ const authenticatedProviders = await collectProfileAuthentication(profiles);
1342
1508
  return (await Promise.all(
1343
- projectModelProfileCatalog(profiles, ctx.modelRegistry.getError()).map(async item => ({
1344
- ...item,
1345
- available:
1346
- isModelProfileProviderAvailable(profiles.get(item.id)!, authenticatedProviders) &&
1347
- (
1348
- await resolveProfileAvailability(profiles.get(item.id)!, authenticatedProviders)
1349
- ).available,
1350
- })),
1509
+ projectModelProfileCatalog(profiles, ctx.modelRegistry.getError()).map(async item => {
1510
+ const profile = profiles.get(item.id)!;
1511
+ const profileAuthenticated = new Set(authenticatedProviders);
1512
+ const proxyProvider = profile.source === "user" ? undefined : tryResolveProxyProviderId(profileSettings);
1513
+ if (proxyProvider !== undefined && profileAuthenticated.has(proxyProvider))
1514
+ for (const provider of getProxyRoutableProviders(profile)) profileAuthenticated.add(provider);
1515
+ const available =
1516
+ isModelProfileProviderAvailable(profile, profileAuthenticated) &&
1517
+ (await resolveProfileAvailability(profile, authenticatedProviders)).available;
1518
+ return { ...item, available };
1519
+ }),
1351
1520
  )) as unknown[];
1352
1521
  },
1353
1522
  installedQueries: policy.installedQueries,
@@ -1600,6 +1769,55 @@ function providerFailureFromAgentEnd(event: unknown): { code: string; message: s
1600
1769
  }
1601
1770
  }
1602
1771
 
1772
+ interface PromptTerminalEvidence {
1773
+ content?: TurnResultContent;
1774
+ hasActivity: boolean;
1775
+ }
1776
+
1777
+ function promptTerminalEvidenceFromAgentEnd(event: unknown): PromptTerminalEvidence {
1778
+ try {
1779
+ if (!event || typeof event !== "object") return { hasActivity: false };
1780
+ const messages = (event as { messages?: unknown }).messages;
1781
+ if (!Array.isArray(messages)) return { hasActivity: false };
1782
+ const assistant = [...messages]
1783
+ .reverse()
1784
+ .find(
1785
+ message => message && typeof message === "object" && (message as { role?: unknown }).role === "assistant",
1786
+ );
1787
+ if (!assistant || typeof assistant !== "object") return { hasActivity: false };
1788
+ const content = (assistant as { content?: unknown }).content;
1789
+ if (typeof content === "string") {
1790
+ const bounded = sanitizeTurnResultContent(content);
1791
+ return { content: bounded, hasActivity: content.trim().length > 0 };
1792
+ }
1793
+ if (Array.isArray(content)) {
1794
+ const hasActivity = content.some(block => {
1795
+ if (block === null || typeof block !== "object") return false;
1796
+ const type = (block as { type?: unknown }).type;
1797
+ if (type === "text") {
1798
+ const text = (block as { text?: unknown }).text;
1799
+ return typeof text === "string" && text.trim().length > 0;
1800
+ }
1801
+ return typeof type === "string" && type.length > 0;
1802
+ });
1803
+ const text = content
1804
+ .filter(
1805
+ (block): block is { type: "text"; text: string } =>
1806
+ block !== null &&
1807
+ typeof block === "object" &&
1808
+ (block as { type?: unknown }).type === "text" &&
1809
+ typeof (block as { text?: unknown }).text === "string",
1810
+ )
1811
+ .map(block => block.text)
1812
+ .join("");
1813
+ return { content: sanitizeTurnResultContent(text), hasActivity };
1814
+ }
1815
+ return { content: sanitizeTurnResultContent(""), hasActivity: false };
1816
+ } catch {
1817
+ return { hasActivity: false };
1818
+ }
1819
+ }
1820
+
1603
1821
  function createControlSurface(
1604
1822
  ctx: ExtensionContext,
1605
1823
  api: ExtensionAPI,
@@ -1611,6 +1829,7 @@ function createControlSurface(
1611
1829
  startsOwnTurn: boolean,
1612
1830
  sdkRunToken: string,
1613
1831
  ) => void,
1832
+ armPromptDeadline: (correlation: InvocationCorrelation) => void,
1614
1833
  steerReconciliation: KindAwareReconciliation,
1615
1834
  onPromotedTurn?: (
1616
1835
  kind: InvocationKind,
@@ -1638,6 +1857,36 @@ function createControlSurface(
1638
1857
  canResolveGate: () => boolean = () => true,
1639
1858
  trackGateResolution: <T>(resolution: Promise<T>) => Promise<T> = async resolution => await resolution,
1640
1859
  ): ControlSurface {
1860
+ const normalizePromptImages = (value: unknown): ImageContent[] => {
1861
+ if (!Array.isArray(value)) return [];
1862
+ const normalized: ImageContent[] = [];
1863
+ for (const candidate of value) {
1864
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) continue;
1865
+ const image = candidate as Record<string, unknown>;
1866
+ if (typeof image.data !== "string" || image.data.trim().length === 0) continue;
1867
+ if (image.mimeType !== undefined && typeof image.mimeType !== "string") continue;
1868
+ normalized.push({
1869
+ type: "image",
1870
+ data: image.data,
1871
+ mimeType: typeof image.mimeType === "string" && image.mimeType.length > 0 ? image.mimeType : "image/png",
1872
+ });
1873
+ }
1874
+ return normalized;
1875
+ };
1876
+ type InternalSendOptions = NonNullable<Parameters<ExtensionAPI["sendUserMessage"]>[1]> & {
1877
+ sdkRunCapability?: SdkRunCapability;
1878
+ };
1879
+ type InternalSdkApi = Omit<ExtensionAPI, "sendUserMessage"> & {
1880
+ sendUserMessage: (
1881
+ content: Parameters<ExtensionAPI["sendUserMessage"]>[0],
1882
+ options?: InternalSendOptions,
1883
+ ) => Promise<void>;
1884
+ };
1885
+ const internalApi = api as InternalSdkApi;
1886
+ const sendSdkUserMessage = (
1887
+ content: Parameters<ExtensionAPI["sendUserMessage"]>[0],
1888
+ options?: Record<string, unknown>,
1889
+ ): Promise<void> => internalApi.sendUserMessage(content, options);
1641
1890
  const surfacePolicy =
1642
1891
  policy ?? createSdkSurfacePolicyForContext(ctx, hasSdkWorkflowGateCapability(ctx.workflowGate));
1643
1892
  const typed = (operation: string, input: Record<string, unknown> = {}) =>
@@ -1722,7 +1971,7 @@ function createControlSurface(
1722
1971
  kind: InvocationKind,
1723
1972
  clientRef: string | undefined,
1724
1973
  run: (options: {
1725
- sdkRunToken: string;
1974
+ sdkRunCapability: SdkRunCapability;
1726
1975
  onPreflightAccepted: () => void;
1727
1976
  onPreflightAcceptCommit: () => Promise<void>;
1728
1977
  /** Internal disposition before a queued submission is actually consumed. */
@@ -1743,6 +1992,7 @@ function createControlSurface(
1743
1992
  reconciliation.admit(kind, retainedClientRef);
1744
1993
  const correlation = newCorrelation();
1745
1994
  const sdkRunToken = `${correlation.commandId}:${correlation.turnId}`;
1995
+ const sdkRunCapability = createSdkRunCapability(sdkRunToken);
1746
1996
  const preflight = Promise.withResolvers<void>();
1747
1997
  let accepted = false;
1748
1998
  let settled = false;
@@ -1766,6 +2016,7 @@ function createControlSurface(
1766
2016
  await reconciliation.noteAccepted(kind, correlation, retainedClientRef);
1767
2017
  accepted = true;
1768
2018
  settled = true;
2019
+ if (kind === "prompt" && startsOwnTurn) armPromptDeadline(correlation);
1769
2020
  // The accepted submission does NOT own the active turn until its run
1770
2021
  // actually STARTS: the connection is carried on the pending entry and
1771
2022
  // associated at agent_start instead (review thread P1).
@@ -1801,7 +2052,7 @@ function createControlSurface(
1801
2052
  const submission = Promise.resolve(
1802
2053
  run({
1803
2054
  onPreflightAccepted: () => void accept().catch(() => undefined),
1804
- sdkRunToken,
2055
+ sdkRunCapability,
1805
2056
  onPreflightAcceptCommit: accept,
1806
2057
  onDispatchDisposition: promotion => {
1807
2058
  promotionStartsOwnRun = promotion.startsOwnRun;
@@ -2881,52 +3132,77 @@ function createControlSurface(
2881
3132
  }
2882
3133
  };
2883
3134
  return {
2884
- prompt: async (text, images, clientRef) =>
2885
- submit("prompt", clientRef, ({ queuedAtDispatch, sdkRunToken, ...options }) =>
2886
- api.sendUserMessage(
2887
- typeof images === "undefined" ? text : ([{ type: "text", text }, ...(images as never[])] as never),
3135
+ prompt: async (text, images, clientRef) => {
3136
+ const invalid = validateRequiredPromptText("turn.prompt", { text, images });
3137
+ if (invalid) throw Object.assign(new Error(invalid.message), { code: invalid.code });
3138
+ return await submit("prompt", clientRef, ({ queuedAtDispatch, sdkRunCapability, ...options }) =>
3139
+ sendSdkUserMessage(
3140
+ typeof images === "undefined"
3141
+ ? text
3142
+ : ([{ type: "text", text }, ...normalizePromptImages(images)] as [
3143
+ { type: "text"; text: string },
3144
+ ...ImageContent[],
3145
+ ]),
2888
3146
  {
2889
3147
  ...options,
2890
- sdkRunToken,
3148
+ sdkRunCapability,
3149
+ // ACP terminal settlement is owned by the correlated agent_end
3150
+ // publication. Post-prompt recovery may include independent
3151
+ // subagent work and must not hold that client-facing boundary.
2891
3152
  ...(queuedAtDispatch ? { queuedAtDispatch: true } : {}),
2892
3153
  },
2893
3154
  ),
2894
- ),
3155
+ );
3156
+ },
2895
3157
  steer: async (text, clientRef) => {
3158
+ const invalid = validateRequiredPromptText("turn.steer", { text });
3159
+ if (invalid) throw Object.assign(new Error(invalid.message), { code: invalid.code });
2896
3160
  const retainedClientRef = normalizeClientRef(clientRef);
2897
3161
  if (retainedClientRef === undefined) {
2898
3162
  const correlation = newCorrelation();
2899
- await api.sendUserMessage(text, { deliverAs: "steer" });
3163
+ await sendSdkUserMessage(text, { deliverAs: "steer" });
2900
3164
  return { accepted: true, ...correlation };
2901
3165
  }
2902
3166
  const durable = steerReconciliation;
2903
3167
  const reservation = await durable.reserveSteer(retainedClientRef, text);
2904
3168
  if (reservation.replay) return { accepted: reservation.result.status === "accepted", ...reservation.result };
2905
3169
  try {
2906
- await api.sendUserMessage(text, { deliverAs: "steer" });
3170
+ await sendSdkUserMessage(text, { deliverAs: "steer" });
2907
3171
  return { accepted: true, ...(await durable.settleSteer(retainedClientRef, "accepted")) };
2908
3172
  } catch (error) {
2909
3173
  return { accepted: false, ...(await durable.settleSteer(retainedClientRef, "rejected", error)) };
2910
3174
  }
2911
3175
  },
2912
- followUp: async text =>
2913
- submit(
3176
+ followUp: async text => {
3177
+ const invalid = validateRequiredPromptText("turn.follow_up", { text });
3178
+ if (invalid) throw Object.assign(new Error(invalid.message), { code: invalid.code });
3179
+ return await submit(
2914
3180
  "prompt",
2915
3181
  undefined,
2916
- options => api.sendUserMessage(text, { ...options, deliverAs: "followUp" }),
3182
+ ({ sdkRunCapability, ...options }) =>
3183
+ sendSdkUserMessage(text, {
3184
+ ...options,
3185
+ sdkRunCapability,
3186
+ deliverAs: "followUp",
3187
+ }),
2917
3188
  undefined,
2918
3189
  false,
2919
3190
  // Follow-ups never start inline; ownership correlates at promotion.
2920
3191
  true,
2921
- ),
3192
+ );
3193
+ },
2922
3194
  abort: async () => {
2923
3195
  await Promise.resolve(ctx.abort()).catch(() => undefined);
2924
3196
  return { aborted: true };
2925
3197
  },
2926
3198
  abortTerminal: terminalAbort,
2927
3199
  abortAndPrompt: async text => {
3200
+ const invalid = validateRequiredPromptText("turn.abort_and_prompt", { text });
3201
+ if (invalid) throw Object.assign(new Error(invalid.message), { code: invalid.code });
2928
3202
  await ctx.abort();
2929
- return await submit("prompt", undefined, options => api.sendUserMessage(text, options));
3203
+ return await submit("prompt", undefined, ({ sdkRunCapability, ...options }) =>
3204
+ sendSdkUserMessage(text, { ...options, sdkRunCapability }),
3205
+ );
2930
3206
  },
2931
3207
  answerAsk: unavailable("ask.answer"),
2932
3208
  answerGate: async (id, response, expectedSessionId, idempotencyKey) =>
@@ -2953,9 +3229,10 @@ function createControlSurface(
2953
3229
  return await submit(
2954
3230
  "skill",
2955
3231
  clientRef,
2956
- options =>
3232
+ ({ sdkRunCapability, ...options }) =>
2957
3233
  ctx.invokeSkill!(name, args, {
2958
3234
  ...options,
3235
+ sdkRunCapability,
2959
3236
  onSkillPrepared: meta => {
2960
3237
  prepared = meta;
2961
3238
  },
@@ -3374,6 +3651,9 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
3374
3651
  failureCause?: unknown,
3375
3652
  maintenanceOutcome?: string,
3376
3653
  lifecycleOwner?: LifecycleOwner,
3654
+ terminalContent?: TurnResultContent,
3655
+ terminalHasActivity = false,
3656
+ terminalOutcome?: { kind: "stopped"; reason: "cancelled"; provenance: "client_cancel" },
3377
3657
  ): Promise<void> => {
3378
3658
  const current = lifecycleOwner?.state ?? active;
3379
3659
  if (!current) return;
@@ -3461,6 +3741,11 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
3461
3741
  if (current.activeInvocation?.kind === "prompt") {
3462
3742
  current.deadlineManager.onAccepted(current.activeInvocation.correlation);
3463
3743
  }
3744
+ } else {
3745
+ // An empty drain is an agent-initiated successor run. Clear the
3746
+ // predecessor's SDK owner and active invocation so abort ownership and
3747
+ // tool-progress renewal cannot leak into the successor.
3748
+ adoptLifecycleBatch(undefined);
3464
3749
  }
3465
3750
  transitions = drained.map(({ kind, correlation }) => ({ kind, correlation }));
3466
3751
  } else {
@@ -3626,7 +3911,17 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
3626
3911
  const reasonKey = `${invocation.correlation.commandId}:${invocation.correlation.turnId}`;
3627
3912
  const unrecorded = type === "agent_end" ? current.unrecordedFailureReasons?.get(reasonKey) : undefined;
3628
3913
  if (type === "agent_end" && invocation.kind === "prompt")
3629
- current.deadlineManager.noteTerminalTransition(invocation.correlation, unrecorded);
3914
+ current.deadlineManager.noteTerminalTransition(
3915
+ invocation.correlation,
3916
+ unrecorded,
3917
+ terminalContent !== undefined || terminalHasActivity || terminalOutcome !== undefined
3918
+ ? ({
3919
+ content: terminalContent,
3920
+ hasActivity: terminalHasActivity,
3921
+ outcome: terminalOutcome,
3922
+ } satisfies PromptTerminalTransitionEvidence)
3923
+ : undefined,
3924
+ );
3630
3925
  if (type === "agent_end") {
3631
3926
  // Compound recovery (exact-head review P1): if this run's
3632
3927
  // agent_failed write failed, re-record the sanitized reason
@@ -3650,7 +3945,16 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
3650
3945
  error:
3651
3946
  failureCause ?? Object.assign(new Error("agent run failed"), { code: "agent_failed" }),
3652
3947
  }
3653
- : { type };
3948
+ : {
3949
+ type,
3950
+ ...(type === "agent_end" && terminalContent !== undefined
3951
+ ? { content: terminalContent }
3952
+ : {}),
3953
+ ...(type === "agent_end" && terminalHasActivity ? { hasActivity: true } : {}),
3954
+ ...(type === "agent_end" && terminalOutcome !== undefined
3955
+ ? { outcome: terminalOutcome }
3956
+ : {}),
3957
+ };
3654
3958
  await current.reconciliation.noteTransition(invocation.kind, invocation.correlation, frame as never);
3655
3959
  if ((type as string) === "agent_end") {
3656
3960
  if (invocation.kind === "prompt") current.deadlineManager.clear(invocation.correlation);
@@ -3814,6 +4118,11 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
3814
4118
  owner?.failureDiagnosticKeys.has(lifecycleCorrelationKey(correlation)),
3815
4119
  );
3816
4120
  const failureAlreadyPublished = failure === undefined || (hasExistingFailure && genericFailureKeys.length === 0);
4121
+ const terminalEvidence = promptTerminalEvidenceFromAgentEnd(event);
4122
+ const terminalOutcome =
4123
+ event.stopReason === "cancelled"
4124
+ ? ({ kind: "stopped", reason: "cancelled", provenance: "client_cancel" } as const)
4125
+ : undefined;
3817
4126
  return trackLifecycle(async () => {
3818
4127
  if (failure && !failureAlreadyPublished) {
3819
4128
  for (const key of genericFailureKeys) owner?.failureDiagnosticKeys.delete(key);
@@ -3825,6 +4134,9 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
3825
4134
  undefined,
3826
4135
  event.stopReason === "maintenance" ? event.maintenanceOutcome : undefined,
3827
4136
  lifecycleOwner,
4137
+ terminalEvidence.content,
4138
+ terminalEvidence.hasActivity,
4139
+ terminalOutcome,
3828
4140
  );
3829
4141
  }, owner).finally(() => {
3830
4142
  if (typeof event.sdkRunToken === "string" && lifecycleRunOwners.get(event.sdkRunToken)?.state === owner)
@@ -4159,8 +4471,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4159
4471
  // promotion/agent_start instead, so a prompt waiting behind a
4160
4472
  // legitimately long turn never false-fires. The agent_start
4161
4473
  // re-entry in emitLifecycle is a no-op for an existing lease.
4162
- if (startsOwnTurn && kind === "prompt") deadlineManager.onAccepted(correlation);
4163
4474
  },
4475
+ correlation => deadlineManager.onAccepted(correlation),
4164
4476
  steerReconciliation,
4165
4477
  (kind, correlation, connectionId, sdkRunToken, promotion) => {
4166
4478
  const bindPromotedToken = (batch?: LifecycleBatch): void => {