@gajae-code/coding-agent 0.18.5 → 0.18.7

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 +140 -0
  2. package/dist/types/capability/types.d.ts +4 -0
  3. package/dist/types/cli-commands.d.ts +10 -0
  4. package/dist/types/cli-ordinary.d.ts +13 -0
  5. package/dist/types/cli.d.ts +12 -3
  6. package/dist/types/commands/contribution-prep.d.ts +0 -3
  7. package/dist/types/commands/sdk.d.ts +3 -0
  8. package/dist/types/commands/update.d.ts +1 -0
  9. package/dist/types/config/model-profile-activation.d.ts +19 -0
  10. package/dist/types/config/settings.d.ts +4 -0
  11. package/dist/types/coordinator-mcp/session-reaper.d.ts +5 -0
  12. package/dist/types/discovery/helpers.d.ts +2 -2
  13. package/dist/types/edit/modes/replace.d.ts +3 -3
  14. package/dist/types/edit/streaming.d.ts +2 -2
  15. package/dist/types/exec/bash-shell-supervisor.d.ts +12 -0
  16. package/dist/types/runtime-mcp/elicitation.d.ts +12 -0
  17. package/dist/types/sdk/broker/discovery.d.ts +8 -0
  18. package/dist/types/sdk/broker/ensure.d.ts +1 -0
  19. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +4 -0
  20. package/dist/types/sdk/broker/lifecycle.d.ts +9 -0
  21. package/dist/types/sdk/broker/runtime.d.ts +1 -1
  22. package/dist/types/sdk/broker/task-artifact-owner-validation.d.ts +55 -0
  23. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  24. package/dist/types/sdk/bus/telegram-daemon.d.ts +4 -2
  25. package/dist/types/sdk/cli/diagnostics-cli.d.ts +18 -0
  26. package/dist/types/sdk/diagnostics/observe-broker.d.ts +185 -0
  27. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  28. package/dist/types/sdk/host/host.d.ts +6 -0
  29. package/dist/types/sdk/index.d.ts +1 -0
  30. package/dist/types/sdk/router/session-router.d.ts +9 -0
  31. package/dist/types/sdk/session.d.ts +13 -0
  32. package/dist/types/session/agent-session.d.ts +5 -1
  33. package/dist/types/session/internal/managed-session-storage.d.ts +11 -1
  34. package/dist/types/session/internal/task-artifact-owner-access.d.ts +17 -0
  35. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +3 -0
  36. package/dist/types/session/session-retirement.d.ts +47 -0
  37. package/dist/types/session/session-storage.d.ts +0 -46
  38. package/dist/types/session/session-transcript-header.d.ts +2 -0
  39. package/dist/types/session/task-artifact-owner-codec.d.ts +107 -0
  40. package/dist/types/session/task-artifact-owner-retirement.d.ts +7 -0
  41. package/dist/types/session/task-artifact-owner.d.ts +10 -0
  42. package/dist/types/tools/ask-contract.d.ts +75 -254
  43. package/dist/types/tools/bash.d.ts +1 -1
  44. package/package.json +7 -7
  45. package/scripts/build-sdk-package-smoke.ts +5 -0
  46. package/scripts/resident-memory-bench.ts +174 -20
  47. package/src/capability/index.ts +1 -0
  48. package/src/capability/types.ts +4 -0
  49. package/src/cli/config-cli.ts +8 -18
  50. package/src/cli/public-command-entry.ts +5 -0
  51. package/src/cli/ssh-cli.ts +6 -3
  52. package/src/cli/web-search-cli.ts +9 -3
  53. package/src/cli-commands.ts +14 -0
  54. package/src/cli-main.ts +7 -8
  55. package/src/cli-ordinary.ts +197 -0
  56. package/src/cli.ts +25 -182
  57. package/src/commands/contribution-prep.ts +1 -3
  58. package/src/commands/sdk.ts +171 -21
  59. package/src/commands/update.ts +15 -1
  60. package/src/config/file-lock.ts +91 -2
  61. package/src/config/model-profile-activation.ts +70 -4
  62. package/src/config/model-profile-contract.ts +4 -1
  63. package/src/config/model-profiles.ts +23 -31
  64. package/src/config/model-resolver.ts +9 -1
  65. package/src/config/settings.ts +44 -4
  66. package/src/coordinator-mcp/event-webhook.ts +19 -14
  67. package/src/coordinator-mcp/policy.ts +14 -4
  68. package/src/coordinator-mcp/server.ts +142 -7
  69. package/src/coordinator-mcp/session-reaper.ts +9 -1
  70. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  71. package/src/discovery/helpers.ts +9 -3
  72. package/src/discovery/ssh.ts +8 -3
  73. package/src/edit/modes/replace.ts +6 -6
  74. package/src/edit/streaming.ts +15 -15
  75. package/src/exec/bash-shell-supervisor.ts +60 -3
  76. package/src/gjc-runtime/gc-runtime.ts +2 -1
  77. package/src/hindsight/config.ts +9 -3
  78. package/src/internal-urls/docs-index.generated.ts +11 -10
  79. package/src/memories/index.ts +8 -2
  80. package/src/modes/acp/acp-agent.ts +724 -370
  81. package/src/modes/components/tool-execution.ts +1 -1
  82. package/src/prompts/tools/ask.md +2 -0
  83. package/src/runtime-mcp/client.ts +13 -4
  84. package/src/runtime-mcp/elicitation.ts +142 -0
  85. package/src/sdk/broker/broker.ts +54 -1
  86. package/src/sdk/broker/daemon-entry.ts +67 -6
  87. package/src/sdk/broker/discovery.ts +8 -0
  88. package/src/sdk/broker/ensure.ts +156 -20
  89. package/src/sdk/broker/lifecycle-ledger.ts +110 -12
  90. package/src/sdk/broker/lifecycle.ts +334 -348
  91. package/src/sdk/broker/runtime.ts +5 -1
  92. package/src/sdk/broker/task-artifact-owner-validation.ts +259 -0
  93. package/src/sdk/broker/transport.ts +5 -1
  94. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  95. package/src/sdk/bus/chat-daemon-runtime.ts +3 -0
  96. package/src/sdk/bus/index.ts +3 -3
  97. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  98. package/src/sdk/bus/telegram-daemon-control.ts +15 -12
  99. package/src/sdk/bus/telegram-daemon.ts +38 -3
  100. package/src/sdk/cli/diagnostics-cli.ts +247 -0
  101. package/src/sdk/diagnostics/observe-broker.ts +654 -0
  102. package/src/sdk/host/control/dispatch.ts +52 -4
  103. package/src/sdk/host/control/operations.ts +1 -1
  104. package/src/sdk/host/host.ts +6 -0
  105. package/src/sdk/host/session-runtime.test.ts +83 -4
  106. package/src/sdk/host/session-runtime.ts +15 -7
  107. package/src/sdk/index.ts +1 -0
  108. package/src/sdk/lifecycle-session.ts +1 -0
  109. package/src/sdk/mcp/server.ts +15 -3
  110. package/src/sdk/protocol/operation-registry.ts +1 -1
  111. package/src/sdk/router/session-router.ts +26 -3
  112. package/src/sdk/session.ts +51 -8
  113. package/src/session/agent-session.ts +169 -43
  114. package/src/session/blob-store.ts +78 -33
  115. package/src/session/internal/managed-session-storage.ts +203 -4
  116. package/src/session/internal/task-artifact-owner-access.ts +229 -0
  117. package/src/session/internal/task-artifact-owner-transcript.ts +68 -0
  118. package/src/session/session-retirement.ts +155 -0
  119. package/src/session/session-storage.ts +1 -160
  120. package/src/session/session-transcript-header.ts +17 -0
  121. package/src/session/task-artifact-owner-codec.ts +578 -0
  122. package/src/session/task-artifact-owner-retirement.ts +475 -0
  123. package/src/session/task-artifact-owner.ts +289 -0
  124. package/src/setup/hermes/templates/operator-instructions.v1.md +19 -1
  125. package/src/setup/hermes-setup.ts +4 -1
  126. package/src/slash-commands/builtin-registry.ts +8 -5
  127. package/src/tools/ask-contract.test.ts +76 -1
  128. package/src/tools/ask-contract.ts +57 -6
  129. package/src/tools/ask.ts +16 -7
  130. package/src/tools/bash.ts +38 -29
  131. package/src/tools/descriptors.test.ts +29 -0
  132. package/src/tools/tool-catalog.generated.ts +7 -4
  133. package/src/web/search/index.ts +2 -2
@@ -127,12 +127,14 @@ import {
127
127
  streamSimple,
128
128
  } from "@gajae-code/ai/core";
129
129
  import { normalizeAnthropicBaseUrl } from "@gajae-code/ai/providers/anthropic";
130
+ import { isExplicitCodexTerminalVeto } from "@gajae-code/ai/providers/openai-codex-responses";
130
131
  import {
131
132
  type AuthDisposition,
132
133
  beginAttempt,
133
134
  classifyFallbackTrigger,
134
135
  EMPTY_RESPONSE_PROVIDER_CODE,
135
136
  type FallbackAttemptToken,
137
+ PROVIDER_PROTOCOL_MISMATCH_ERROR_CODE,
136
138
  type FallbackTriggerClass,
137
139
  SERVER_OVERLOADED_PROVIDER_CODE,
138
140
  STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE,
@@ -1210,7 +1212,10 @@ export interface TrackedSendUserMessageOptions extends SendUserMessageOptions {
1210
1212
  trackSubmission: true;
1211
1213
  }
1212
1214
 
1213
- type SendUserMessageDispatchOptions = SendUserMessageOptions & { trackSubmission?: boolean };
1215
+ type SendUserMessageDispatchOptions = SendUserMessageOptions & {
1216
+ trackSubmission?: boolean;
1217
+ expectedSdkRunToken?: string;
1218
+ };
1214
1219
 
1215
1220
  type InternalPromptOptions = PromptOptions & { sdkRunToken?: string };
1216
1221
  type InternalCustomMessageOptions = Pick<
@@ -1419,8 +1424,13 @@ const BARE_DEFAULT_WATCHDOG_ERROR =
1419
1424
  const PROVIDER_FIRST_EVENT_TIMEOUT_ERROR = "Provider stream timed out while waiting for the first event";
1420
1425
  const WRAPPED_PROVIDER_FIRST_EVENT_TIMEOUT_ERROR = `Error: ${PROVIDER_FIRST_EVENT_TIMEOUT_ERROR}`;
1421
1426
  const PROVIDER_FIRST_EVENT_TIMEOUT_WITHOUT_ARTICLE_ERROR = "Provider stream timed out while waiting for first event";
1422
- const BARE_DEFAULT_CODEX_OVERLOAD_ERROR = /^Codex error event(?:: .*)? \(code=server_is_overloaded(?:, [^)]+)*\)$/;
1423
- /** Anthropic's typed capacity-overload `error.type`, the only overload code admitted below. */
1427
+ const BARE_DEFAULT_CODEX_RETRYABLE_ERROR =
1428
+ /^Codex error event(?:: .*)? \(code=(?:server_is_overloaded|server_error|internal_error)(?:, [^)]+)*\)$/;
1429
+ const BARE_DEFAULT_CODEX_RETRYABLE_CODES = new Set(["server_is_overloaded", "server_error", "internal_error"]);
1430
+ const BARE_DEFAULT_CODEX_OVERLOAD_ERROR =
1431
+ /^Codex error event(?:: .*)? \(code=server_is_overloaded(?:, [^)]+)*\)$/;
1432
+ const BARE_DEFAULT_OVERLOAD_CODES = new Set([SERVER_OVERLOADED_PROVIDER_CODE]);
1433
+ /** Anthropic's typed capacity-overload `error.type`, the only Anthropic overload code admitted below. */
1424
1434
  const ANTHROPIC_OVERLOADED_ERROR_TYPE = "overloaded_error";
1425
1435
  const KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES = {
1426
1436
  "anthropic-messages": new Set([
@@ -1501,8 +1511,8 @@ function isBareDefaultMessageOnlyFirstEventTimeout(message: AssistantMessage): b
1501
1511
  );
1502
1512
  }
1503
1513
  /**
1504
- * True when the attempt's structured evidence is exactly the provider's typed
1505
- * capacity-overload code and nothing else: no status, no retained retry header,
1514
+ * True when the attempt's structured evidence is exactly an admitted provider
1515
+ * code and nothing else: no status, no retained retry header,
1506
1516
  * no second typed code, no error kind, and none of the transport's other
1507
1517
  * observations (request size, first-event timing, endpoint class, provider retry
1508
1518
  * ceiling). The code arrives statuslessly (a Codex error event, an HTTP 200
@@ -1511,15 +1521,18 @@ function isBareDefaultMessageOnlyFirstEventTimeout(message: AssistantMessage): b
1511
1521
  * attempt is no longer provably that bare capacity rejection. The code is
1512
1522
  * compared case-sensitively, matching the parser and the transport gate.
1513
1523
  */
1514
- function isExactTypedOverloadFacts(message: AssistantMessage): boolean {
1524
+ function isExactTypedOverloadFacts(
1525
+ message: AssistantMessage,
1526
+ acceptedProviderCodes: ReadonlySet<string> = BARE_DEFAULT_OVERLOAD_CODES,
1527
+ ): boolean {
1515
1528
  if (message.errorKind !== undefined || message.errorStatus !== undefined) return false;
1516
1529
  const facts = message.transportFailure;
1517
1530
  if (!facts) return false;
1518
1531
  return (
1519
- facts.providerCode === SERVER_OVERLOADED_PROVIDER_CODE &&
1532
+ acceptedProviderCodes.has(facts.providerCode ?? "") &&
1520
1533
  facts.status === undefined &&
1521
1534
  facts.anthropicErrorType === undefined &&
1522
- (facts.openaiErrorCode === undefined || facts.openaiErrorCode === SERVER_OVERLOADED_PROVIDER_CODE) &&
1535
+ (facts.openaiErrorCode === undefined || facts.openaiErrorCode === facts.providerCode) &&
1523
1536
  (facts.headers === undefined || Object.keys(facts.headers).length === 0) &&
1524
1537
  facts.requestBytes === undefined &&
1525
1538
  facts.firstEventElapsedMs === undefined &&
@@ -1545,11 +1558,41 @@ function isStatuslessTypedOverloadFacts(facts: TransportFailureFacts | undefined
1545
1558
  );
1546
1559
  }
1547
1560
 
1561
+ // Deterministic auth/request/model diagnostics must surface even when a provider
1562
+ // labels the failure with a transient code.
1563
+ const TERMINAL_ERROR_MESSAGE =
1564
+ /unauthorized|forbidden|authentication_error|permission_error|permission denied|invalid api key|invalid_request_error|invalid request|bad request|bad_request|validation_error|unprocessable|payload too large|payment required|insufficient_quota|insufficient credits|missing required (parameter|field)|invalid schema|invalid tool_choice|unsupported (parameter|value|model)|model_not_found|no such model|unknown model|does not (exist|support)|request was aborted|request aborted|the user aborted/i;
1565
+
1566
+ function isTerminalCodexProviderFailure(message: AssistantMessage): boolean {
1567
+ if (message.api !== "openai-codex-responses") return false;
1568
+ if (isExplicitCodexTerminalVeto(message.transportFailure?.providerCode, message.errorMessage)) return true;
1569
+ return (
1570
+ (BARE_DEFAULT_CODEX_RETRYABLE_CODES.has(message.transportFailure?.providerCode ?? "") ||
1571
+ BARE_DEFAULT_CODEX_RETRYABLE_ERROR.test(message.errorMessage ?? "")) &&
1572
+ TERMINAL_ERROR_MESSAGE.test(message.errorMessage ?? "")
1573
+ );
1574
+ }
1575
+
1548
1576
  function isBareDefaultCodexOverload(message: AssistantMessage): boolean {
1549
1577
  return (
1550
1578
  message.api === "openai-codex-responses" &&
1551
- BARE_DEFAULT_CODEX_OVERLOAD_ERROR.test(message.errorMessage ?? "") &&
1552
- (isExactTypedOverloadFacts(message) || !hasBareDefaultRetryDisqualifyingFacts(message)) &&
1579
+ !TERMINAL_ERROR_MESSAGE.test(message.errorMessage ?? "") &&
1580
+ !isExplicitCodexTerminalVeto(undefined, message.errorMessage) &&
1581
+ (isExactTypedOverloadFacts(message, BARE_DEFAULT_OVERLOAD_CODES) ||
1582
+ (BARE_DEFAULT_CODEX_OVERLOAD_ERROR.test(message.errorMessage ?? "") &&
1583
+ !hasBareDefaultRetryDisqualifyingFacts(message))) &&
1584
+ !assistantMessageHasVisibleOrToolContent(message)
1585
+ );
1586
+ }
1587
+
1588
+ function isBareDefaultCodexProviderFailure(message: AssistantMessage): boolean {
1589
+ return (
1590
+ message.api === "openai-codex-responses" &&
1591
+ !TERMINAL_ERROR_MESSAGE.test(message.errorMessage ?? "") &&
1592
+ !isExplicitCodexTerminalVeto(message.transportFailure?.providerCode, message.errorMessage) &&
1593
+ (isExactTypedOverloadFacts(message, BARE_DEFAULT_CODEX_RETRYABLE_CODES) ||
1594
+ (BARE_DEFAULT_CODEX_RETRYABLE_ERROR.test(message.errorMessage ?? "") &&
1595
+ !hasBareDefaultRetryDisqualifyingFacts(message))) &&
1553
1596
  !assistantMessageHasVisibleOrToolContent(message)
1554
1597
  );
1555
1598
  }
@@ -1609,7 +1652,10 @@ function isBareDefaultAnthropicOverload(message: AssistantMessage): boolean {
1609
1652
  function assistantMessageHasVisibleOrToolContent(message: AssistantMessage): boolean {
1610
1653
  return message.content.some(content => {
1611
1654
  if (content.type === "text") return content.text.length > 0;
1612
- return content.type === "thinking" || content.type === "redactedThinking" || content.type === "toolCall";
1655
+ if (content.type === "thinking") {
1656
+ return content.thinking.length > 0 || (content.thinkingSignature?.length ?? 0) > 0;
1657
+ }
1658
+ return content.type === "redactedThinking" || content.type === "toolCall";
1613
1659
  });
1614
1660
  }
1615
1661
 
@@ -5077,6 +5123,7 @@ export class AgentSession {
5077
5123
  if (lease) this.#deferredAgentEndLeases.delete(pending);
5078
5124
  this.#startAgentEndPublication(pending, lease);
5079
5125
  }
5126
+
5080
5127
  #startAgentEndPublication(pending: AgentSessionEvent, lease?: RunResourceProducerLease): void {
5081
5128
  this.#agentEndPublicationInFlight++;
5082
5129
  const pendingScope = (pending as AgentSessionEvent & { scope?: AttemptScopeRef }).scope as
@@ -6166,7 +6213,10 @@ export class AgentSession {
6166
6213
  path.basename(sessionModeStatePath(this.sessionManager.getCwd(), currentSessionId, active.skill)),
6167
6214
  );
6168
6215
  const raw = fs.readFileSync(filePath, "utf-8");
6169
- const parsed = JSON.parse(raw) as { state?: { intent_contract?: unknown } };
6216
+ const parsed = JSON.parse(raw) as { current_phase?: unknown; state?: { intent_contract?: unknown } };
6217
+ // A persisted final spec moves to handoff. Its next-workflow/approval
6218
+ // asks are not interview rounds and must not require recorder metadata.
6219
+ if (parsed.current_phase !== "interviewing") return undefined;
6170
6220
  const intentContract = parsed.state?.intent_contract;
6171
6221
  if (intentContract === undefined) return "topology";
6172
6222
  assertDeepInterviewIntentManifest(intentContract);
@@ -6318,7 +6368,10 @@ export class AgentSession {
6318
6368
  },
6319
6369
  };
6320
6370
  }
6321
- const transformedMessages = await this.#transformContext([...this.messages], options.signal);
6371
+ const transformedMessages = await this.#transformContext(
6372
+ this.#withoutEphemeralCustomMessages(this.messages),
6373
+ options.signal,
6374
+ );
6322
6375
  const convertedMessages = await this.#convertToLlm(transformedMessages);
6323
6376
  const providerMessages = this.model
6324
6377
  ? normalizeMessagesForProvider(convertedMessages, this.model)
@@ -12297,9 +12350,16 @@ export class AgentSession {
12297
12350
  return this.agent.activeResourceRunId;
12298
12351
  }
12299
12352
 
12300
- /** All messages including custom types like BashExecutionMessage */
12353
+ /**
12354
+ * All messages including custom types like BashExecutionMessage. Request-scoped
12355
+ * ephemeral copies are retained only in `agent.state.messages` to keep the provider
12356
+ * cache prefix stable; they are never part of this public view.
12357
+ */
12301
12358
  get messages(): AgentMessage[] {
12302
- return this.agent.state.messages;
12359
+ const messages = this.agent.state.messages;
12360
+ return messages.some(message => message.role === "custom" && this.#isEphemeralCustomMessageType(message.customType))
12361
+ ? this.#withoutEphemeralCustomMessages(messages)
12362
+ : messages;
12303
12363
  }
12304
12364
  get transcriptPromptGeneration(): number {
12305
12365
  return this.#promptGeneration;
@@ -12389,9 +12449,16 @@ export class AgentSession {
12389
12449
  let continuationSdkRunToken: string | undefined;
12390
12450
  try {
12391
12451
  const volatileProjectContextMessage = await this.#buildVolatileProjectContextMessage();
12392
- this.agent.appendMessage(volatileProjectContextMessage);
12452
+ if (!this.#isLatestRetainedEphemeralCopy(volatileProjectContextMessage)) {
12453
+ this.agent.appendMessage(volatileProjectContextMessage);
12454
+ }
12393
12455
  const untrustedMcpServerInstructionsMessage = this.#buildUntrustedMcpServerInstructionsMessage();
12394
- if (untrustedMcpServerInstructionsMessage) this.agent.appendMessage(untrustedMcpServerInstructionsMessage);
12456
+ if (
12457
+ untrustedMcpServerInstructionsMessage &&
12458
+ !this.#isLatestRetainedEphemeralCopy(untrustedMcpServerInstructionsMessage)
12459
+ ) {
12460
+ this.agent.appendMessage(untrustedMcpServerInstructionsMessage);
12461
+ }
12395
12462
  const hindsightState = this.getHindsightSessionState();
12396
12463
  await hindsightState?.maybeRecallOnAgentStart();
12397
12464
  hindsightRecall = hindsightState?.getRecallSnippetForInjection();
@@ -12429,7 +12496,6 @@ export class AgentSession {
12429
12496
  });
12430
12497
  if (continuationSdkRunToken === undefined) await this.#waitForPostPromptRecovery();
12431
12498
  } finally {
12432
- this.#removeEphemeralCustomMessages();
12433
12499
  await this.#settleEndedInFlight(
12434
12500
  inFlightPrompt,
12435
12501
  continuationSdkRunToken === undefined ? "full" : "publication",
@@ -13293,6 +13359,21 @@ export class AgentSession {
13293
13359
  );
13294
13360
  }
13295
13361
 
13362
+ /**
13363
+ * Sent ephemeral copies stay in live state so the provider prefix is stable. A new
13364
+ * copy is only worth appending when its provider-visible content changed; an
13365
+ * identical one would just repeat tokens that still count against the window.
13366
+ */
13367
+ #isLatestRetainedEphemeralCopy(message: CustomMessage): boolean {
13368
+ const messages = this.agent.state.messages;
13369
+ for (let index = messages.length - 1; index >= 0; index--) {
13370
+ const candidate = messages[index];
13371
+ if (candidate?.role !== "custom" || candidate.customType !== message.customType) continue;
13372
+ return JSON.stringify(candidate.content) === JSON.stringify(message.content);
13373
+ }
13374
+ return false;
13375
+ }
13376
+
13296
13377
  #removeEphemeralCustomMessages(): void {
13297
13378
  const messages = this.agent.state.messages;
13298
13379
  const withoutEphemeralMessages = this.#withoutEphemeralCustomMessages(messages);
@@ -13988,8 +14069,6 @@ export class AgentSession {
13988
14069
  throw new Error(formatNoCredentialOnboardingError(this.model.provider));
13989
14070
  }
13990
14071
 
13991
- this.#removeEphemeralCustomMessages();
13992
-
13993
14072
  // Check if we need to compact before sending (catches aborted responses)
13994
14073
  const lastAssistant = this.#findLastAssistantMessage();
13995
14074
  if (lastAssistant && !options?.skipCompactionCheck) {
@@ -14164,9 +14243,16 @@ export class AgentSession {
14164
14243
  messages.push(goalModeMessage);
14165
14244
  }
14166
14245
  const volatileProjectContextMessage = await this.#buildVolatileProjectContextMessage();
14167
- messages.push(volatileProjectContextMessage);
14246
+ if (!this.#isLatestRetainedEphemeralCopy(volatileProjectContextMessage)) {
14247
+ messages.push(volatileProjectContextMessage);
14248
+ }
14168
14249
  const untrustedMcpServerInstructionsMessage = this.#buildUntrustedMcpServerInstructionsMessage();
14169
- if (untrustedMcpServerInstructionsMessage) messages.push(untrustedMcpServerInstructionsMessage);
14250
+ if (
14251
+ untrustedMcpServerInstructionsMessage &&
14252
+ !this.#isLatestRetainedEphemeralCopy(untrustedMcpServerInstructionsMessage)
14253
+ ) {
14254
+ messages.push(untrustedMcpServerInstructionsMessage);
14255
+ }
14170
14256
 
14171
14257
  // Roster: one Phase A claim, revalidated and reused on every attempt;
14172
14258
  // a superseded claim releases and the prompt proceeds without it.
@@ -14328,7 +14414,6 @@ export class AgentSession {
14328
14414
  return;
14329
14415
  throw error;
14330
14416
  } finally {
14331
- this.#removeEphemeralCustomMessages();
14332
14417
  if (rosterClaim) {
14333
14418
  this.agent.replaceMessages(
14334
14419
  this.agent.state.messages.filter(
@@ -14606,6 +14691,7 @@ export class AgentSession {
14606
14691
  onQueuedAfterAdmission?: (message: AgentMessage, cancelQueued: () => boolean) => void;
14607
14692
  external?: boolean;
14608
14693
  sdkRunToken?: string;
14694
+ expectedSdkRunToken?: string;
14609
14695
  forceOneAtATime?: boolean;
14610
14696
  allowDuringSessionTransition?: boolean;
14611
14697
  allowCancelAndSubmit?: boolean;
@@ -14622,11 +14708,18 @@ export class AgentSession {
14622
14708
  const message = { role: "user" as const, content, attribution: "user" as const, timestamp: Date.now() };
14623
14709
  // Enqueue-time admission: the Agent only accepts a steer into a live,
14624
14710
  // non-aborted run. Anything else (idle, unwinding after agent_end, another
14625
- // prompt still in preflight, aborting) has no run to steer, so it is routed
14626
- // as a sequential follow-up owned by the next turn instead of being parked
14627
- // in a queue nobody owns.
14711
+ // prompt still in preflight, aborting) has no run to steer. Ordinary steers
14712
+ // become a sequential follow-up owned by the next turn; SDK turn-bound
14713
+ // steers fail closed rather than being delivered to a successor run.
14714
+ if (
14715
+ options?.expectedSdkRunToken !== undefined &&
14716
+ this.#activeSdkRunToken !== options.expectedSdkRunToken
14717
+ )
14718
+ throw Object.assign(new Error("The expected SDK run is not active."), { code: "turn_not_active" });
14628
14719
  const admission = this.agent.steer(message, options?.forceOneAtATime ? { forceOneAtATime: true } : undefined);
14629
14720
  if (!admission.admitted) {
14721
+ if (options?.expectedSdkRunToken !== undefined)
14722
+ throw Object.assign(new Error("The expected SDK run is not active."), { code: "turn_not_active" });
14630
14723
  const queuedFollowUp = await this.#queueFollowUpAfterReservation(message, displayText, {
14631
14724
  forceOneAtATime: options?.forceOneAtATime,
14632
14725
  claimsGenuineUserIntent: options?.claimsGenuineUserIntent,
@@ -15779,6 +15872,7 @@ export class AgentSession {
15779
15872
  onPromoted: onQueuedPromoted,
15780
15873
  external: true,
15781
15874
  sdkRunToken: internalOptions?.sdkRunToken,
15875
+ expectedSdkRunToken: internalOptions?.expectedSdkRunToken,
15782
15876
  forceOneAtATime: options?.queuePolicy === "sequential",
15783
15877
  onQueuedAfterAdmission: onTrackedQueued,
15784
15878
  allowDuringSessionTransition: this.#hasCommittedSuccessorTrackedAdmission(options),
@@ -16145,6 +16239,9 @@ export class AgentSession {
16145
16239
  this.#cachedWorkspaceTree = undefined;
16146
16240
  this.#cachedWorkspaceTreeAt = 0;
16147
16241
  this.#pendingWorkspaceTreeRescope = true;
16242
+ // Sent copies name the abandoned root; the rescope already rebuilds the stable
16243
+ // prompt, so there is no cache prefix left to preserve by keeping them.
16244
+ this.#removeEphemeralCustomMessages();
16148
16245
  }
16149
16246
 
16150
16247
  /** Skill loading warnings captured by SDK */
@@ -20282,7 +20379,7 @@ export class AgentSession {
20282
20379
  }
20283
20380
 
20284
20381
  const handoffText = await generateHandoff(
20285
- this.agent.state.messages,
20382
+ this.#withoutEphemeralCustomMessages(this.agent.state.messages),
20286
20383
  model,
20287
20384
  apiKey,
20288
20385
  {
@@ -20516,7 +20613,7 @@ export class AgentSession {
20516
20613
  sessionId: this.sessionId,
20517
20614
  cwd: this.sessionManager.getCwd(),
20518
20615
  sessionFile: this.sessionFile,
20519
- messages: this.agent.state.messages,
20616
+ messages: this.#withoutEphemeralCustomMessages(this.agent.state.messages),
20520
20617
  customInstructions: options.customInstructions,
20521
20618
  },
20522
20619
  options,
@@ -22699,6 +22796,7 @@ export class AgentSession {
22699
22796
  willRetry && !continuationSkipReason
22700
22797
  ? await this.#scheduleOverflowRetryContinuation(generation, options?.resourceRunId)
22701
22798
  : false;
22799
+
22702
22800
  await this.#emitSessionEvent({
22703
22801
  type: "auto_compaction_end",
22704
22802
  action,
@@ -22707,6 +22805,7 @@ export class AgentSession {
22707
22805
  willRetry: overflowContinuationScheduled,
22708
22806
  continuationSkipReason,
22709
22807
  });
22808
+
22710
22809
  if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
22711
22810
 
22712
22811
  if (willRetry) {
@@ -22930,6 +23029,7 @@ export class AgentSession {
22930
23029
  }
22931
23030
 
22932
23031
  #isRetryableError(message: AssistantMessage): boolean {
23032
+ if (isTerminalCodexProviderFailure(message)) return false;
22933
23033
  if (this.#isTerminalProviderFirstEventTimeout(message)) return false;
22934
23034
  if (message.errorMessage?.startsWith("Model fallback chain exhausted;")) return false;
22935
23035
  if (message.errorMessage?.startsWith("Managed fallback retried the escaped non-ASCII")) return false;
@@ -22940,6 +23040,11 @@ export class AgentSession {
22940
23040
  return false;
22941
23041
  const contextWindow = this.model?.contextWindow ?? 0;
22942
23042
  if (classifyContextOverflow(message, transportFailure, contextWindow)) return false;
23043
+ if (
23044
+ (isBareDefaultCodexOverload(message) || isBareDefaultCodexProviderFailure(message)) &&
23045
+ this.settings.get("retry.enabled") !== false
23046
+ )
23047
+ return true;
22943
23048
  const managedFallback = this.#defaultFallbackChain().chain.entries.length > 1;
22944
23049
  // An account-specific model rejection that cannot rotate to another
22945
23050
  // credential stays terminal only on the session's own retry path; managed
@@ -23052,9 +23157,7 @@ export class AgentSession {
23052
23157
  // Errors that will never succeed on retry (auth/permission, malformed
23053
23158
  // request, unknown/unsupported model). These surface immediately rather
23054
23159
  // than retry forever.
23055
- return /unauthorized|forbidden|authentication_error|permission_error|permission denied|invalid api key|invalid_request_error|invalid request|bad request|bad_request|validation_error|unprocessable|payload too large|payment required|insufficient_quota|insufficient credits|missing required (parameter|field)|invalid schema|invalid tool_choice|unsupported (parameter|value|model)|model_not_found|no such model|unknown model|does not (exist|support)|request was aborted|request aborted|the user aborted/i.test(
23056
- errorMessage,
23057
- );
23160
+ return TERMINAL_ERROR_MESSAGE.test(errorMessage);
23058
23161
  }
23059
23162
 
23060
23163
  #extractExplicitHttpStatusFromErrorMessage(errorMessage: string): number | undefined {
@@ -23082,6 +23185,9 @@ export class AgentSession {
23082
23185
  // Without this the message carries no transport facts and would fall
23083
23186
  // through to "unknown", which is admitted for bounded retry (#5627).
23084
23187
  if (message.errorCode === REPETITION_GUARD_ERROR_CODE) return "terminal";
23188
+ // A 2xx body in the wrong protocol replays identically; its embedded body text
23189
+ // (e.g. "HTTP 503") must not be reparsed as a transient failure.
23190
+ if (message.errorCode === PROVIDER_PROTOCOL_MISMATCH_ERROR_CODE) return "terminal";
23085
23191
  if (message.errorKind === "local_snapshot_failure") return "local_snapshot";
23086
23192
  if (message.errorKind === "local_buffer_overflow") return "local_buffer_overflow";
23087
23193
  if (this.#isTypedFirstEventTimeout(message)) return "first_event_timeout";
@@ -24240,6 +24346,13 @@ export class AgentSession {
24240
24346
  const classification = this.#classifyErrorForRetry(message);
24241
24347
  const localSnapshot = classification === "local_snapshot";
24242
24348
  const localBufferOverflow = classification === "local_buffer_overflow";
24349
+ // Managed outcomes can reach retry handling without the ordinary admission
24350
+ // check. Do not let retained transport facts bypass the same terminal veto.
24351
+ if (isTerminalCodexProviderFailure(message)) {
24352
+ return managedOutcome
24353
+ ? { type: "terminal", terminal: { stopReason: "error", messages: [message] } }
24354
+ : false;
24355
+ }
24243
24356
  if (retryCancelled()) {
24244
24357
  return managedOutcome ? { type: "terminal", terminal: { stopReason: "cancelled" } } : false;
24245
24358
  }
@@ -24301,6 +24414,9 @@ export class AgentSession {
24301
24414
  isBareDefaultCodexOverload(message) ||
24302
24415
  isBareDefaultAnthropicOverload(message) ||
24303
24416
  isBareDefaultOpenAIResponsesOverload(message);
24417
+ const canReplayBareDefaultCodexFailure = isBareDefaultCodexProviderFailure(message);
24418
+ const boundBareDefaultCodexFailure = canReplayBareDefaultCodexFailure && !legacyRetryConfigured;
24419
+ const canReplayCodexProviderOverload = isBareDefaultCodexOverload(message);
24304
24420
  const reportedRetryMaxAttempts = transportFailure?.retryMaxAttempts;
24305
24421
  if (reportedRetryMaxAttempts !== undefined) {
24306
24422
  this.#providerRetryMaxAttempts = Math.min(
@@ -24340,19 +24456,26 @@ export class AgentSession {
24340
24456
  }
24341
24457
  : false;
24342
24458
  }
24459
+ const fallbackTrigger = this.#fallbackTriggerFor(message, !managedFallback, transportFailure);
24343
24460
  const trigger:
24344
24461
  | { class: FallbackTriggerClass; retryAfterMs?: number; authDisposition?: AuthDisposition }
24345
- | undefined = this.#fallbackTriggerFor(message, !managedFallback, transportFailure);
24462
+ | undefined = canReplayCodexProviderOverload || canReplayBareDefaultCodexFailure
24463
+ ? { class: "server" }
24464
+ : fallbackTrigger;
24346
24465
  // OpenAI's typed statusless capacity-overload code (issue #5018) must not
24347
24466
  // gain managed-chain retry/advance authority from its new facts. Before
24348
24467
  // the code survived transport, this failure reached the session as an
24349
24468
  // ordinary committed error and surfaced immediately, so mirror that
24350
- // behavior with the existing exhaustion decision.
24469
+ // behavior with the existing exhaustion decision. However, when called from
24470
+ // the agent_end path (managedOutcome=false), we must return false to allow
24471
+ // proper session termination handling.
24351
24472
  if (managedFallback && isStatuslessTypedOverloadFacts(transportFailure)) {
24352
- return this.#managedFallbackExhaustionDecision(
24353
- message,
24354
- message.errorMessage || "Model fallback attempt failed",
24355
- );
24473
+ return managedOutcome
24474
+ ? this.#managedFallbackExhaustionDecision(
24475
+ message,
24476
+ message.errorMessage || "Model fallback attempt failed",
24477
+ )
24478
+ : false;
24356
24479
  }
24357
24480
  if (!trigger) {
24358
24481
  return managedOutcome
@@ -24442,6 +24565,7 @@ export class AgentSession {
24442
24565
  if (!managedFallback && !legacyRetryConfigured && !canReplayRotatedCredential && !canReplayEmptyResponse) {
24443
24566
  if (
24444
24567
  (!canReplayProviderOverload &&
24568
+ !canReplayBareDefaultCodexFailure &&
24445
24569
  !canReplayUnexpectedSocketClose &&
24446
24570
  !this.#isTypedFirstEventTimeout(message) &&
24447
24571
  !messageOnlyWatchdogTimeout &&
@@ -24460,6 +24584,7 @@ export class AgentSession {
24460
24584
  !managedFallback &&
24461
24585
  classification === "transient" &&
24462
24586
  !canReplayProviderOverload &&
24587
+ !boundBareDefaultCodexFailure &&
24463
24588
  !canReplayUnexpectedSocketClose &&
24464
24589
  !this.#isIdleStreamStallErrorMessage(message.errorMessage ?? "");
24465
24590
 
@@ -26036,7 +26161,8 @@ export class AgentSession {
26036
26161
 
26037
26162
  /** Build a background snapshot with in-flight assistant and optional context. */
26038
26163
  #buildEphemeralSnapshot(promptText: string, prependMessages?: AgentMessage[]): AgentMessage[] {
26039
- const messages = [...this.messages];
26164
+ // Side requests run in a fresh provider session with no prefix to preserve.
26165
+ const messages = this.#withoutEphemeralCustomMessages(this.messages);
26040
26166
  const streaming = this.agent.state.streamMessage;
26041
26167
  if (streaming && streaming.role === "assistant") {
26042
26168
  const preservedBlocks: AssistantMessage["content"] = [];
@@ -27268,7 +27394,7 @@ export class AgentSession {
27268
27394
  const branchEntries = this.sessionManager.getBranch();
27269
27395
  const latestCompaction = getLatestCompactionEntry(branchEntries);
27270
27396
  const boundaryTs = latestCompaction ? new Date(latestCompaction.timestamp).getTime() : 0;
27271
- const anchor = this.#findAnchorableUsageIndex(this.messages, boundaryTs);
27397
+ const anchor = this.#findAnchorableUsageIndex(this.agent.state.messages, boundaryTs);
27272
27398
  let value: ContextUsage;
27273
27399
  if (latestCompaction && !anchor) {
27274
27400
  value = { tokens: null, contextWindow, percent: null, source: "unknown" };
@@ -27291,7 +27417,7 @@ export class AgentSession {
27291
27417
  }
27292
27418
 
27293
27419
  #contextUsageCacheKey(model: Model, contextWindow: number): string {
27294
- const messages = this.messages;
27420
+ const messages = this.agent.state.messages;
27295
27421
  const lastMessage = messages[messages.length - 1];
27296
27422
  // Entry and leaf revisions change whenever the active branch changes, avoiding getBranch() on warm reads.
27297
27423
  const revision = this.sessionManager.revisionSnapshot();
@@ -27440,7 +27566,7 @@ export class AgentSession {
27440
27566
  * raw promptTokens/estimatedTokens quotient). Clamped to [0.5, 2] downstream.
27441
27567
  */
27442
27568
  #computeCompactionTokenCorrectionRatio(): number | undefined {
27443
- const messages = this.messages;
27569
+ const messages = this.agent.state.messages;
27444
27570
  let lastUsageIndex = -1;
27445
27571
  let lastUsage: Usage | undefined;
27446
27572
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -27490,7 +27616,7 @@ export class AgentSession {
27490
27616
  tokens: number;
27491
27617
  anchored: boolean;
27492
27618
  } {
27493
- const messages = this.messages;
27619
+ const messages = this.agent.state.messages;
27494
27620
  let anchor = knownAnchor;
27495
27621
  if (boundaryTs === undefined) {
27496
27622
  const latestCompaction = getLatestCompactionEntry(this.sessionManager.getBranch());
@@ -27666,7 +27792,7 @@ export class AgentSession {
27666
27792
  */
27667
27793
  formatSessionAsText(): string {
27668
27794
  return formatSessionDumpText({
27669
- messages: this.messages,
27795
+ messages: this.#withoutEphemeralCustomMessages(this.messages),
27670
27796
  systemPrompt: this.agent.state.systemPrompt,
27671
27797
  model: this.agent.state.model,
27672
27798
  thinkingLevel: this.#thinkingLevel,