@gajae-code/coding-agent 0.12.6 → 0.12.8

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 (150) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/dist/types/async/job-manager.d.ts +36 -1
  3. package/dist/types/commands/notify.d.ts +4 -0
  4. package/dist/types/config/file-lock.d.ts +3 -0
  5. package/dist/types/config/model-discovery-manager.d.ts +6 -1
  6. package/dist/types/config/model-registry.d.ts +2 -0
  7. package/dist/types/config/resolve-config-value.d.ts +2 -1
  8. package/dist/types/config/settings-schema.d.ts +17 -1
  9. package/dist/types/eval/backend.d.ts +2 -2
  10. package/dist/types/exec/bash-executor.d.ts +8 -10
  11. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -36
  12. package/dist/types/internal-urls/registry-helpers.d.ts +0 -18
  13. package/dist/types/main.d.ts +1 -0
  14. package/dist/types/modes/components/hook-selector.d.ts +20 -0
  15. package/dist/types/modes/components/notifications-settings-editor.d.ts +53 -4
  16. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  17. package/dist/types/modes/controllers/selector-controller.d.ts +4 -2
  18. package/dist/types/sdk/broker/broker.d.ts +5 -1
  19. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
  20. package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -3
  21. package/dist/types/sdk/bus/config.d.ts +75 -113
  22. package/dist/types/sdk/bus/discord-live-provider.d.ts +8 -2
  23. package/dist/types/sdk/bus/discord-provider.d.ts +19 -0
  24. package/dist/types/sdk/bus/index.d.ts +1 -1
  25. package/dist/types/sdk/bus/notification-orchestration.d.ts +127 -18
  26. package/dist/types/sdk/bus/notification-service.d.ts +31 -4
  27. package/dist/types/sdk/bus/session-control.d.ts +8 -3
  28. package/dist/types/sdk/bus/slack-live-provider.d.ts +9 -2
  29. package/dist/types/sdk/bus/slack-provider.d.ts +22 -0
  30. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
  31. package/dist/types/sdk/client/discovery.d.ts +2 -1
  32. package/dist/types/sdk/host/host.d.ts +2 -23
  33. package/dist/types/sdk/host/query/handlers.d.ts +2 -0
  34. package/dist/types/sdk/host/reverse-leases.d.ts +4 -7
  35. package/dist/types/sdk/index.d.ts +1 -0
  36. package/dist/types/sdk/providers.d.ts +22 -0
  37. package/dist/types/sdk/session-directory.d.ts +1 -1
  38. package/dist/types/session/artifacts.d.ts +5 -8
  39. package/dist/types/session/client-bridge.d.ts +0 -14
  40. package/dist/types/session/internal/managed-session-scope.d.ts +17 -1
  41. package/dist/types/session/internal/managed-session-storage.d.ts +11 -4
  42. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -0
  43. package/dist/types/session/internal/session-open-errors.d.ts +5 -0
  44. package/dist/types/session/messages.d.ts +1 -4
  45. package/dist/types/session/session-manager.d.ts +7 -6
  46. package/dist/types/session/session-storage.d.ts +2 -0
  47. package/dist/types/session/streaming-output.d.ts +0 -32
  48. package/dist/types/task/receipt.d.ts +1 -2
  49. package/dist/types/task/types.d.ts +43 -178
  50. package/dist/types/tools/bash.d.ts +1 -1
  51. package/dist/types/tools/fetch.d.ts +0 -3
  52. package/dist/types/tools/output-meta.d.ts +5 -37
  53. package/dist/types/tools/subagent.d.ts +15 -0
  54. package/dist/types/tools/todo-write.d.ts +3 -0
  55. package/dist/types/tools/tool-result.d.ts +0 -1
  56. package/package.json +10 -8
  57. package/scripts/build-sdk-package-smoke.ts +51 -0
  58. package/scripts/capture-notifications-settings-showcase.ts +1 -1
  59. package/src/async/job-manager.ts +430 -46
  60. package/src/cli/notify-cli.ts +204 -94
  61. package/src/commands/notify.ts +19 -8
  62. package/src/config/file-lock.ts +6 -0
  63. package/src/config/model-discovery-manager.ts +50 -8
  64. package/src/config/model-registry.ts +862 -75
  65. package/src/config/resolve-config-value.ts +10 -8
  66. package/src/config/settings-schema.ts +25 -15
  67. package/src/config/settings.ts +15 -15
  68. package/src/cursor.ts +13 -20
  69. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -3
  70. package/src/eval/backend.ts +2 -2
  71. package/src/exec/bash-executor.ts +151 -425
  72. package/src/gjc-runtime/session-state-sidecar.ts +12 -2
  73. package/src/gjc-runtime/ultragoal-runtime.ts +3 -280
  74. package/src/gjc-runtime/workflow-manifest.generated.json +1 -40
  75. package/src/gjc-runtime/workflow-manifest.ts +0 -12
  76. package/src/internal-urls/agent-protocol.ts +38 -98
  77. package/src/internal-urls/artifact-protocol.ts +17 -28
  78. package/src/internal-urls/docs-index.generated.ts +7 -7
  79. package/src/internal-urls/registry-helpers.ts +0 -62
  80. package/src/main.ts +43 -7
  81. package/src/modes/DESIGN.md +11 -11
  82. package/src/modes/acp/acp-agent.ts +0 -24
  83. package/src/modes/acp/acp-client-bridge.ts +11 -154
  84. package/src/modes/components/hook-selector.ts +155 -44
  85. package/src/modes/components/model-selector.ts +242 -49
  86. package/src/modes/components/notifications-settings-editor.ts +631 -85
  87. package/src/modes/controllers/extension-ui-controller.ts +114 -15
  88. package/src/modes/controllers/selector-controller.ts +464 -50
  89. package/src/prompts/tools/bash.md +2 -2
  90. package/src/sdk/acp/adapter.ts +9 -200
  91. package/src/sdk/broker/broker.ts +54 -10
  92. package/src/sdk/broker/lifecycle-ledger.ts +34 -1
  93. package/src/sdk/broker/lifecycle.ts +47 -50
  94. package/src/sdk/bus/chat-daemon-cli.ts +12 -5
  95. package/src/sdk/bus/chat-daemon-control.ts +35 -8
  96. package/src/sdk/bus/chat-daemon-runtime.ts +16 -2
  97. package/src/sdk/bus/config.ts +534 -286
  98. package/src/sdk/bus/discord-live-provider.ts +57 -2
  99. package/src/sdk/bus/discord-provider.ts +21 -0
  100. package/src/sdk/bus/index.ts +105 -359
  101. package/src/sdk/bus/notification-orchestration.ts +371 -68
  102. package/src/sdk/bus/notification-service.ts +431 -130
  103. package/src/sdk/bus/session-control.ts +119 -44
  104. package/src/sdk/bus/slack-live-provider.ts +78 -2
  105. package/src/sdk/bus/slack-provider.ts +24 -0
  106. package/src/sdk/bus/telegram-daemon-cli.ts +9 -2
  107. package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
  108. package/src/sdk/bus/telegram-daemon-control.ts +3 -3
  109. package/src/sdk/bus/telegram-daemon.ts +23 -15
  110. package/src/sdk/client/discovery.ts +10 -4
  111. package/src/sdk/host/host.ts +9 -187
  112. package/src/sdk/host/query/handlers.ts +20 -1
  113. package/src/sdk/host/reverse-leases.ts +42 -249
  114. package/src/sdk/index.ts +1 -0
  115. package/src/sdk/protocol/operation-inventory.generated.json +18 -0
  116. package/src/sdk/protocol/operation-registry.ts +5 -2
  117. package/src/sdk/providers.ts +51 -0
  118. package/src/sdk/session-directory.ts +1 -0
  119. package/src/sdk/session.ts +10 -7
  120. package/src/session/agent-session.ts +59 -202
  121. package/src/session/artifacts.ts +74 -211
  122. package/src/session/client-bridge.ts +0 -15
  123. package/src/session/internal/managed-session-scope.ts +981 -450
  124. package/src/session/internal/managed-session-storage.ts +362 -124
  125. package/src/session/internal/native-publish-outcome.ts +3 -1
  126. package/src/session/internal/session-open-errors.ts +11 -0
  127. package/src/session/messages.ts +7 -32
  128. package/src/session/session-manager.ts +48 -15
  129. package/src/session/session-storage.ts +72 -5
  130. package/src/session/streaming-output.ts +40 -198
  131. package/src/slash-commands/builtin-registry.ts +82 -4
  132. package/src/task/index.ts +237 -65
  133. package/src/task/receipt.ts +2 -4
  134. package/src/task/render.ts +0 -30
  135. package/src/task/types.ts +10 -34
  136. package/src/tools/ask.ts +1 -1
  137. package/src/tools/bash.ts +239 -818
  138. package/src/tools/eval.ts +4 -99
  139. package/src/tools/fetch.ts +15 -77
  140. package/src/tools/find.ts +3 -7
  141. package/src/tools/output-meta.ts +114 -696
  142. package/src/tools/search.ts +4 -9
  143. package/src/tools/subagent.ts +97 -75
  144. package/src/tools/telegram-send.ts +4 -2
  145. package/src/tools/todo-write.ts +47 -0
  146. package/src/tools/tool-result.ts +0 -1
  147. package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +0 -83
  148. package/dist/types/utils/artifact-id.d.ts +0 -2
  149. package/src/gjc-runtime/ultragoal-review-source.ts +0 -378
  150. package/src/utils/artifact-id.ts +0 -6
@@ -349,12 +349,7 @@ import {
349
349
  import { assertEditableFile } from "../tools/auto-generated-guard";
350
350
  import { releaseTabsForOwner } from "../tools/browser/tab-supervisor";
351
351
  import type { CheckpointState } from "../tools/checkpoint";
352
- import {
353
- createArtifactFailurePreview,
354
- type OutputMeta,
355
- outputMeta,
356
- wrapToolWithMetaNotice,
357
- } from "../tools/output-meta";
352
+ import { outputMeta, wrapToolWithMetaNotice } from "../tools/output-meta";
358
353
  import { normalizeLocalScheme, resolveReadPath, resolveToCwd } from "../tools/path-utils";
359
354
  import { registerResourceGcSession } from "../tools/resource-gc";
360
355
  import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from "../tools/todo-write";
@@ -1062,102 +1057,6 @@ function boundAgentBashArtifactSaveDiagnostic(error: unknown): string {
1062
1057
  return truncateHeadBytes(normalized, AGENT_BASH_ARTIFACT_SAVE_DIAGNOSTIC_MAX_BYTES).text;
1063
1058
  }
1064
1059
 
1065
- const PRE_ADMISSION_ARTIFACT_SAVE_WAIT_MS = 500;
1066
- const PRE_ADMISSION_ARTIFACT_MAX_PENDING_SAVES = 2;
1067
-
1068
- type PreAdmissionArtifactSaveOutcome =
1069
- | { kind: "saved"; saved: Awaited<ReturnType<SessionManager["saveArtifactReceipt"]>> }
1070
- | { kind: "failed"; error: unknown };
1071
-
1072
- interface PreAdmissionArtifactSaveAdmission {
1073
- readonly pending: Set<Promise<PreAdmissionArtifactSaveOutcome>>;
1074
- }
1075
-
1076
- /**
1077
- * Pre-admission spills are owned by their concrete SessionManager. A stalled
1078
- * save remains admitted until its promise settles, so timeout does not turn
1079
- * each subsequent tool result into another retained full payload.
1080
- */
1081
- const preAdmissionArtifactSaveAdmissions = new WeakMap<SessionManager, PreAdmissionArtifactSaveAdmission>();
1082
-
1083
- function preAdmissionArtifactSaveAdmissionFor(sessionManager: SessionManager): PreAdmissionArtifactSaveAdmission {
1084
- let admission = preAdmissionArtifactSaveAdmissions.get(sessionManager);
1085
- if (!admission) {
1086
- admission = { pending: new Set() };
1087
- preAdmissionArtifactSaveAdmissions.set(sessionManager, admission);
1088
- }
1089
- return admission;
1090
- }
1091
-
1092
- function beginPreAdmissionArtifactSave(
1093
- sessionManager: SessionManager,
1094
- content: string,
1095
- toolType: string,
1096
- ): { kind: "capacity" } | { kind: "started"; outcome: Promise<PreAdmissionArtifactSaveOutcome> } {
1097
- const admission = preAdmissionArtifactSaveAdmissionFor(sessionManager);
1098
- if (admission.pending.size >= PRE_ADMISSION_ARTIFACT_MAX_PENDING_SAVES) return { kind: "capacity" };
1099
-
1100
- let outcome: Promise<PreAdmissionArtifactSaveOutcome>;
1101
- try {
1102
- outcome = sessionManager.saveArtifactReceipt(content, toolType).then(
1103
- saved => ({ kind: "saved" as const, saved }),
1104
- error => ({ kind: "failed" as const, error }),
1105
- );
1106
- } catch (error) {
1107
- return { kind: "started", outcome: Promise.resolve({ kind: "failed", error }) };
1108
- }
1109
- admission.pending.add(outcome);
1110
- void outcome.then(
1111
- () => admission.pending.delete(outcome),
1112
- () => admission.pending.delete(outcome),
1113
- );
1114
- return { kind: "started", outcome };
1115
- }
1116
-
1117
- function replaceToolResultWithArtifactFailurePreview(
1118
- message: ToolResultMessage,
1119
- fullText: string,
1120
- maxInlineBytes: number | undefined,
1121
- diagnostic: string,
1122
- ): void {
1123
- const preview = createArtifactFailurePreview(fullText, maxInlineBytes, diagnostic);
1124
- const existingDetails =
1125
- message.details && typeof message.details === "object" ? (message.details as Record<string, unknown>) : {};
1126
- const existingMeta =
1127
- existingDetails.meta && typeof existingDetails.meta === "object" ? (existingDetails.meta as OutputMeta) : {};
1128
- const existingTruncation = existingMeta.truncation;
1129
- const totalLines = fullText.length > 0 ? fullText.split("\n").length : 0;
1130
- const outputLines = preview.text.length > 0 ? preview.text.split("\n").length : 0;
1131
- const outputBytes = Buffer.byteLength(preview.text, "utf-8");
1132
- const truncation: NonNullable<OutputMeta["truncation"]> = {
1133
- ...(existingTruncation ?? {}),
1134
- direction: "tail",
1135
- truncatedBy: "bytes",
1136
- noticeOwner: undefined,
1137
- totalLines,
1138
- totalBytes: Buffer.byteLength(fullText, "utf-8"),
1139
- outputLines,
1140
- outputBytes,
1141
- maxBytes: maxInlineBytes,
1142
- shownRange: undefined,
1143
- headRange: undefined,
1144
- tailRange: undefined,
1145
- nextOffset: undefined,
1146
- artifactId: undefined,
1147
- artifactVerified: false,
1148
- sourceCaptureIncomplete: true,
1149
- artifactFailureDiagnostic: diagnostic,
1150
- };
1151
- message.details = {
1152
- ...existingDetails,
1153
- meta: { ...existingMeta, truncation },
1154
- };
1155
- message.content = [
1156
- ...message.content.filter((block): block is ImageContent => block.type === "image"),
1157
- { type: "text", text: preview.text },
1158
- ];
1159
- }
1160
-
1161
1060
  function summarizeAgentBashArtifactSave(
1162
1061
  artifactId: string,
1163
1062
  originalText: string,
@@ -1970,13 +1869,6 @@ export class AgentSession {
1970
1869
  this.#retryReplayEpoch++;
1971
1870
  this.#retryReplayUnsafeEpoch = undefined;
1972
1871
  this.#firstEventTimeoutRetryStartedAt = Date.now();
1973
- if (
1974
- this.#extensionRunner?.hasHandlers("context") ||
1975
- this.#extensionRunner?.hasHandlers("before_provider_request") ||
1976
- this.#extensionRunner?.hasHandlers("after_provider_response")
1977
- ) {
1978
- this.#markRetryReplayUnsafe();
1979
- }
1980
1872
  }
1981
1873
 
1982
1874
  #markRetryReplayUnsafe(): void {
@@ -1986,6 +1878,14 @@ export class AgentSession {
1986
1878
  get #hasCleanRetryReplaySafety(): boolean {
1987
1879
  return this.#retryReplayEpoch > 0 && this.#retryReplayUnsafeEpoch !== this.#retryReplayEpoch;
1988
1880
  }
1881
+ #bindAttemptScope(scope: AttemptScope | undefined): void {
1882
+ if (!scope) return;
1883
+ this.#attemptRecordStore.register(scope);
1884
+ this.#attemptRecordStore.establishClean(scope);
1885
+ }
1886
+ #isRetryScopeClean(scope: AttemptScope | undefined): boolean {
1887
+ return scope !== undefined && this.#attemptRecordStore.isClean(scope);
1888
+ }
1989
1889
  #prePromptContextCheckPromise: Promise<void> | undefined = undefined;
1990
1890
  /** Display-only context snapshot; pre-prompt compaction estimates deliberately remain uncached. */
1991
1891
  #contextUsageCache: { key: string; value: ContextUsage } | undefined;
@@ -1999,6 +1899,8 @@ export class AgentSession {
1999
1899
  // Handoff state
2000
1900
  #handoffAbortController: AbortController | undefined = undefined;
2001
1901
  #skipPostTurnMaintenanceAssistantTimestamp: number | undefined = undefined;
1902
+ /** Scope cleanliness captured after message_end extension delivery and before terminal agent_end retirement. */
1903
+ #assistantAttemptScopes = new WeakMap<AssistantMessage, { scope?: AttemptScope; wasClean: boolean }>();
2002
1904
 
2003
1905
  // Retry state
2004
1906
  #retryAbortController: AbortController | undefined = undefined;
@@ -2707,6 +2609,7 @@ export class AgentSession {
2707
2609
  if (this.#extensionRunner && typeof this.#extensionRunner.setAttemptRecordStore === "function") {
2708
2610
  this.#extensionRunner.setAttemptRecordStore(this.#attemptRecordStore);
2709
2611
  }
2612
+ this.agent.setMainAttemptScopeObserver(scope => this.#bindAttemptScope(scope));
2710
2613
  this.#skills = config.skills ?? [];
2711
2614
  this.#skillWarnings = config.skillWarnings ?? [];
2712
2615
  this.#customCommands = config.customCommands ?? [];
@@ -3782,101 +3685,39 @@ export class AgentSession {
3782
3685
  const textParts = message.content.flatMap(block => (block.type === "text" ? [block.text] : []));
3783
3686
  if (textParts.length === 0) return;
3784
3687
  const fullText = textParts.join("\n");
3785
- const configuredInlineKiB = this.settings.get("tools.maxInlineResultBytes");
3786
- const maxInlineBytes =
3787
- configuredInlineKiB > 0 ? Math.max(1024, Math.floor(configuredInlineKiB * 1024)) : undefined;
3788
- const totalBytes = Buffer.byteLength(fullText, "utf-8");
3789
- const exceedsInlineCap = maxInlineBytes !== undefined && totalBytes > maxInlineBytes;
3790
3688
  const contextWindow = this.model?.contextWindow;
3791
3689
  const thresholdTokens = Math.min(
3792
3690
  8_000,
3793
3691
  contextWindow && contextWindow > 0 ? Math.floor(contextWindow * 0.05) : 8_000,
3794
3692
  );
3795
- // The inline byte cap is an independent admission bound and must be checked
3796
- // before token heuristics can opt this result out of spilling.
3797
- if (!exceedsInlineCap && (thresholdTokens <= 0 || estimateTextTokensHeuristic(fullText) <= thresholdTokens))
3798
- return;
3693
+ if (thresholdTokens <= 0 || estimateTextTokensHeuristic(fullText) <= thresholdTokens) return;
3799
3694
 
3800
- const saveAdmission = beginPreAdmissionArtifactSave(this.sessionManager, fullText, "tool-result");
3801
- if (saveAdmission.kind === "capacity") {
3802
- replaceToolResultWithArtifactFailurePreview(
3803
- message,
3804
- fullText,
3805
- maxInlineBytes,
3806
- "artifact save capacity exhausted; the previous save is still settling",
3807
- );
3808
- return;
3809
- }
3810
3695
  try {
3811
- const saveOutcome = await Promise.race([
3812
- saveAdmission.outcome,
3813
- Bun.sleep(PRE_ADMISSION_ARTIFACT_SAVE_WAIT_MS).then(() => ({ kind: "timeout" as const })),
3814
- ]);
3815
- if (saveOutcome.kind !== "saved" || !saveOutcome.saved) {
3816
- const diagnostic =
3817
- saveOutcome.kind === "timeout"
3818
- ? `did not settle within ${PRE_ADMISSION_ARTIFACT_SAVE_WAIT_MS}ms`
3819
- : saveOutcome.kind === "failed"
3820
- ? boundAgentBashArtifactSaveDiagnostic(saveOutcome.error)
3821
- : "artifact storage is unavailable";
3822
- replaceToolResultWithArtifactFailurePreview(message, fullText, maxInlineBytes, diagnostic);
3823
- return;
3824
- }
3825
- const saved = saveOutcome.saved;
3826
- const existingDetails = message.details;
3827
- const detailRecord =
3828
- existingDetails && typeof existingDetails === "object" ? (existingDetails as Record<string, unknown>) : {};
3829
- const existingMeta =
3830
- detailRecord.meta && typeof detailRecord.meta === "object" ? (detailRecord.meta as OutputMeta) : {};
3831
- const existingTruncation = existingMeta.truncation;
3832
- const priorLoss = existingTruncation !== undefined;
3696
+ const artifactId = await this.sessionManager.saveArtifact(fullText, "tool-result");
3697
+ if (!artifactId) return;
3833
3698
  const digest = crypto.createHash("sha256").update(fullText).digest("hex");
3834
- const sourceCaptureIncomplete = priorLoss || !saved.complete || undefined;
3835
- const preview = createPreAdmissionArtifactSpillPreview(fullText, saved.id, digest, {
3836
- maxInlineBytes,
3837
- fullOutput: saved.complete && !priorLoss,
3838
- });
3699
+ const preview = createPreAdmissionArtifactSpillPreview(fullText, artifactId, digest);
3839
3700
  const spillMeta = outputMeta()
3840
3701
  .truncationFromText(preview, {
3841
3702
  direction: "middle",
3842
3703
  totalLines: fullText.split("\n").length,
3843
- totalBytes,
3844
- artifactId: saved.id,
3845
- artifactVerified: true,
3846
- sourceCaptureIncomplete,
3847
- ...(maxInlineBytes !== undefined ? { maxBytes: maxInlineBytes } : {}),
3704
+ totalBytes: Buffer.byteLength(fullText, "utf-8"),
3705
+ artifactId,
3848
3706
  })
3849
3707
  .get();
3850
- const spillTruncation = spillMeta?.truncation;
3851
- const artifactTruncatedBytes =
3852
- Math.max(existingTruncation?.artifactTruncatedBytes ?? 0, saved.omittedBytes ?? 0) || undefined;
3853
- const mergedTruncation = spillTruncation
3854
- ? {
3855
- ...existingTruncation,
3856
- ...spillTruncation,
3857
- artifactId: saved.id,
3858
- artifactVerified: true,
3859
- artifactTruncatedBytes,
3860
- sourceTruncatedBytes: existingTruncation?.sourceTruncatedBytes,
3861
- sourceCaptureIncomplete,
3862
- columnDroppedBytes: existingTruncation?.columnDroppedBytes,
3863
- shownRange: sourceCaptureIncomplete ? undefined : spillTruncation.shownRange,
3864
- headRange: sourceCaptureIncomplete ? undefined : spillTruncation.headRange,
3865
- tailRange: sourceCaptureIncomplete ? undefined : spillTruncation.tailRange,
3866
- nextOffset: sourceCaptureIncomplete ? undefined : spillTruncation.nextOffset,
3867
- }
3868
- : existingTruncation;
3869
- message.details = {
3870
- ...detailRecord,
3871
- meta: { ...existingMeta, ...spillMeta, truncation: mergedTruncation },
3872
- };
3708
+ const existingDetails = message.details;
3709
+ const detailRecord =
3710
+ existingDetails && typeof existingDetails === "object" ? (existingDetails as Record<string, unknown>) : {};
3711
+ const existingMeta =
3712
+ detailRecord.meta && typeof detailRecord.meta === "object"
3713
+ ? (detailRecord.meta as Record<string, unknown>)
3714
+ : {};
3715
+ message.details = { ...detailRecord, meta: { ...existingMeta, ...spillMeta } };
3873
3716
  message.content = [
3874
3717
  ...message.content.filter((block): block is ImageContent => block.type === "image"),
3875
3718
  { type: "text", text: preview },
3876
3719
  ];
3877
3720
  } catch (error) {
3878
- const diagnostic = boundAgentBashArtifactSaveDiagnostic(error);
3879
- replaceToolResultWithArtifactFailurePreview(message, fullText, maxInlineBytes, diagnostic);
3880
3721
  logger.warn("Failed to spill oversized tool result before context admission", {
3881
3722
  toolName: message.toolName,
3882
3723
  error: error instanceof Error ? error.message : String(error),
@@ -3902,11 +3743,7 @@ export class AgentSession {
3902
3743
  /** Internal handler for agent events - shared by subscribe and reconnect */
3903
3744
  #handleAgentEvent = async (event: AgentEvent, activePromptHandle?: string): Promise<void> => {
3904
3745
  const attemptScope = (event as AgentEvent & { scope?: AttemptScope }).scope;
3905
- if ((event.type === "agent_start" || event.type === "turn_start") && attemptScope) {
3906
- this.#attemptRecordStore.register(attemptScope);
3907
- this.#attemptRecordStore.establishClean(attemptScope);
3908
- }
3909
- if (this.#extensionRunner?.hasHandlers(event.type)) this.#markRetryReplayUnsafe();
3746
+
3910
3747
  if (
3911
3748
  event.type === "tool_execution_start" ||
3912
3749
  event.type === "tool_execution_update" ||
@@ -4110,6 +3947,12 @@ export class AgentSession {
4110
3947
  }
4111
3948
 
4112
3949
  await this.#emitSessionEvent(displayEvent);
3950
+ if (event.type === "message_end" && event.message.role === "assistant") {
3951
+ this.#assistantAttemptScopes.set(event.message, {
3952
+ scope: attemptScope,
3953
+ wasClean: this.#isRetryScopeClean(attemptScope),
3954
+ });
3955
+ }
4113
3956
  if (
4114
3957
  displayEvent !== event &&
4115
3958
  displayEvent.type === "message_end" &&
@@ -4484,7 +4327,14 @@ export class AgentSession {
4484
4327
  if (this.#isRetryableError(msg)) {
4485
4328
  const transportFailure = (msg as AssistantMessage & { transportFailure?: TransportFailureFacts })
4486
4329
  .transportFailure;
4487
- const didRetry = await this.#handleRetryableError(msg, false, transportFailure);
4330
+ const messageScope = this.#assistantAttemptScopes.get(msg);
4331
+ const didRetry = await this.#handleRetryableError(
4332
+ msg,
4333
+ false,
4334
+ transportFailure,
4335
+ messageScope?.scope ?? event.scope,
4336
+ messageScope?.wasClean ?? false,
4337
+ );
4488
4338
  if (didRetry) return; // Retry was initiated, don't proceed to compaction
4489
4339
  }
4490
4340
  if (this.#retryAttempt > 0) {
@@ -6001,6 +5851,7 @@ export class AgentSession {
6001
5851
  this.#abortActiveMidRunBarriers();
6002
5852
  this.abortCompaction();
6003
5853
  this.agent.abort();
5854
+ this.agent.setMainAttemptScopeObserver(undefined);
6004
5855
  // Disconnect the Agent event bridge NOW — before the maintenance join and the
6005
5856
  // bounded idle / forceAbort below — so no agent_end emitted during teardown
6006
5857
  // (including the one forceAbort emits) can re-enter #handleAgentEvent and start
@@ -14736,6 +14587,8 @@ export class AgentSession {
14736
14587
  outcome.failure.message,
14737
14588
  true,
14738
14589
  outcome.failure.transportFailure,
14590
+ outcome.scope,
14591
+ this.#isRetryScopeClean(outcome.scope),
14739
14592
  ) as Promise<ManagedAttemptDecision>;
14740
14593
  }
14741
14594
 
@@ -14896,6 +14749,8 @@ export class AgentSession {
14896
14749
  message: AssistantMessage,
14897
14750
  managedOutcome = false,
14898
14751
  transportFailure?: TransportFailureFacts,
14752
+ scope?: AttemptScope,
14753
+ scopeWasClean = this.#isRetryScopeClean(scope),
14899
14754
  ): Promise<boolean | ManagedAttemptDecision> {
14900
14755
  const controller = this.#defaultFallbackChain();
14901
14756
  const managedFallback = controller.chain.entries.length > 1;
@@ -14910,12 +14765,16 @@ export class AgentSession {
14910
14765
  if (!managedFallback && !retrySettings.enabled) return false;
14911
14766
  const classification = this.#classifyErrorForRetry(message);
14912
14767
  const firstEventTimeout = classification === "first_event_timeout";
14913
- if (!managedFallback && firstEventTimeout) {
14914
- this.#firstEventTimeoutRetryStartedAt ??= Date.now();
14768
+ if (!managedFallback && firstEventTimeout && this.#retryAttempt === 0) {
14769
+ this.#firstEventTimeoutRetryStartedAt = Date.now();
14915
14770
  }
14916
- // First-event timeouts are replay-safe only before any model progress,
14917
- // abort, or extension/provider lifecycle participation.
14918
- if (firstEventTimeout && (!this.#hasCleanRetryReplaySafety || assistantMessageHasVisibleOrToolContent(message))) {
14771
+ const requiresScopedFirstEventTimeout = managedFallback || !legacyRetryConfigured;
14772
+ if (
14773
+ firstEventTimeout &&
14774
+ (assistantMessageHasVisibleOrToolContent(message) ||
14775
+ (this.#retryAttempt > 0 && !this.#hasCleanRetryReplaySafety) ||
14776
+ (requiresScopedFirstEventTimeout && (!this.#isTypedFirstEventTimeout(message) || !scope || !scopeWasClean)))
14777
+ ) {
14919
14778
  return managedOutcome
14920
14779
  ? {
14921
14780
  type: "terminal",
@@ -14943,14 +14802,12 @@ export class AgentSession {
14943
14802
  // before the failure. This bypasses #hasCleanRetryReplaySafety because the
14944
14803
  // content-free check is the replay-safety guarantee for credential rotation.
14945
14804
  const canReplayRotatedCredential = credentialRotated;
14946
- // Bare defaults admit only clean, side-effect-free canonical stream watchdog failures,
14947
- // content-free quota/rate-limit credential rotations, and the explicit Codex
14948
- // capacity-overload event.
14805
+ // Bare defaults retain their narrow watchdog and Codex admissions. A
14806
+ // first-event timeout adds the typed, content-free, current-clean-scope
14807
+ // requirement above; other transient watchdogs preserve legacy behavior.
14949
14808
  if (!managedFallback && !legacyRetryConfigured && !canReplayRotatedCredential) {
14950
14809
  const bareDefaultCodexOverload = isBareDefaultCodexOverload(message);
14951
14810
  const bareDefaultWrappedFirstEventTimeout = isBareDefaultWrappedFirstEventTimeout(message);
14952
- // Content-free Codex overload is replay-safe by the same reasoning as
14953
- // credential rotation: no partial output means no observable state.
14954
14811
  const canReplayCodexOverload = bareDefaultCodexOverload;
14955
14812
  if (
14956
14813
  (!canReplayCodexOverload &&
@@ -14959,7 +14816,7 @@ export class AgentSession {
14959
14816
  (hasBareDefaultRetryDisqualifyingFacts(message) ||
14960
14817
  (classification !== "transient" && classification !== "first_event_timeout") ||
14961
14818
  !BARE_DEFAULT_WATCHDOG_ERROR.test(message.errorMessage ?? ""))) ||
14962
- (!canReplayCodexOverload && !this.#hasCleanRetryReplaySafety)
14819
+ (!canReplayCodexOverload && !firstEventTimeout && !this.#hasCleanRetryReplaySafety)
14963
14820
  ) {
14964
14821
  return false;
14965
14822
  }