@gajae-code/coding-agent 0.18.1 → 0.18.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 (128) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/dist/types/cli/stats-cli.d.ts +0 -5
  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/autorouting-tier-map.d.ts +29 -2
  10. package/dist/types/config/model-profile-activation.d.ts +19 -0
  11. package/dist/types/config/model-registry.d.ts +16 -1
  12. package/dist/types/coordinator-mcp/session-reaper.d.ts +5 -0
  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/exa/mcp-client.d.ts +1 -5
  16. package/dist/types/exec/bash-shell-guardian.d.ts +26 -0
  17. package/dist/types/exec/bash-shell-supervisor.d.ts +12 -0
  18. package/dist/types/hashline/parser.d.ts +28 -5
  19. package/dist/types/modes/acp/acp-agent.d.ts +6 -1
  20. package/dist/types/sdk/broker/discovery.d.ts +8 -0
  21. package/dist/types/sdk/broker/ensure.d.ts +1 -0
  22. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +4 -0
  23. package/dist/types/sdk/broker/lifecycle.d.ts +6 -1
  24. package/dist/types/sdk/broker/recovery-backoff.d.ts +62 -0
  25. package/dist/types/sdk/broker/runtime.d.ts +21 -1
  26. package/dist/types/sdk/broker/startup-budget.d.ts +5 -1
  27. package/dist/types/sdk/bus/index.d.ts +1 -0
  28. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  29. package/dist/types/sdk/bus/telegram-daemon.d.ts +1 -0
  30. package/dist/types/sdk/cli/diagnostics-cli.d.ts +18 -0
  31. package/dist/types/sdk/diagnostics/observe-broker.d.ts +185 -0
  32. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  33. package/dist/types/sdk/host/session-runtime.d.ts +7 -0
  34. package/dist/types/sdk/index.d.ts +1 -0
  35. package/dist/types/sdk/prompt-watchdog.d.ts +1 -1
  36. package/dist/types/sdk/router/session-router.d.ts +2 -0
  37. package/dist/types/sdk/session.d.ts +13 -0
  38. package/dist/types/session/agent-session.d.ts +5 -1
  39. package/dist/types/tools/ask-contract.d.ts +75 -254
  40. package/dist/types/tools/bash.d.ts +1 -0
  41. package/package.json +7 -7
  42. package/scripts/build-sdk-package-smoke.ts +5 -0
  43. package/src/cli/public-command-entry.ts +5 -0
  44. package/src/cli/ssh-cli.ts +6 -3
  45. package/src/cli/stats-cli.ts +4 -36
  46. package/src/cli/web-search-cli.ts +9 -3
  47. package/src/cli-commands.ts +14 -0
  48. package/src/cli-main.ts +4 -8
  49. package/src/cli-ordinary.ts +197 -0
  50. package/src/cli.ts +25 -186
  51. package/src/commands/commit.ts +2 -2
  52. package/src/commands/contribution-prep.ts +1 -3
  53. package/src/commands/sdk.ts +60 -2
  54. package/src/commands/update.ts +15 -1
  55. package/src/config/autorouting-tier-map.ts +20 -2
  56. package/src/config/file-lock.ts +91 -2
  57. package/src/config/model-preset-registry.ts +17 -2
  58. package/src/config/model-profile-activation.ts +73 -5
  59. package/src/config/model-profile-contract.ts +4 -1
  60. package/src/config/model-profiles.ts +29 -29
  61. package/src/config/model-registry.ts +45 -17
  62. package/src/config/model-resolver.ts +9 -1
  63. package/src/coordinator-mcp/event-webhook.ts +19 -14
  64. package/src/coordinator-mcp/policy.ts +14 -4
  65. package/src/coordinator-mcp/server.ts +142 -7
  66. package/src/coordinator-mcp/session-reaper.ts +9 -1
  67. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  68. package/src/edit/modes/replace.ts +6 -6
  69. package/src/edit/streaming.ts +15 -15
  70. package/src/exa/mcp-client.ts +41 -8
  71. package/src/exec/bash-shell-guardian.ts +184 -34
  72. package/src/exec/bash-shell-supervisor.ts +60 -3
  73. package/src/hashline/execute.ts +51 -3
  74. package/src/hashline/parser.ts +63 -16
  75. package/src/internal-urls/docs-index.generated.ts +29 -28
  76. package/src/memories/index.ts +8 -2
  77. package/src/modes/acp/acp-agent.ts +688 -371
  78. package/src/modes/components/tool-execution.ts +1 -1
  79. package/src/modes/controllers/ssh-command-controller.ts +3 -1
  80. package/src/prompts/tools/ask.md +2 -0
  81. package/src/sdk/broker/broker.ts +46 -0
  82. package/src/sdk/broker/daemon-entry.ts +67 -6
  83. package/src/sdk/broker/discovery.ts +8 -0
  84. package/src/sdk/broker/ensure.ts +156 -20
  85. package/src/sdk/broker/lifecycle-ledger.ts +110 -12
  86. package/src/sdk/broker/lifecycle.ts +108 -34
  87. package/src/sdk/broker/recovery-backoff.ts +135 -0
  88. package/src/sdk/broker/runtime.ts +88 -1
  89. package/src/sdk/broker/startup-budget.ts +6 -1
  90. package/src/sdk/broker/transport.ts +5 -1
  91. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  92. package/src/sdk/bus/index.ts +305 -19
  93. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  94. package/src/sdk/bus/telegram-daemon-control.ts +15 -12
  95. package/src/sdk/bus/telegram-daemon.ts +34 -1
  96. package/src/sdk/cli/diagnostics-cli.ts +247 -0
  97. package/src/sdk/diagnostics/observe-broker.ts +654 -0
  98. package/src/sdk/host/control/dispatch.ts +52 -4
  99. package/src/sdk/host/control/operations.ts +1 -1
  100. package/src/sdk/host/session-runtime.test.ts +40 -2
  101. package/src/sdk/host/session-runtime.ts +112 -8
  102. package/src/sdk/index.ts +1 -0
  103. package/src/sdk/lifecycle-session.ts +1 -0
  104. package/src/sdk/prompt-watchdog.ts +17 -4
  105. package/src/sdk/protocol/operation-registry.ts +1 -1
  106. package/src/sdk/router/session-router.ts +7 -1
  107. package/src/sdk/session.ts +37 -8
  108. package/src/session/agent-session.ts +128 -41
  109. package/src/session/blob-store.ts +78 -33
  110. package/src/session/internal/managed-session-storage.ts +119 -3
  111. package/src/setup/hermes/templates/operator-instructions.v1.md +19 -1
  112. package/src/setup/hermes-setup.ts +4 -1
  113. package/src/slash-commands/builtin-registry.ts +8 -5
  114. package/src/tools/ask-contract.test.ts +76 -1
  115. package/src/tools/ask-contract.ts +57 -6
  116. package/src/tools/ask.ts +16 -7
  117. package/src/tools/bash.ts +77 -29
  118. package/src/tools/descriptors.test.ts +29 -0
  119. package/src/tools/tool-catalog.generated.ts +7 -4
  120. package/src/web/search/index.ts +2 -2
  121. package/dist/types/cli/grep-cli.d.ts +0 -17
  122. package/dist/types/cli/shell-cli.d.ts +0 -8
  123. package/dist/types/commands/grep.d.ts +0 -42
  124. package/dist/types/commands/shell.d.ts +0 -21
  125. package/src/cli/grep-cli.ts +0 -160
  126. package/src/cli/shell-cli.ts +0 -176
  127. package/src/commands/grep.ts +0 -48
  128. package/src/commands/shell.ts +0 -29
@@ -133,6 +133,7 @@ import {
133
133
  classifyFallbackTrigger,
134
134
  EMPTY_RESPONSE_PROVIDER_CODE,
135
135
  type FallbackAttemptToken,
136
+ PROVIDER_PROTOCOL_MISMATCH_ERROR_CODE,
136
137
  type FallbackTriggerClass,
137
138
  SERVER_OVERLOADED_PROVIDER_CODE,
138
139
  STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE,
@@ -1210,7 +1211,10 @@ export interface TrackedSendUserMessageOptions extends SendUserMessageOptions {
1210
1211
  trackSubmission: true;
1211
1212
  }
1212
1213
 
1213
- type SendUserMessageDispatchOptions = SendUserMessageOptions & { trackSubmission?: boolean };
1214
+ type SendUserMessageDispatchOptions = SendUserMessageOptions & {
1215
+ trackSubmission?: boolean;
1216
+ expectedSdkRunToken?: string;
1217
+ };
1214
1218
 
1215
1219
  type InternalPromptOptions = PromptOptions & { sdkRunToken?: string };
1216
1220
  type InternalCustomMessageOptions = Pick<
@@ -1419,8 +1423,11 @@ const BARE_DEFAULT_WATCHDOG_ERROR =
1419
1423
  const PROVIDER_FIRST_EVENT_TIMEOUT_ERROR = "Provider stream timed out while waiting for the first event";
1420
1424
  const WRAPPED_PROVIDER_FIRST_EVENT_TIMEOUT_ERROR = `Error: ${PROVIDER_FIRST_EVENT_TIMEOUT_ERROR}`;
1421
1425
  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. */
1426
+ const BARE_DEFAULT_CODEX_RETRYABLE_ERROR =
1427
+ /^Codex error event(?:: .*)? \(code=(?:server_is_overloaded|server_error|internal_error)(?:, [^)]+)*\)$/;
1428
+ const BARE_DEFAULT_CODEX_RETRYABLE_CODES = new Set(["server_is_overloaded", "server_error", "internal_error"]);
1429
+ const BARE_DEFAULT_OVERLOAD_CODES = new Set([SERVER_OVERLOADED_PROVIDER_CODE]);
1430
+ /** Anthropic's typed capacity-overload `error.type`, the only Anthropic overload code admitted below. */
1424
1431
  const ANTHROPIC_OVERLOADED_ERROR_TYPE = "overloaded_error";
1425
1432
  const KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES = {
1426
1433
  "anthropic-messages": new Set([
@@ -1501,8 +1508,8 @@ function isBareDefaultMessageOnlyFirstEventTimeout(message: AssistantMessage): b
1501
1508
  );
1502
1509
  }
1503
1510
  /**
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,
1511
+ * True when the attempt's structured evidence is exactly an admitted provider
1512
+ * code and nothing else: no status, no retained retry header,
1506
1513
  * no second typed code, no error kind, and none of the transport's other
1507
1514
  * observations (request size, first-event timing, endpoint class, provider retry
1508
1515
  * ceiling). The code arrives statuslessly (a Codex error event, an HTTP 200
@@ -1511,15 +1518,18 @@ function isBareDefaultMessageOnlyFirstEventTimeout(message: AssistantMessage): b
1511
1518
  * attempt is no longer provably that bare capacity rejection. The code is
1512
1519
  * compared case-sensitively, matching the parser and the transport gate.
1513
1520
  */
1514
- function isExactTypedOverloadFacts(message: AssistantMessage): boolean {
1521
+ function isExactTypedOverloadFacts(
1522
+ message: AssistantMessage,
1523
+ acceptedProviderCodes: ReadonlySet<string> = BARE_DEFAULT_OVERLOAD_CODES,
1524
+ ): boolean {
1515
1525
  if (message.errorKind !== undefined || message.errorStatus !== undefined) return false;
1516
1526
  const facts = message.transportFailure;
1517
1527
  if (!facts) return false;
1518
1528
  return (
1519
- facts.providerCode === SERVER_OVERLOADED_PROVIDER_CODE &&
1529
+ acceptedProviderCodes.has(facts.providerCode ?? "") &&
1520
1530
  facts.status === undefined &&
1521
1531
  facts.anthropicErrorType === undefined &&
1522
- (facts.openaiErrorCode === undefined || facts.openaiErrorCode === SERVER_OVERLOADED_PROVIDER_CODE) &&
1532
+ (facts.openaiErrorCode === undefined || facts.openaiErrorCode === facts.providerCode) &&
1523
1533
  (facts.headers === undefined || Object.keys(facts.headers).length === 0) &&
1524
1534
  facts.requestBytes === undefined &&
1525
1535
  facts.firstEventElapsedMs === undefined &&
@@ -1548,8 +1558,9 @@ function isStatuslessTypedOverloadFacts(facts: TransportFailureFacts | undefined
1548
1558
  function isBareDefaultCodexOverload(message: AssistantMessage): boolean {
1549
1559
  return (
1550
1560
  message.api === "openai-codex-responses" &&
1551
- BARE_DEFAULT_CODEX_OVERLOAD_ERROR.test(message.errorMessage ?? "") &&
1552
- (isExactTypedOverloadFacts(message) || !hasBareDefaultRetryDisqualifyingFacts(message)) &&
1561
+ (isExactTypedOverloadFacts(message, BARE_DEFAULT_CODEX_RETRYABLE_CODES) ||
1562
+ (BARE_DEFAULT_CODEX_RETRYABLE_ERROR.test(message.errorMessage ?? "") &&
1563
+ !hasBareDefaultRetryDisqualifyingFacts(message))) &&
1553
1564
  !assistantMessageHasVisibleOrToolContent(message)
1554
1565
  );
1555
1566
  }
@@ -5077,6 +5088,7 @@ export class AgentSession {
5077
5088
  if (lease) this.#deferredAgentEndLeases.delete(pending);
5078
5089
  this.#startAgentEndPublication(pending, lease);
5079
5090
  }
5091
+
5080
5092
  #startAgentEndPublication(pending: AgentSessionEvent, lease?: RunResourceProducerLease): void {
5081
5093
  this.#agentEndPublicationInFlight++;
5082
5094
  const pendingScope = (pending as AgentSessionEvent & { scope?: AttemptScopeRef }).scope as
@@ -6166,7 +6178,10 @@ export class AgentSession {
6166
6178
  path.basename(sessionModeStatePath(this.sessionManager.getCwd(), currentSessionId, active.skill)),
6167
6179
  );
6168
6180
  const raw = fs.readFileSync(filePath, "utf-8");
6169
- const parsed = JSON.parse(raw) as { state?: { intent_contract?: unknown } };
6181
+ const parsed = JSON.parse(raw) as { current_phase?: unknown; state?: { intent_contract?: unknown } };
6182
+ // A persisted final spec moves to handoff. Its next-workflow/approval
6183
+ // asks are not interview rounds and must not require recorder metadata.
6184
+ if (parsed.current_phase !== "interviewing") return undefined;
6170
6185
  const intentContract = parsed.state?.intent_contract;
6171
6186
  if (intentContract === undefined) return "topology";
6172
6187
  assertDeepInterviewIntentManifest(intentContract);
@@ -6318,7 +6333,10 @@ export class AgentSession {
6318
6333
  },
6319
6334
  };
6320
6335
  }
6321
- const transformedMessages = await this.#transformContext([...this.messages], options.signal);
6336
+ const transformedMessages = await this.#transformContext(
6337
+ this.#withoutEphemeralCustomMessages(this.messages),
6338
+ options.signal,
6339
+ );
6322
6340
  const convertedMessages = await this.#convertToLlm(transformedMessages);
6323
6341
  const providerMessages = this.model
6324
6342
  ? normalizeMessagesForProvider(convertedMessages, this.model)
@@ -12297,9 +12315,16 @@ export class AgentSession {
12297
12315
  return this.agent.activeResourceRunId;
12298
12316
  }
12299
12317
 
12300
- /** All messages including custom types like BashExecutionMessage */
12318
+ /**
12319
+ * All messages including custom types like BashExecutionMessage. Request-scoped
12320
+ * ephemeral copies are retained only in `agent.state.messages` to keep the provider
12321
+ * cache prefix stable; they are never part of this public view.
12322
+ */
12301
12323
  get messages(): AgentMessage[] {
12302
- return this.agent.state.messages;
12324
+ const messages = this.agent.state.messages;
12325
+ return messages.some(message => message.role === "custom" && this.#isEphemeralCustomMessageType(message.customType))
12326
+ ? this.#withoutEphemeralCustomMessages(messages)
12327
+ : messages;
12303
12328
  }
12304
12329
  get transcriptPromptGeneration(): number {
12305
12330
  return this.#promptGeneration;
@@ -12389,9 +12414,16 @@ export class AgentSession {
12389
12414
  let continuationSdkRunToken: string | undefined;
12390
12415
  try {
12391
12416
  const volatileProjectContextMessage = await this.#buildVolatileProjectContextMessage();
12392
- this.agent.appendMessage(volatileProjectContextMessage);
12417
+ if (!this.#isLatestRetainedEphemeralCopy(volatileProjectContextMessage)) {
12418
+ this.agent.appendMessage(volatileProjectContextMessage);
12419
+ }
12393
12420
  const untrustedMcpServerInstructionsMessage = this.#buildUntrustedMcpServerInstructionsMessage();
12394
- if (untrustedMcpServerInstructionsMessage) this.agent.appendMessage(untrustedMcpServerInstructionsMessage);
12421
+ if (
12422
+ untrustedMcpServerInstructionsMessage &&
12423
+ !this.#isLatestRetainedEphemeralCopy(untrustedMcpServerInstructionsMessage)
12424
+ ) {
12425
+ this.agent.appendMessage(untrustedMcpServerInstructionsMessage);
12426
+ }
12395
12427
  const hindsightState = this.getHindsightSessionState();
12396
12428
  await hindsightState?.maybeRecallOnAgentStart();
12397
12429
  hindsightRecall = hindsightState?.getRecallSnippetForInjection();
@@ -12429,7 +12461,6 @@ export class AgentSession {
12429
12461
  });
12430
12462
  if (continuationSdkRunToken === undefined) await this.#waitForPostPromptRecovery();
12431
12463
  } finally {
12432
- this.#removeEphemeralCustomMessages();
12433
12464
  await this.#settleEndedInFlight(
12434
12465
  inFlightPrompt,
12435
12466
  continuationSdkRunToken === undefined ? "full" : "publication",
@@ -13293,6 +13324,21 @@ export class AgentSession {
13293
13324
  );
13294
13325
  }
13295
13326
 
13327
+ /**
13328
+ * Sent ephemeral copies stay in live state so the provider prefix is stable. A new
13329
+ * copy is only worth appending when its provider-visible content changed; an
13330
+ * identical one would just repeat tokens that still count against the window.
13331
+ */
13332
+ #isLatestRetainedEphemeralCopy(message: CustomMessage): boolean {
13333
+ const messages = this.agent.state.messages;
13334
+ for (let index = messages.length - 1; index >= 0; index--) {
13335
+ const candidate = messages[index];
13336
+ if (candidate?.role !== "custom" || candidate.customType !== message.customType) continue;
13337
+ return JSON.stringify(candidate.content) === JSON.stringify(message.content);
13338
+ }
13339
+ return false;
13340
+ }
13341
+
13296
13342
  #removeEphemeralCustomMessages(): void {
13297
13343
  const messages = this.agent.state.messages;
13298
13344
  const withoutEphemeralMessages = this.#withoutEphemeralCustomMessages(messages);
@@ -13988,8 +14034,6 @@ export class AgentSession {
13988
14034
  throw new Error(formatNoCredentialOnboardingError(this.model.provider));
13989
14035
  }
13990
14036
 
13991
- this.#removeEphemeralCustomMessages();
13992
-
13993
14037
  // Check if we need to compact before sending (catches aborted responses)
13994
14038
  const lastAssistant = this.#findLastAssistantMessage();
13995
14039
  if (lastAssistant && !options?.skipCompactionCheck) {
@@ -14164,9 +14208,16 @@ export class AgentSession {
14164
14208
  messages.push(goalModeMessage);
14165
14209
  }
14166
14210
  const volatileProjectContextMessage = await this.#buildVolatileProjectContextMessage();
14167
- messages.push(volatileProjectContextMessage);
14211
+ if (!this.#isLatestRetainedEphemeralCopy(volatileProjectContextMessage)) {
14212
+ messages.push(volatileProjectContextMessage);
14213
+ }
14168
14214
  const untrustedMcpServerInstructionsMessage = this.#buildUntrustedMcpServerInstructionsMessage();
14169
- if (untrustedMcpServerInstructionsMessage) messages.push(untrustedMcpServerInstructionsMessage);
14215
+ if (
14216
+ untrustedMcpServerInstructionsMessage &&
14217
+ !this.#isLatestRetainedEphemeralCopy(untrustedMcpServerInstructionsMessage)
14218
+ ) {
14219
+ messages.push(untrustedMcpServerInstructionsMessage);
14220
+ }
14170
14221
 
14171
14222
  // Roster: one Phase A claim, revalidated and reused on every attempt;
14172
14223
  // a superseded claim releases and the prompt proceeds without it.
@@ -14328,7 +14379,6 @@ export class AgentSession {
14328
14379
  return;
14329
14380
  throw error;
14330
14381
  } finally {
14331
- this.#removeEphemeralCustomMessages();
14332
14382
  if (rosterClaim) {
14333
14383
  this.agent.replaceMessages(
14334
14384
  this.agent.state.messages.filter(
@@ -14606,6 +14656,7 @@ export class AgentSession {
14606
14656
  onQueuedAfterAdmission?: (message: AgentMessage, cancelQueued: () => boolean) => void;
14607
14657
  external?: boolean;
14608
14658
  sdkRunToken?: string;
14659
+ expectedSdkRunToken?: string;
14609
14660
  forceOneAtATime?: boolean;
14610
14661
  allowDuringSessionTransition?: boolean;
14611
14662
  allowCancelAndSubmit?: boolean;
@@ -14622,11 +14673,18 @@ export class AgentSession {
14622
14673
  const message = { role: "user" as const, content, attribution: "user" as const, timestamp: Date.now() };
14623
14674
  // Enqueue-time admission: the Agent only accepts a steer into a live,
14624
14675
  // 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.
14676
+ // prompt still in preflight, aborting) has no run to steer. Ordinary steers
14677
+ // become a sequential follow-up owned by the next turn; SDK turn-bound
14678
+ // steers fail closed rather than being delivered to a successor run.
14679
+ if (
14680
+ options?.expectedSdkRunToken !== undefined &&
14681
+ this.#activeSdkRunToken !== options.expectedSdkRunToken
14682
+ )
14683
+ throw Object.assign(new Error("The expected SDK run is not active."), { code: "turn_not_active" });
14628
14684
  const admission = this.agent.steer(message, options?.forceOneAtATime ? { forceOneAtATime: true } : undefined);
14629
14685
  if (!admission.admitted) {
14686
+ if (options?.expectedSdkRunToken !== undefined)
14687
+ throw Object.assign(new Error("The expected SDK run is not active."), { code: "turn_not_active" });
14630
14688
  const queuedFollowUp = await this.#queueFollowUpAfterReservation(message, displayText, {
14631
14689
  forceOneAtATime: options?.forceOneAtATime,
14632
14690
  claimsGenuineUserIntent: options?.claimsGenuineUserIntent,
@@ -15779,6 +15837,7 @@ export class AgentSession {
15779
15837
  onPromoted: onQueuedPromoted,
15780
15838
  external: true,
15781
15839
  sdkRunToken: internalOptions?.sdkRunToken,
15840
+ expectedSdkRunToken: internalOptions?.expectedSdkRunToken,
15782
15841
  forceOneAtATime: options?.queuePolicy === "sequential",
15783
15842
  onQueuedAfterAdmission: onTrackedQueued,
15784
15843
  allowDuringSessionTransition: this.#hasCommittedSuccessorTrackedAdmission(options),
@@ -16145,6 +16204,9 @@ export class AgentSession {
16145
16204
  this.#cachedWorkspaceTree = undefined;
16146
16205
  this.#cachedWorkspaceTreeAt = 0;
16147
16206
  this.#pendingWorkspaceTreeRescope = true;
16207
+ // Sent copies name the abandoned root; the rescope already rebuilds the stable
16208
+ // prompt, so there is no cache prefix left to preserve by keeping them.
16209
+ this.#removeEphemeralCustomMessages();
16148
16210
  }
16149
16211
 
16150
16212
  /** Skill loading warnings captured by SDK */
@@ -20282,7 +20344,7 @@ export class AgentSession {
20282
20344
  }
20283
20345
 
20284
20346
  const handoffText = await generateHandoff(
20285
- this.agent.state.messages,
20347
+ this.#withoutEphemeralCustomMessages(this.agent.state.messages),
20286
20348
  model,
20287
20349
  apiKey,
20288
20350
  {
@@ -20516,7 +20578,7 @@ export class AgentSession {
20516
20578
  sessionId: this.sessionId,
20517
20579
  cwd: this.sessionManager.getCwd(),
20518
20580
  sessionFile: this.sessionFile,
20519
- messages: this.agent.state.messages,
20581
+ messages: this.#withoutEphemeralCustomMessages(this.agent.state.messages),
20520
20582
  customInstructions: options.customInstructions,
20521
20583
  },
20522
20584
  options,
@@ -22699,6 +22761,7 @@ export class AgentSession {
22699
22761
  willRetry && !continuationSkipReason
22700
22762
  ? await this.#scheduleOverflowRetryContinuation(generation, options?.resourceRunId)
22701
22763
  : false;
22764
+
22702
22765
  await this.#emitSessionEvent({
22703
22766
  type: "auto_compaction_end",
22704
22767
  action,
@@ -22707,6 +22770,7 @@ export class AgentSession {
22707
22770
  willRetry: overflowContinuationScheduled,
22708
22771
  continuationSkipReason,
22709
22772
  });
22773
+
22710
22774
  if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
22711
22775
 
22712
22776
  if (willRetry) {
@@ -22940,6 +23004,7 @@ export class AgentSession {
22940
23004
  return false;
22941
23005
  const contextWindow = this.model?.contextWindow ?? 0;
22942
23006
  if (classifyContextOverflow(message, transportFailure, contextWindow)) return false;
23007
+ if (isBareDefaultCodexOverload(message) && this.settings.get("retry.enabled") !== false) return true;
22943
23008
  const managedFallback = this.#defaultFallbackChain().chain.entries.length > 1;
22944
23009
  // An account-specific model rejection that cannot rotate to another
22945
23010
  // credential stays terminal only on the session's own retry path; managed
@@ -23082,6 +23147,9 @@ export class AgentSession {
23082
23147
  // Without this the message carries no transport facts and would fall
23083
23148
  // through to "unknown", which is admitted for bounded retry (#5627).
23084
23149
  if (message.errorCode === REPETITION_GUARD_ERROR_CODE) return "terminal";
23150
+ // A 2xx body in the wrong protocol replays identically; its embedded body text
23151
+ // (e.g. "HTTP 503") must not be reparsed as a transient failure.
23152
+ if (message.errorCode === PROVIDER_PROTOCOL_MISMATCH_ERROR_CODE) return "terminal";
23085
23153
  if (message.errorKind === "local_snapshot_failure") return "local_snapshot";
23086
23154
  if (message.errorKind === "local_buffer_overflow") return "local_buffer_overflow";
23087
23155
  if (this.#isTypedFirstEventTimeout(message)) return "first_event_timeout";
@@ -24301,6 +24369,7 @@ export class AgentSession {
24301
24369
  isBareDefaultCodexOverload(message) ||
24302
24370
  isBareDefaultAnthropicOverload(message) ||
24303
24371
  isBareDefaultOpenAIResponsesOverload(message);
24372
+ const canReplayCodexProviderOverload = isBareDefaultCodexOverload(message);
24304
24373
  const reportedRetryMaxAttempts = transportFailure?.retryMaxAttempts;
24305
24374
  if (reportedRetryMaxAttempts !== undefined) {
24306
24375
  this.#providerRetryMaxAttempts = Math.min(
@@ -24340,19 +24409,24 @@ export class AgentSession {
24340
24409
  }
24341
24410
  : false;
24342
24411
  }
24412
+ const fallbackTrigger = this.#fallbackTriggerFor(message, !managedFallback, transportFailure);
24343
24413
  const trigger:
24344
24414
  | { class: FallbackTriggerClass; retryAfterMs?: number; authDisposition?: AuthDisposition }
24345
- | undefined = this.#fallbackTriggerFor(message, !managedFallback, transportFailure);
24415
+ | undefined = canReplayCodexProviderOverload ? { class: "server" } : fallbackTrigger;
24346
24416
  // OpenAI's typed statusless capacity-overload code (issue #5018) must not
24347
24417
  // gain managed-chain retry/advance authority from its new facts. Before
24348
24418
  // the code survived transport, this failure reached the session as an
24349
24419
  // ordinary committed error and surfaced immediately, so mirror that
24350
- // behavior with the existing exhaustion decision.
24420
+ // behavior with the existing exhaustion decision. However, when called from
24421
+ // the agent_end path (managedOutcome=false), we must return false to allow
24422
+ // proper session termination handling.
24351
24423
  if (managedFallback && isStatuslessTypedOverloadFacts(transportFailure)) {
24352
- return this.#managedFallbackExhaustionDecision(
24353
- message,
24354
- message.errorMessage || "Model fallback attempt failed",
24355
- );
24424
+ return managedOutcome
24425
+ ? this.#managedFallbackExhaustionDecision(
24426
+ message,
24427
+ message.errorMessage || "Model fallback attempt failed",
24428
+ )
24429
+ : false;
24356
24430
  }
24357
24431
  if (!trigger) {
24358
24432
  return managedOutcome
@@ -24425,8 +24499,15 @@ export class AgentSession {
24425
24499
  if (!managedFallback && assistantMessageHasVisibleOrToolContent(message)) {
24426
24500
  return false;
24427
24501
  }
24502
+ const canReplayUnexpectedSocketClose =
24503
+ classification === "transient" &&
24504
+ isUnexpectedSocketCloseMessage(message.errorMessage ?? "") &&
24505
+ !hasBareDefaultRetryDisqualifyingFacts(message) &&
24506
+ this.#hasCleanRetryReplaySafety;
24428
24507
  // Bare defaults retain their narrow watchdog and provider capacity-overload
24429
- // admissions. A first-event timeout adds the typed, content-free,
24508
+ // admissions. A content-free unexpected socket close is replay-safe when
24509
+ // the attempt has no conflicting facts and the retry scope is clean. A
24510
+ // first-event timeout adds the typed, content-free,
24430
24511
  // current-clean-scope requirement above; other transient watchdogs preserve
24431
24512
  // legacy behavior. A provider overload is admitted only from that provider's
24432
24513
  // own typed overload code on a content-free attempt carrying no conflicting
@@ -24435,12 +24516,16 @@ export class AgentSession {
24435
24516
  if (!managedFallback && !legacyRetryConfigured && !canReplayRotatedCredential && !canReplayEmptyResponse) {
24436
24517
  if (
24437
24518
  (!canReplayProviderOverload &&
24519
+ !canReplayUnexpectedSocketClose &&
24438
24520
  !this.#isTypedFirstEventTimeout(message) &&
24439
24521
  !messageOnlyWatchdogTimeout &&
24440
24522
  (hasBareDefaultRetryDisqualifyingFacts(message) ||
24441
24523
  (classification !== "transient" && classification !== "first_event_timeout") ||
24442
24524
  !BARE_DEFAULT_WATCHDOG_ERROR.test(message.errorMessage ?? ""))) ||
24443
- (!firstEventTimeout && !messageOnlyWatchdogTimeout && !this.#hasCleanRetryReplaySafety)
24525
+ (!firstEventTimeout &&
24526
+ !messageOnlyWatchdogTimeout &&
24527
+ !canReplayUnexpectedSocketClose &&
24528
+ !this.#hasCleanRetryReplaySafety)
24444
24529
  ) {
24445
24530
  return false;
24446
24531
  }
@@ -24449,6 +24534,7 @@ export class AgentSession {
24449
24534
  !managedFallback &&
24450
24535
  classification === "transient" &&
24451
24536
  !canReplayProviderOverload &&
24537
+ !canReplayUnexpectedSocketClose &&
24452
24538
  !this.#isIdleStreamStallErrorMessage(message.errorMessage ?? "");
24453
24539
 
24454
24540
  const failedSelector = managedFallback ? controller.currentSelector() : undefined;
@@ -26024,7 +26110,8 @@ export class AgentSession {
26024
26110
 
26025
26111
  /** Build a background snapshot with in-flight assistant and optional context. */
26026
26112
  #buildEphemeralSnapshot(promptText: string, prependMessages?: AgentMessage[]): AgentMessage[] {
26027
- const messages = [...this.messages];
26113
+ // Side requests run in a fresh provider session with no prefix to preserve.
26114
+ const messages = this.#withoutEphemeralCustomMessages(this.messages);
26028
26115
  const streaming = this.agent.state.streamMessage;
26029
26116
  if (streaming && streaming.role === "assistant") {
26030
26117
  const preservedBlocks: AssistantMessage["content"] = [];
@@ -27256,7 +27343,7 @@ export class AgentSession {
27256
27343
  const branchEntries = this.sessionManager.getBranch();
27257
27344
  const latestCompaction = getLatestCompactionEntry(branchEntries);
27258
27345
  const boundaryTs = latestCompaction ? new Date(latestCompaction.timestamp).getTime() : 0;
27259
- const anchor = this.#findAnchorableUsageIndex(this.messages, boundaryTs);
27346
+ const anchor = this.#findAnchorableUsageIndex(this.agent.state.messages, boundaryTs);
27260
27347
  let value: ContextUsage;
27261
27348
  if (latestCompaction && !anchor) {
27262
27349
  value = { tokens: null, contextWindow, percent: null, source: "unknown" };
@@ -27279,7 +27366,7 @@ export class AgentSession {
27279
27366
  }
27280
27367
 
27281
27368
  #contextUsageCacheKey(model: Model, contextWindow: number): string {
27282
- const messages = this.messages;
27369
+ const messages = this.agent.state.messages;
27283
27370
  const lastMessage = messages[messages.length - 1];
27284
27371
  // Entry and leaf revisions change whenever the active branch changes, avoiding getBranch() on warm reads.
27285
27372
  const revision = this.sessionManager.revisionSnapshot();
@@ -27428,7 +27515,7 @@ export class AgentSession {
27428
27515
  * raw promptTokens/estimatedTokens quotient). Clamped to [0.5, 2] downstream.
27429
27516
  */
27430
27517
  #computeCompactionTokenCorrectionRatio(): number | undefined {
27431
- const messages = this.messages;
27518
+ const messages = this.agent.state.messages;
27432
27519
  let lastUsageIndex = -1;
27433
27520
  let lastUsage: Usage | undefined;
27434
27521
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -27478,7 +27565,7 @@ export class AgentSession {
27478
27565
  tokens: number;
27479
27566
  anchored: boolean;
27480
27567
  } {
27481
- const messages = this.messages;
27568
+ const messages = this.agent.state.messages;
27482
27569
  let anchor = knownAnchor;
27483
27570
  if (boundaryTs === undefined) {
27484
27571
  const latestCompaction = getLatestCompactionEntry(this.sessionManager.getBranch());
@@ -27654,7 +27741,7 @@ export class AgentSession {
27654
27741
  */
27655
27742
  formatSessionAsText(): string {
27656
27743
  return formatSessionDumpText({
27657
- messages: this.messages,
27744
+ messages: this.#withoutEphemeralCustomMessages(this.messages),
27658
27745
  systemPrompt: this.agent.state.systemPrompt,
27659
27746
  model: this.agent.state.model,
27660
27747
  thinkingLevel: this.#thinkingLevel,
@@ -4,6 +4,7 @@ import * as fsp from "node:fs/promises";
4
4
  import * as path from "node:path";
5
5
  import { exactUnlink } from "@gajae-code/natives";
6
6
  import { isEnoent, logger, postmortem } from "@gajae-code/utils";
7
+ import { readLinuxProcStartTimeSync } from "../gjc-runtime/linux-proc";
7
8
 
8
9
  const BLOB_PREFIX = "blob:sha256:";
9
10
  const CANONICAL_BLOB_NAME = /^[0-9a-f]{64}$/;
@@ -46,10 +47,21 @@ const RESIDENT_CACHE_SWEEP_EXPECTED_IO_CODES = new Set([
46
47
  ]);
47
48
  const ownedResidentCacheInstanceDirs = new Set<string>();
48
49
  const activeResidentCacheRootSweeps = new Set<string>();
49
- let ownResidentCacheProcessStartTimeMs: number | null | undefined;
50
+ let ownResidentCacheProcessStart: ResidentCacheProcessStart | undefined;
50
51
 
51
- /** Marks `startTimeMs` as an absolute instant parsed from a UTC-pinned `ps` render. */
52
- const RESIDENT_CACHE_START_TIME_BASIS = "utc";
52
+ /** Marks `startTimeMs` as an absolute instant parsed from a UTC-pinned `ps` render (non-Linux). */
53
+ const RESIDENT_CACHE_UTC_START_TIME_BASIS = "utc";
54
+ /**
55
+ * Prefix for a Linux start identity: `startTimeMs` holds `/proc/<pid>/stat` field 22
56
+ * (clock ticks since boot) and the suffix is the kernel `boot_id`. Ticks are immune
57
+ * to wall-clock adjustment; the boot id scopes them to one kernel boot.
58
+ */
59
+ const RESIDENT_CACHE_LINUX_START_TIME_BASIS_PREFIX = "linux-proc-ticks:";
60
+
61
+ interface ResidentCacheProcessStart {
62
+ readonly startTimeMs: number | null;
63
+ readonly basis: string;
64
+ }
53
65
 
54
66
  interface ResidentCacheOwnerToken {
55
67
  readonly pid: number;
@@ -240,19 +252,47 @@ function assertResidentCacheDirectoryPathMatchesDescriptor(pathname: string, des
240
252
  assertResidentCacheDirectoryDescriptor(pathname, current, descriptor, uid);
241
253
  }
242
254
 
243
- function residentCacheProcessStartTimeMs(pid: number): number | null {
244
- if (pid === process.pid && ownResidentCacheProcessStartTimeMs !== undefined) {
245
- return ownResidentCacheProcessStartTimeMs;
246
- }
255
+ let residentCacheLinuxBootId: string | null | undefined;
247
256
 
248
- // Mirror file-lock's `ps`-based process incarnation probe, but pin the locale
249
- // AND the zone. `lstart` is a zoneless local wall clock rendered by `ps` from
250
- // /etc/localtime, while `Date.parse` of a zoneless string binds it to the JS
251
- // runtime's own zone. Those are two independent resolutions, so a reader whose
252
- // runtime resolves UTC while `ps` renders UTC+9 derives an epoch nine hours off
253
- // for the very same live process — and this value's only job is to detect PID
254
- // reuse, so a disagreeing reader reports every live owner as reused and the GC
255
- // reaps a running session's cache. Pinning TZ on the child fixes the rendering
257
+ function readResidentCacheLinuxBootId(): string | null {
258
+ if (residentCacheLinuxBootId !== undefined) return residentCacheLinuxBootId;
259
+ let bootId: string | null = null;
260
+ try {
261
+ const text = fs.readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
262
+ if (/^[0-9a-f-]{36}$/.test(text)) bootId = text;
263
+ } catch {
264
+ // Without a boot id, tick counts cannot be scoped to one boot.
265
+ }
266
+ residentCacheLinuxBootId = bootId;
267
+ return bootId;
268
+ }
269
+
270
+ function residentCacheProcessStart(pid: number): ResidentCacheProcessStart {
271
+ if (pid === process.pid && ownResidentCacheProcessStart !== undefined) return ownResidentCacheProcessStart;
272
+ const start = process.platform === "linux" ? residentCacheLinuxProcessStart(pid) : residentCacheUtcProcessStart(pid);
273
+ if (pid === process.pid) ownResidentCacheProcessStart = start;
274
+ return start;
275
+ }
276
+
277
+ // `ps -o lstart` is NOT a stable identity on Linux: procps renders it as
278
+ // `btime + starttime_ticks`, and `btime` is re-derived from the current wall clock
279
+ // minus uptime. On hosts whose clock is continuously slewed (WSL2 is the common
280
+ // case) `btime` drifts, so the very same live process reports a start time that
281
+ // moves by seconds to minutes over a session. Any drift reads as PID reuse and the
282
+ // GC reaps a running session's cache. The raw tick count never moves.
283
+ function residentCacheLinuxProcessStart(pid: number): ResidentCacheProcessStart {
284
+ const bootId = readResidentCacheLinuxBootId();
285
+ if (bootId === null) return { startTimeMs: null, basis: RESIDENT_CACHE_LINUX_START_TIME_BASIS_PREFIX };
286
+ const basis = `${RESIDENT_CACHE_LINUX_START_TIME_BASIS_PREFIX}${bootId}`;
287
+ const ticks = readLinuxProcStartTimeSync(pid);
288
+ const startTimeMs = ticks === null ? Number.NaN : Number(ticks);
289
+ return { startTimeMs: Number.isSafeInteger(startTimeMs) ? startTimeMs : null, basis };
290
+ }
291
+
292
+ function residentCacheUtcProcessStart(pid: number): ResidentCacheProcessStart {
293
+ // Pin the locale AND the zone. `lstart` is a zoneless local wall clock rendered
294
+ // by `ps` from /etc/localtime, while `Date.parse` of a zoneless string binds it
295
+ // to the JS runtime's own zone. Pinning TZ on the child fixes the rendering
256
296
  // side; the explicit GMT suffix fixes the parsing side.
257
297
  let startTimeMs: number | null = null;
258
298
  try {
@@ -269,8 +309,7 @@ function residentCacheProcessStartTimeMs(pid: number): number | null {
269
309
  } catch {
270
310
  // An unavailable process-start source cannot prove PID reuse.
271
311
  }
272
- if (pid === process.pid) ownResidentCacheProcessStartTimeMs = startTimeMs;
273
- return startTimeMs;
312
+ return { startTimeMs, basis: RESIDENT_CACHE_UTC_START_TIME_BASIS };
274
313
  }
275
314
 
276
315
  type ResidentCacheOwnerLiveness = "alive" | "dead" | "unknown";
@@ -376,10 +415,11 @@ function readResidentCacheOwnerSnapshot(instanceDir: string, uid: number): Resid
376
415
  function writeResidentCacheOwnerToken(instanceDir: string, uid: number): void {
377
416
  const nonce = path.basename(instanceDir).slice("i-".length);
378
417
  if (!nonce) throw new ResidentCacheTrustError("instance_nonce_missing", instanceDir);
418
+ const ownStart = residentCacheProcessStart(process.pid);
379
419
  const owner: ResidentCacheOwnerToken = {
380
420
  pid: process.pid,
381
- startTimeMs: residentCacheProcessStartTimeMs(process.pid),
382
- startTimeBasis: RESIDENT_CACHE_START_TIME_BASIS,
421
+ startTimeMs: ownStart.startTimeMs,
422
+ startTimeBasis: ownStart.basis,
383
423
  nonce,
384
424
  createdAt: Date.now(),
385
425
  };
@@ -579,26 +619,31 @@ function residentCacheSweepLimit(value: number | undefined, ceiling: number): nu
579
619
  return Math.max(0, Math.min(Math.floor(value), ceiling));
580
620
  }
581
621
 
582
- function cachedResidentCacheProcessStartTimeMs(pid: number, cache: Map<number, number | null>): number | null {
583
- if (cache.has(pid)) return cache.get(pid) ?? null;
584
- const startTimeMs = residentCacheProcessStartTimeMs(pid);
585
- cache.set(pid, startTimeMs);
586
- return startTimeMs;
622
+ function cachedResidentCacheProcessStart(
623
+ pid: number,
624
+ cache: Map<number, ResidentCacheProcessStart>,
625
+ ): ResidentCacheProcessStart {
626
+ const cached = cache.get(pid);
627
+ if (cached !== undefined) return cached;
628
+ const start = residentCacheProcessStart(pid);
629
+ cache.set(pid, start);
630
+ return start;
587
631
  }
588
632
 
589
633
  function residentCacheOwnerIsStale(
590
634
  owner: ResidentCacheOwnerToken,
591
- startTimeCache: Map<number, number | null>,
635
+ startTimeCache: Map<number, ResidentCacheProcessStart>,
592
636
  ): boolean {
593
637
  const liveness = residentCacheOwnerLiveness(owner.pid);
594
638
  if (liveness === "dead") return true;
595
639
  if (liveness !== "alive" || owner.startTimeMs === null) return false;
596
- // A token from before the UTC pin carries a zone-dependent wall clock. Comparing
597
- // it against an absolute instant would read an ordinary timezone offset as PID
598
- // reuse and reap a live owner, so an unlabelled basis leaves reuse unproven.
599
- if (owner.startTimeBasis !== RESIDENT_CACHE_START_TIME_BASIS) return false;
600
- const currentStartTimeMs = cachedResidentCacheProcessStartTimeMs(owner.pid, startTimeCache);
601
- return currentStartTimeMs !== null && currentStartTimeMs !== owner.startTimeMs;
640
+ const current = cachedResidentCacheProcessStart(owner.pid, startTimeCache);
641
+ // Start times are comparable only on the same basis (and, on Linux, the same
642
+ // boot). A token from an older writer (unlabelled zone-dependent wall clock, or
643
+ // the drifting `ps` render) or another basis leaves reuse unproven, so a live
644
+ // owner is never reaped on a basis mismatch.
645
+ if (owner.startTimeBasis !== current.basis) return false;
646
+ return current.startTimeMs !== null && current.startTimeMs !== owner.startTimeMs;
602
647
  }
603
648
 
604
649
  function reapResidentCacheInstanceDir(
@@ -681,7 +726,7 @@ export async function sweepResidentCacheRoot(root: string, options: ResidentCach
681
726
  // cache state behind.
682
727
  rootDescriptor = openVerifiedResidentCacheDirectory(root, uid);
683
728
  assertResidentCacheDirectoryPathMatchesDescriptor(root, rootDescriptor, uid);
684
- const startTimeCache = new Map<number, number | null>();
729
+ const startTimeCache = new Map<number, ResidentCacheProcessStart>();
685
730
  const entries = fs.opendirSync(root);
686
731
  try {
687
732
  let examined = 0;
@@ -746,7 +791,7 @@ function openVerifiedCacheInstanceDir(root: string, instanceName?: string): stri
746
791
  const staleInstance = readResidentCacheOwnerSnapshot(candidate, uid);
747
792
  if (
748
793
  staleInstance === null ||
749
- !residentCacheOwnerIsStale(staleInstance.owner, new Map<number, number | null>()) ||
794
+ !residentCacheOwnerIsStale(staleInstance.owner, new Map<number, ResidentCacheProcessStart>()) ||
750
795
  !reapResidentCacheInstanceDir(root, rootDescriptor, candidate, staleInstance, uid)
751
796
  ) {
752
797
  throw error;