@gajae-code/coding-agent 0.4.5 → 0.5.1

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 (185) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/types/async/job-manager.d.ts +26 -0
  3. package/dist/types/cli/args.d.ts +1 -0
  4. package/dist/types/cli/list-models.d.ts +6 -0
  5. package/dist/types/commands/gc.d.ts +26 -0
  6. package/dist/types/commands/harness.d.ts +3 -0
  7. package/dist/types/config/file-lock-gc.d.ts +5 -0
  8. package/dist/types/config/file-lock.d.ts +7 -0
  9. package/dist/types/config/model-profile-activation.d.ts +11 -2
  10. package/dist/types/config/model-profiles.d.ts +7 -0
  11. package/dist/types/config/model-registry.d.ts +3 -0
  12. package/dist/types/config/model-resolver.d.ts +2 -0
  13. package/dist/types/config/models-config-schema.d.ts +30 -0
  14. package/dist/types/config/settings-schema.d.ts +4 -3
  15. package/dist/types/coordinator/contract.d.ts +1 -1
  16. package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
  17. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
  18. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
  19. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
  21. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
  22. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
  23. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
  24. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
  25. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
  26. package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
  27. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  28. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
  29. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
  30. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
  31. package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
  32. package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
  33. package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
  34. package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
  35. package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
  36. package/dist/types/gjc-runtime/team-runtime.d.ts +5 -1
  37. package/dist/types/gjc-runtime/tmux-common.d.ts +14 -0
  38. package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
  39. package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
  40. package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
  41. package/dist/types/harness-control-plane/owner.d.ts +8 -1
  42. package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
  43. package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
  44. package/dist/types/harness-control-plane/storage.d.ts +20 -0
  45. package/dist/types/harness-control-plane/types.d.ts +4 -0
  46. package/dist/types/hindsight/mental-models.d.ts +5 -5
  47. package/dist/types/modes/components/hook-selector.d.ts +7 -1
  48. package/dist/types/modes/components/model-selector.d.ts +1 -12
  49. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  50. package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
  51. package/dist/types/modes/rpc/rpc-mode.d.ts +16 -1
  52. package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
  53. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
  54. package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
  55. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
  56. package/dist/types/sdk.d.ts +5 -0
  57. package/dist/types/session/agent-session.d.ts +3 -1
  58. package/dist/types/session/blob-store.d.ts +59 -4
  59. package/dist/types/session/session-manager.d.ts +24 -6
  60. package/dist/types/session/streaming-output.d.ts +3 -2
  61. package/dist/types/session/tool-choice-queue.d.ts +6 -0
  62. package/dist/types/skill-state/workflow-hud.d.ts +14 -0
  63. package/dist/types/task/receipt.d.ts +1 -0
  64. package/dist/types/task/types.d.ts +7 -0
  65. package/dist/types/thinking-metadata.d.ts +16 -0
  66. package/dist/types/thinking.d.ts +3 -12
  67. package/dist/types/tools/ask.d.ts +15 -1
  68. package/dist/types/tools/index.d.ts +2 -0
  69. package/dist/types/tools/resolve.d.ts +0 -10
  70. package/dist/types/tools/subagent.d.ts +6 -0
  71. package/dist/types/utils/tool-choice.d.ts +14 -1
  72. package/package.json +7 -7
  73. package/src/async/job-manager.ts +52 -0
  74. package/src/cli/args.ts +3 -0
  75. package/src/cli/auth-broker-cli.ts +1 -0
  76. package/src/cli/list-models.ts +13 -1
  77. package/src/cli.ts +9 -4
  78. package/src/commands/gc.ts +22 -0
  79. package/src/commands/harness.ts +43 -5
  80. package/src/commands/launch.ts +2 -2
  81. package/src/commands/session.ts +3 -1
  82. package/src/config/file-lock-gc.ts +181 -0
  83. package/src/config/file-lock.ts +14 -0
  84. package/src/config/model-profile-activation.ts +15 -3
  85. package/src/config/model-profiles.ts +264 -56
  86. package/src/config/model-resolver.ts +9 -6
  87. package/src/config/models-config-schema.ts +1 -0
  88. package/src/config/settings-schema.ts +6 -3
  89. package/src/coordinator/contract.ts +1 -0
  90. package/src/coordinator-mcp/server.ts +513 -26
  91. package/src/cursor.ts +16 -2
  92. package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
  93. package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
  94. package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
  95. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
  96. package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
  97. package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
  98. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
  99. package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
  100. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
  101. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
  102. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
  103. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
  104. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
  105. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
  106. package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
  107. package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
  108. package/src/defaults/gjc/skills/team/SKILL.md +3 -2
  109. package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
  110. package/src/defaults/gjc-defaults.ts +7 -0
  111. package/src/defaults/gjc-grok-cli.ts +22 -0
  112. package/src/export/html/index.ts +13 -9
  113. package/src/extensibility/extensions/index.ts +1 -0
  114. package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
  115. package/src/gjc-runtime/deep-interview-recorder.ts +417 -0
  116. package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
  117. package/src/gjc-runtime/deep-interview-state.ts +324 -0
  118. package/src/gjc-runtime/gc-render.ts +70 -0
  119. package/src/gjc-runtime/gc-runtime.ts +403 -0
  120. package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
  121. package/src/gjc-runtime/ralplan-runtime.ts +58 -7
  122. package/src/gjc-runtime/state-renderer.ts +12 -3
  123. package/src/gjc-runtime/state-runtime.ts +46 -29
  124. package/src/gjc-runtime/team-gc.ts +49 -0
  125. package/src/gjc-runtime/team-runtime.ts +211 -8
  126. package/src/gjc-runtime/tmux-common.ts +29 -0
  127. package/src/gjc-runtime/tmux-gc.ts +176 -0
  128. package/src/gjc-runtime/tmux-sessions.ts +68 -12
  129. package/src/gjc-runtime/ultragoal-runtime.ts +517 -41
  130. package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
  131. package/src/gjc-runtime/workflow-manifest.ts +16 -1
  132. package/src/harness-control-plane/gc-adapter.ts +184 -0
  133. package/src/harness-control-plane/owner.ts +89 -27
  134. package/src/harness-control-plane/receipt-spool.ts +128 -0
  135. package/src/harness-control-plane/state-machine.ts +27 -6
  136. package/src/harness-control-plane/storage.ts +93 -0
  137. package/src/harness-control-plane/types.ts +4 -0
  138. package/src/hindsight/mental-models.ts +17 -16
  139. package/src/internal-urls/docs-index.generated.ts +14 -8
  140. package/src/main.ts +7 -2
  141. package/src/modes/components/assistant-message.ts +26 -14
  142. package/src/modes/components/diff.ts +97 -0
  143. package/src/modes/components/hook-selector.ts +19 -0
  144. package/src/modes/components/model-selector.ts +370 -181
  145. package/src/modes/components/status-line/segments.ts +1 -1
  146. package/src/modes/components/tool-execution.ts +30 -13
  147. package/src/modes/controllers/command-controller.ts +25 -6
  148. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  149. package/src/modes/controllers/selector-controller.ts +34 -42
  150. package/src/modes/rpc/rpc-client.ts +3 -2
  151. package/src/modes/rpc/rpc-mode.ts +187 -39
  152. package/src/modes/rpc/rpc-types.ts +5 -2
  153. package/src/modes/shared/agent-wire/command-dispatch.ts +279 -257
  154. package/src/modes/shared/agent-wire/command-validation.ts +11 -0
  155. package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
  156. package/src/modes/shared/agent-wire/session-registry.ts +109 -0
  157. package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
  158. package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
  159. package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
  160. package/src/sdk.ts +46 -5
  161. package/src/secrets/obfuscator.ts +102 -27
  162. package/src/session/agent-session.ts +179 -25
  163. package/src/session/blob-store.ts +148 -6
  164. package/src/session/session-manager.ts +311 -60
  165. package/src/session/streaming-output.ts +185 -122
  166. package/src/session/tool-choice-queue.ts +23 -0
  167. package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
  168. package/src/skill-state/workflow-hud.ts +106 -10
  169. package/src/slash-commands/builtin-registry.ts +3 -2
  170. package/src/task/executor.ts +78 -6
  171. package/src/task/receipt.ts +5 -0
  172. package/src/task/render.ts +21 -1
  173. package/src/task/types.ts +8 -0
  174. package/src/thinking-metadata.ts +51 -0
  175. package/src/thinking.ts +26 -46
  176. package/src/tools/ask.ts +56 -1
  177. package/src/tools/bash.ts +1 -1
  178. package/src/tools/index.ts +2 -0
  179. package/src/tools/job.ts +3 -2
  180. package/src/tools/monitor.ts +36 -1
  181. package/src/tools/resolve.ts +93 -18
  182. package/src/tools/subagent-render.ts +9 -0
  183. package/src/tools/subagent.ts +26 -2
  184. package/src/utils/edit-mode.ts +1 -1
  185. package/src/utils/tool-choice.ts +45 -16
@@ -244,7 +244,7 @@ import { parseCommandArgs } from "../utils/command-args";
244
244
  import { type EditMode, resolveEditMode } from "../utils/edit-mode";
245
245
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
246
246
  import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
247
- import { buildNamedToolChoice } from "../utils/tool-choice";
247
+ import { buildNamedToolChoice, buildNamedToolChoiceResult } from "../utils/tool-choice";
248
248
  import type { AuthStorage } from "./auth-storage";
249
249
  import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
250
250
  import {
@@ -322,7 +322,10 @@ function isUnderProjectGjc(cwd: string, targetPath: string): boolean {
322
322
 
323
323
  /** Listener function for agent session events */
324
324
  export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
325
- export type AsyncJobSnapshotItem = Pick<AsyncJob, "id" | "type" | "status" | "label" | "startTime" | "metadata">;
325
+ export type AsyncJobSnapshotItem = Pick<
326
+ AsyncJob,
327
+ "id" | "type" | "status" | "label" | "startTime" | "endTime" | "metadata"
328
+ >;
326
329
 
327
330
  export interface AsyncJobSnapshot {
328
331
  running: AsyncJobSnapshotItem[];
@@ -839,6 +842,8 @@ export type BeforeAgentStartInternalMessage = Pick<
839
842
  "customType" | "content" | "display" | "details" | "attribution"
840
843
  >;
841
844
 
845
+ type ProviderReplaySourceCacheEntry = { source: string; hash: bigint };
846
+
842
847
  /**
843
848
  * Internal (first-party, non-user-hook) contributor invoked at the active
844
849
  * before-agent-start point alongside the extension runner. Returns an optional
@@ -863,6 +868,7 @@ export class AgentSession {
863
868
 
864
869
  #scopedModels: ScopedModelSelection[];
865
870
  #thinkingLevel: ThinkingLevel | undefined;
871
+ #activeModelProfile: string | undefined;
866
872
  #promptTemplates: PromptTemplate[];
867
873
  #slashCommands: FileSlashCommand[];
868
874
 
@@ -900,6 +906,7 @@ export class AgentSession {
900
906
  // Compaction state
901
907
  #compactionAbortController: AbortController | undefined = undefined;
902
908
  #autoCompactionAbortController: AbortController | undefined = undefined;
909
+ #prePromptContextCheckPromise: Promise<void> | undefined = undefined;
903
910
 
904
911
  // Branch summarization state
905
912
  #branchSummaryAbortController: AbortController | undefined = undefined;
@@ -1056,6 +1063,7 @@ export class AgentSession {
1056
1063
  #pendingAgentEndEmit: AgentSessionEvent | undefined;
1057
1064
  #obfuscator: SecretObfuscator | undefined;
1058
1065
  #checkpointState: CheckpointState | undefined = undefined;
1066
+ #providerReplaySourceCache = new WeakMap<AgentMessage, ProviderReplaySourceCacheEntry>();
1059
1067
  #pendingRewindReport: string | undefined = undefined;
1060
1068
  #lastSuccessfulYieldToolCallId: string | undefined = undefined;
1061
1069
  #promptGeneration = 0;
@@ -1419,7 +1427,7 @@ export class AgentSession {
1419
1427
  recordSkip("unsupported-role");
1420
1428
  return undefined;
1421
1429
  }
1422
- const cloned = structuredClone(message) as Message;
1430
+ const cloned = cloneJsonValueForForkSeed(message) as Message;
1423
1431
  if ("providerPayload" in cloned) {
1424
1432
  delete (cloned as { providerPayload?: unknown }).providerPayload;
1425
1433
  }
@@ -1466,7 +1474,7 @@ export class AgentSession {
1466
1474
  }
1467
1475
  return {
1468
1476
  messages,
1469
- agentMessages: messages.map(message => structuredClone(message) as AgentMessage),
1477
+ agentMessages: messages.map(message => cloneJsonValueForForkSeed(message) as AgentMessage),
1470
1478
  metadata: {
1471
1479
  sourceSessionId: this.sessionId,
1472
1480
  parentMessageCount: providerMessages.length,
@@ -1559,6 +1567,7 @@ export class AgentSession {
1559
1567
  status: job.status,
1560
1568
  label: job.label,
1561
1569
  startTime: job.startTime,
1570
+ endTime: job.endTime,
1562
1571
  metadata: job.metadata,
1563
1572
  }));
1564
1573
  const recent = manager.getRecentJobs(options?.recentLimit ?? 5, ownerFilter).map(job => ({
@@ -1567,6 +1576,7 @@ export class AgentSession {
1567
1576
  status: job.status,
1568
1577
  label: job.label,
1569
1578
  startTime: job.startTime,
1579
+ endTime: job.endTime,
1570
1580
  metadata: job.metadata,
1571
1581
  }));
1572
1582
  const delivery = manager.getDeliveryState(ownerFilter);
@@ -4588,7 +4598,7 @@ export class AgentSession {
4588
4598
  : { role: "user" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() };
4589
4599
  await this.refreshGjcSubskillTools();
4590
4600
 
4591
- if (eagerTodoPrelude) {
4601
+ if (eagerTodoPrelude?.toolChoice) {
4592
4602
  this.#toolChoiceQueue.pushOnce(eagerTodoPrelude.toolChoice, {
4593
4603
  label: "eager-todo",
4594
4604
  });
@@ -4749,6 +4759,13 @@ export class AgentSession {
4749
4759
  if (lastAssistant && !options?.skipCompactionCheck) {
4750
4760
  await this.#checkCompaction(lastAssistant, false);
4751
4761
  }
4762
+ if (!options?.skipCompactionCheck) {
4763
+ await this.#checkEstimatedContextBeforePrompt([
4764
+ ...(options?.prependMessages ?? []),
4765
+ message,
4766
+ ...this.#pendingNextTurnMessages,
4767
+ ]);
4768
+ }
4752
4769
 
4753
4770
  // Build messages array (session context, eager todo prelude, then active prompt message)
4754
4771
  const messages: AgentMessage[] = [];
@@ -5212,7 +5229,9 @@ export class AgentSession {
5212
5229
  }
5213
5230
  await this.#syncSkillPromptActiveStateSafely(appMessage, true);
5214
5231
  try {
5215
- await this.agent.prompt(appMessage);
5232
+ await this.#promptWithMessage(appMessage, this.#getCustomMessageTextContent(appMessage), {
5233
+ skipPostPromptRecoveryWait: true,
5234
+ });
5216
5235
  } finally {
5217
5236
  await this.#syncSkillPromptActiveStateSafely(appMessage, false);
5218
5237
  }
@@ -5236,7 +5255,9 @@ export class AgentSession {
5236
5255
  }
5237
5256
  await this.#syncSkillPromptActiveStateSafely(appMessage, true);
5238
5257
  try {
5239
- await this.agent.prompt(appMessage);
5258
+ await this.#promptWithMessage(appMessage, this.#getCustomMessageTextContent(appMessage), {
5259
+ skipPostPromptRecoveryWait: true,
5260
+ });
5240
5261
  } finally {
5241
5262
  await this.#syncSkillPromptActiveStateSafely(appMessage, false);
5242
5263
  }
@@ -5728,6 +5749,14 @@ export class AgentSession {
5728
5749
  await this.#syncEditToolModeAfterModelChange(previousEditMode);
5729
5750
  }
5730
5751
 
5752
+ setActiveModelProfile(name: string | undefined): void {
5753
+ this.#activeModelProfile = name;
5754
+ }
5755
+
5756
+ getActiveModelProfile(): string | undefined {
5757
+ return this.#activeModelProfile;
5758
+ }
5759
+
5731
5760
  /**
5732
5761
  * Set model temporarily (for this session only).
5733
5762
  * Validates API key, saves to session log but NOT to settings.
@@ -6530,6 +6559,47 @@ export class AgentSession {
6530
6559
  }
6531
6560
  }
6532
6561
  }
6562
+
6563
+ async #checkEstimatedContextBeforePrompt(pendingMessages: readonly AgentMessage[] = []): Promise<void> {
6564
+ if (this.#prePromptContextCheckPromise) {
6565
+ await this.#prePromptContextCheckPromise;
6566
+ }
6567
+
6568
+ const checkPromise = this.#checkEstimatedContextBeforePromptOnce(pendingMessages);
6569
+ this.#prePromptContextCheckPromise = checkPromise;
6570
+ try {
6571
+ await checkPromise;
6572
+ } finally {
6573
+ if (this.#prePromptContextCheckPromise === checkPromise) {
6574
+ this.#prePromptContextCheckPromise = undefined;
6575
+ }
6576
+ }
6577
+ }
6578
+
6579
+ async #checkEstimatedContextBeforePromptOnce(pendingMessages: readonly AgentMessage[]): Promise<void> {
6580
+ const model = this.model;
6581
+ if (!model) return;
6582
+ const contextWindow = model.contextWindow ?? 0;
6583
+ if (contextWindow <= 0) return;
6584
+ const compactionSettings = this.settings.getGroup("compaction");
6585
+ if (!compactionSettings.enabled || compactionSettings.strategy === "off") return;
6586
+
6587
+ let contextTokens = this.#estimateContextTokensForCompaction(pendingMessages).tokens;
6588
+ const maxOutputTokens = model.maxTokens ?? 0;
6589
+ if (!shouldCompact(contextTokens, contextWindow, compactionSettings, maxOutputTokens)) return;
6590
+
6591
+ const pruneResult = await this.#pruneToolOutputs();
6592
+ if (pruneResult) {
6593
+ contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
6594
+ }
6595
+ if (shouldCompact(contextTokens, contextWindow, compactionSettings, maxOutputTokens)) {
6596
+ await this.#runAutoCompaction("threshold", false, false, {
6597
+ continueAfterMaintenance: false,
6598
+ deferHandoffMaintenance: false,
6599
+ });
6600
+ }
6601
+ }
6602
+
6533
6603
  #assistantEndedWithSuccessfulYield(assistantMessage: AssistantMessage): boolean {
6534
6604
  const toolCallId = this.#lastSuccessfulYieldToolCallId;
6535
6605
  if (!toolCallId) return false;
@@ -6627,7 +6697,7 @@ export class AgentSession {
6627
6697
  });
6628
6698
  }
6629
6699
 
6630
- #createEagerTodoPrelude(promptText: string): { message: AgentMessage; toolChoice: ToolChoice } | undefined {
6700
+ #createEagerTodoPrelude(promptText: string): { message: AgentMessage; toolChoice?: ToolChoice } | undefined {
6631
6701
  const eagerTodosEnabled = this.settings.get("todo.eager");
6632
6702
  const todosEnabled = this.settings.get("todo.enabled");
6633
6703
  if (!eagerTodosEnabled || !todosEnabled) {
@@ -6661,13 +6731,15 @@ export class AgentSession {
6661
6731
  return undefined;
6662
6732
  }
6663
6733
 
6664
- const todoWriteToolChoice = buildNamedToolChoice("todo_write", this.model);
6665
- if (!todoWriteToolChoice) {
6666
- logger.warn("Eager todo enforcement skipped because the current model does not support forcing todo_write", {
6734
+ const todoWriteToolChoiceResult = buildNamedToolChoiceResult("todo_write", this.model);
6735
+ const todoWriteToolChoice = todoWriteToolChoiceResult.exactNamed ? todoWriteToolChoiceResult.choice : undefined;
6736
+ if (!todoWriteToolChoiceResult.exactNamed) {
6737
+ logger.debug("Eager todo enforcement degraded; sending reminder without forced tool choice", {
6667
6738
  modelApi: this.model?.api,
6668
6739
  modelId: this.model?.id,
6740
+ resolvedLevel: todoWriteToolChoiceResult.resolved?.resolvedLevel,
6741
+ reason: todoWriteToolChoiceResult.resolved?.reason,
6669
6742
  });
6670
- return undefined;
6671
6743
  }
6672
6744
 
6673
6745
  const eagerTodoReminder = prompt.render(eagerTodoPrompt);
@@ -7049,11 +7121,37 @@ export class AgentSession {
7049
7121
  }
7050
7122
  }
7051
7123
 
7124
+ #getProviderReplaySource(message: AgentMessage): ProviderReplaySourceCacheEntry {
7125
+ const cached = this.#providerReplaySourceCache.get(message);
7126
+ if (cached) return cached;
7127
+ const source = JSON.stringify(this.#normalizeSessionMessageForProviderReplay(message));
7128
+ const hash = this.#hashProviderReplaySource(source);
7129
+ const entry = { source, hash };
7130
+ this.#providerReplaySourceCache.set(message, entry);
7131
+ return entry;
7132
+ }
7133
+
7134
+ #hashProviderReplaySource(source: string): bigint {
7135
+ return Bun.hash.xxHash64(source);
7136
+ }
7137
+
7052
7138
  #didSessionMessagesChange(previousMessages: AgentMessage[], nextMessages: AgentMessage[]): boolean {
7053
- return (
7054
- JSON.stringify(previousMessages.map(message => this.#normalizeSessionMessageForProviderReplay(message))) !==
7055
- JSON.stringify(nextMessages.map(message => this.#normalizeSessionMessageForProviderReplay(message)))
7056
- );
7139
+ if (previousMessages.length !== nextMessages.length) return true;
7140
+
7141
+ const previousSources: ProviderReplaySourceCacheEntry[] = [];
7142
+ const nextSources: ProviderReplaySourceCacheEntry[] = [];
7143
+ for (let i = 0; i < previousMessages.length; i++) {
7144
+ const previous = this.#getProviderReplaySource(previousMessages[i]!);
7145
+ const next = this.#getProviderReplaySource(nextMessages[i]!);
7146
+ if (previous.hash !== next.hash) return true;
7147
+ previousSources.push(previous);
7148
+ nextSources.push(next);
7149
+ }
7150
+
7151
+ for (let i = 0; i < previousSources.length; i++) {
7152
+ if (previousSources[i]!.source !== nextSources[i]!.source) return true;
7153
+ }
7154
+ return false;
7057
7155
  }
7058
7156
 
7059
7157
  #getModelKey(model: Model): string {
@@ -7258,17 +7356,24 @@ export class AgentSession {
7258
7356
  reason: "overflow" | "threshold" | "idle",
7259
7357
  willRetry: boolean,
7260
7358
  deferred = false,
7359
+ options?: { continueAfterMaintenance?: boolean; deferHandoffMaintenance?: boolean },
7261
7360
  ): Promise<void> {
7262
7361
  const compactionSettings = this.settings.getGroup("compaction");
7263
7362
  if (compactionSettings.strategy === "off") return;
7264
7363
  if (reason !== "idle" && !compactionSettings.enabled) return;
7265
7364
  const generation = this.#promptGeneration;
7266
- if (!deferred && reason !== "overflow" && reason !== "idle" && compactionSettings.strategy === "handoff") {
7365
+ if (
7366
+ options?.deferHandoffMaintenance !== false &&
7367
+ !deferred &&
7368
+ reason !== "overflow" &&
7369
+ reason !== "idle" &&
7370
+ compactionSettings.strategy === "handoff"
7371
+ ) {
7267
7372
  this.#schedulePostPromptTask(
7268
7373
  async signal => {
7269
7374
  await Promise.resolve();
7270
7375
  if (signal.aborted) return;
7271
- await this.#runAutoCompaction(reason, willRetry, true);
7376
+ await this.#runAutoCompaction(reason, willRetry, true, options);
7272
7377
  },
7273
7378
  { generation },
7274
7379
  );
@@ -7277,6 +7382,7 @@ export class AgentSession {
7277
7382
 
7278
7383
  let action: "context-full" | "handoff" =
7279
7384
  compactionSettings.strategy === "handoff" && reason !== "overflow" ? "handoff" : "context-full";
7385
+ const continueAfterMaintenance = options?.continueAfterMaintenance !== false;
7280
7386
  await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
7281
7387
  // Abort any older auto-compaction before installing this run's controller.
7282
7388
  this.#autoCompactionAbortController?.abort();
@@ -7316,7 +7422,12 @@ export class AgentSession {
7316
7422
  aborted: false,
7317
7423
  willRetry: false,
7318
7424
  });
7319
- if (!autoCompactionSignal.aborted && reason !== "idle" && compactionSettings.autoContinue !== false) {
7425
+ if (
7426
+ continueAfterMaintenance &&
7427
+ !autoCompactionSignal.aborted &&
7428
+ reason !== "idle" &&
7429
+ compactionSettings.autoContinue !== false
7430
+ ) {
7320
7431
  this.#scheduleAutoContinuePrompt(generation);
7321
7432
  }
7322
7433
  return;
@@ -7378,7 +7489,7 @@ export class AgentSession {
7378
7489
  stopReason: tail?.stopReason,
7379
7490
  });
7380
7491
  }
7381
- } else if (reason !== "idle" && this.agent.hasQueuedMessages()) {
7492
+ } else if (continueAfterMaintenance && reason !== "idle" && this.agent.hasQueuedMessages()) {
7382
7493
  this.#scheduleAgentContinue({
7383
7494
  delayMs: 100,
7384
7495
  generation,
@@ -7386,7 +7497,7 @@ export class AgentSession {
7386
7497
  onSkip: skipReason => this.#logCompactionContinuationSkipped("queued_continue", skipReason),
7387
7498
  onError: error => this.#logCompactionContinuationError("queued_continue", error),
7388
7499
  });
7389
- } else if (reason !== "idle" && compactionSettings.autoContinue !== false) {
7500
+ } else if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
7390
7501
  this.#scheduleAutoContinuePrompt(generation);
7391
7502
  }
7392
7503
  return;
@@ -7607,7 +7718,7 @@ export class AgentSession {
7607
7718
  onError: error => this.#logCompactionContinuationError("overflow_retry", error),
7608
7719
  });
7609
7720
  }
7610
- } else if (reason !== "idle" && this.agent.hasQueuedMessages()) {
7721
+ } else if (continueAfterMaintenance && reason !== "idle" && this.agent.hasQueuedMessages()) {
7611
7722
  // Auto-compaction can complete while follow-up/steering/custom messages are waiting.
7612
7723
  // Kick the loop so queued messages are actually delivered.
7613
7724
  this.#scheduleAgentContinue({
@@ -7617,7 +7728,7 @@ export class AgentSession {
7617
7728
  onSkip: reason => this.#logCompactionContinuationSkipped("queued_continue", reason),
7618
7729
  onError: error => this.#logCompactionContinuationError("queued_continue", error),
7619
7730
  });
7620
- } else if (reason !== "idle" && compactionSettings.autoContinue !== false) {
7731
+ } else if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
7621
7732
  this.#scheduleAutoContinuePrompt(generation);
7622
7733
  }
7623
7734
  } catch (error) {
@@ -9516,6 +9627,21 @@ export class AgentSession {
9516
9627
  */
9517
9628
  #estimateContextTokens(): {
9518
9629
  tokens: number;
9630
+ } {
9631
+ return this.#estimateContextTokensWith(message => this.#estimateMessageDisplayTokens(message));
9632
+ }
9633
+
9634
+ #estimateContextTokensForCompaction(pendingMessages: readonly AgentMessage[]): {
9635
+ tokens: number;
9636
+ } {
9637
+ const estimate = this.#estimateContextTokensWith(message => this.#estimateMessageNativeContextTokens(message));
9638
+ return {
9639
+ tokens: estimate.tokens + this.#estimateMessagesNativeContextTokens(pendingMessages),
9640
+ };
9641
+ }
9642
+
9643
+ #estimateContextTokensWith(estimateMessage: (message: AgentMessage) => number): {
9644
+ tokens: number;
9519
9645
  } {
9520
9646
  const messages = this.messages;
9521
9647
 
@@ -9538,7 +9664,7 @@ export class AgentSession {
9538
9664
  // No usage data - estimate all messages
9539
9665
  let estimated = 0;
9540
9666
  for (const message of messages) {
9541
- estimated += estimateMessageTokensHeuristic(message);
9667
+ estimated += estimateMessage(message);
9542
9668
  }
9543
9669
  return {
9544
9670
  tokens: estimated,
@@ -9548,7 +9674,7 @@ export class AgentSession {
9548
9674
  const usageTokens = calculatePromptTokens(lastUsage);
9549
9675
  let trailingTokens = 0;
9550
9676
  for (let i = lastUsageIndex + 1; i < messages.length; i++) {
9551
- trailingTokens += estimateMessageTokensHeuristic(messages[i]);
9677
+ trailingTokens += estimateMessage(messages[i]);
9552
9678
  }
9553
9679
 
9554
9680
  return {
@@ -9556,6 +9682,30 @@ export class AgentSession {
9556
9682
  };
9557
9683
  }
9558
9684
 
9685
+ #estimateMessagesNativeContextTokens(messages: readonly AgentMessage[]): number {
9686
+ let tokens = 0;
9687
+ for (const message of messages) {
9688
+ tokens += this.#estimateMessageNativeContextTokens(message);
9689
+ }
9690
+ return tokens;
9691
+ }
9692
+
9693
+ #estimateMessageDisplayTokens(message: AgentMessage): number {
9694
+ let tokens = 0;
9695
+ for (const llmMessage of convertToLlm([message])) {
9696
+ tokens += estimateMessageTokensHeuristic(llmMessage);
9697
+ }
9698
+ return tokens;
9699
+ }
9700
+
9701
+ #estimateMessageNativeContextTokens(message: AgentMessage): number {
9702
+ let tokens = 0;
9703
+ for (const llmMessage of convertToLlm([message])) {
9704
+ tokens += estimateTokens(llmMessage);
9705
+ }
9706
+ return tokens;
9707
+ }
9708
+
9559
9709
  /**
9560
9710
  * Export session to HTML.
9561
9711
  * @param outputPath Optional output path (defaults to session directory)
@@ -9768,3 +9918,7 @@ export class AgentSession {
9768
9918
  return this.#extensionRunner;
9769
9919
  }
9770
9920
  }
9921
+
9922
+ function cloneJsonValueForForkSeed<T>(value: T): T {
9923
+ return JSON.parse(JSON.stringify(value)) as T;
9924
+ }
@@ -95,6 +95,77 @@ export class BlobStore {
95
95
  }
96
96
  }
97
97
 
98
+ export class EphemeralBlobStore extends BlobStore {
99
+ /**
100
+ * Bounded LRU byte budget for the in-memory buffer cache. Keeps recent
101
+ * resident blobs hot for rematerialization after the weak materialized
102
+ * view is collected, without re-pinning the whole session in RAM.
103
+ */
104
+ static readonly #BUFFER_CACHE_MAX_BYTES = 8 * 1024 * 1024;
105
+
106
+ #bufferCache = new Map<string, Buffer>();
107
+ #bufferCacheBytes = 0;
108
+
109
+ constructor(dir: string) {
110
+ super(dir);
111
+ fs.rmSync(dir, { recursive: true, force: true });
112
+ fs.mkdirSync(dir, { recursive: true });
113
+ }
114
+
115
+ #cachePut(hash: string, data: Buffer): void {
116
+ const existing = this.#bufferCache.get(hash);
117
+ if (existing) {
118
+ this.#bufferCache.delete(hash);
119
+ this.#bufferCacheBytes -= existing.byteLength;
120
+ }
121
+ if (data.byteLength > EphemeralBlobStore.#BUFFER_CACHE_MAX_BYTES) return;
122
+ this.#bufferCache.set(hash, data);
123
+ this.#bufferCacheBytes += data.byteLength;
124
+ for (const [oldHash, oldData] of this.#bufferCache) {
125
+ if (this.#bufferCacheBytes <= EphemeralBlobStore.#BUFFER_CACHE_MAX_BYTES) break;
126
+ this.#bufferCache.delete(oldHash);
127
+ this.#bufferCacheBytes -= oldData.byteLength;
128
+ }
129
+ }
130
+
131
+ putSync(data: Buffer): BlobPutResult {
132
+ const result = super.putSync(data);
133
+ this.#cachePut(result.hash, Buffer.from(data));
134
+ return result;
135
+ }
136
+
137
+ getSync(hash: string): Buffer | null {
138
+ const cached = this.#bufferCache.get(hash);
139
+ if (cached) {
140
+ const blobPath = path.join(this.dir, hash);
141
+ if (fs.existsSync(blobPath)) {
142
+ // Refresh LRU recency on hit.
143
+ this.#bufferCache.delete(hash);
144
+ this.#bufferCache.set(hash, cached);
145
+ return Buffer.from(cached);
146
+ }
147
+ this.#bufferCache.delete(hash);
148
+ this.#bufferCacheBytes -= cached.byteLength;
149
+ }
150
+ const data = super.getSync(hash);
151
+ if (data) this.#cachePut(hash, Buffer.from(data));
152
+ return data;
153
+ }
154
+
155
+ clear(): void {
156
+ this.#bufferCache.clear();
157
+ this.#bufferCacheBytes = 0;
158
+ fs.rmSync(this.dir, { recursive: true, force: true });
159
+ fs.mkdirSync(this.dir, { recursive: true });
160
+ }
161
+
162
+ dispose(): void {
163
+ this.#bufferCache.clear();
164
+ this.#bufferCacheBytes = 0;
165
+ fs.rmSync(this.dir, { recursive: true, force: true });
166
+ }
167
+ }
168
+
98
169
  export class MemoryBlobStore extends BlobStore {
99
170
  #blobs = new Map<string, Buffer>();
100
171
 
@@ -132,6 +203,18 @@ export class MemoryBlobStore extends BlobStore {
132
203
  }
133
204
  }
134
205
 
206
+ export class ResidentBlobMissingError extends Error {
207
+ constructor(
208
+ readonly hash: string,
209
+ readonly kind: "text" | "imageUrl" | "imageData",
210
+ readonly sessionId?: string,
211
+ readonly sessionFile?: string,
212
+ ) {
213
+ super(`Missing resident ${kind} blob: ${hash}`);
214
+ this.name = "ResidentBlobMissingError";
215
+ }
216
+ }
217
+
135
218
  /** Check if a data string is a blob reference. */
136
219
  export function isBlobRef(data: string): boolean {
137
220
  return data.startsWith(BLOB_PREFIX);
@@ -184,7 +267,13 @@ export function externalizeImageDataSync(blobStore: BlobStore, base64Data: strin
184
267
  /**
185
268
  * Resolve an externalized provider image data URL back to its original string.
186
269
  * If the data is not a blob reference, returns it unchanged.
187
- * If the blob is missing, logs a warning and returns the reference as-is.
270
+ *
271
+ * LEGACY PERSISTED-IMAGE COMPATIBILITY BOUNDARY: when the persisted blob is missing
272
+ * (e.g. resuming an old session whose image blob was pruned), this warns and returns
273
+ * the reference as-is rather than throwing, so legacy resume degrades gracefully.
274
+ * New resident byte-sensitive TEXT uses the fail-closed path instead
275
+ * (`resolveTextBlobSync` -> `ResidentBlobMissingError`). Do NOT route new byte-sensitive
276
+ * resident data through this warn-and-return path.
188
277
  */
189
278
  export async function resolveImageDataUrl(blobStore: BlobStore, data: string): Promise<string> {
190
279
  const hash = parseBlobRef(data);
@@ -201,7 +290,11 @@ export async function resolveImageDataUrl(blobStore: BlobStore, data: string): P
201
290
  /**
202
291
  * Resolve a blob reference back to base64 data.
203
292
  * If the data is not a blob reference, returns it unchanged.
204
- * If the blob is missing, logs a warning and returns a placeholder.
293
+ *
294
+ * LEGACY PERSISTED-IMAGE COMPATIBILITY BOUNDARY: when the blob is missing this warns
295
+ * and returns the reference as-is (downstream sees an invalid base64 ref but does not
296
+ * crash), preserving legacy-session resume. Byte-sensitive resident TEXT is fail-closed
297
+ * via `resolveTextBlobSync`; do NOT route new byte-sensitive resident data here.
205
298
  */
206
299
  export async function resolveImageData(blobStore: BlobStore, data: string): Promise<string> {
207
300
  const hash = parseBlobRef(data);
@@ -239,14 +332,63 @@ export function resolveImageDataSync(blobStore: BlobStore, data: string): string
239
332
  return buffer.toString("base64");
240
333
  }
241
334
 
242
- /** Synchronously resolve a blob reference back to utf8 text. */
243
- export function resolveTextBlobSync(blobStore: BlobStore, data: string): string {
335
+ /**
336
+ * Synchronously resolve a blob reference back to utf8 text.
337
+ *
338
+ * FAIL-CLOSED byte-sensitive path: a missing resident blob throws
339
+ * `ResidentBlobMissingError` rather than degrading, so a missing resident text blob can
340
+ * never silently leak a `blob:sha256:` ref into provider payloads, UI, or exports.
341
+ * (Contrast the legacy persisted-image warn-and-return resolvers above.)
342
+ */
343
+ export function resolveTextBlobSync(
344
+ blobStore: BlobStore,
345
+ data: string,
346
+ context?: { kind?: "text"; sessionId?: string; sessionFile?: string },
347
+ ): string {
244
348
  const hash = parseBlobRef(data);
245
349
  if (!hash) return data;
246
350
  const buffer = blobStore.getSync(hash);
247
351
  if (!buffer) {
248
- logger.warn("Blob not found for text reference", { hash });
249
- return data;
352
+ throw new ResidentBlobMissingError(hash, context?.kind ?? "text", context?.sessionId, context?.sessionFile);
250
353
  }
251
354
  return buffer.toString("utf8");
252
355
  }
356
+
357
+ /**
358
+ * FAIL-CLOSED resident variant of {@link resolveImageDataUrlSync}: a missing resident
359
+ * image-data-url blob throws `ResidentBlobMissingError` ("imageUrl") instead of warn-returning,
360
+ * so resident byte-sensitive provider image data can never leak a `blob:sha256:` ref into
361
+ * materialized entries, context, or provider payloads. The warn-and-return `resolveImageDataUrl*`
362
+ * resolvers remain ONLY for legacy persisted-image resume.
363
+ */
364
+ export function resolveResidentImageDataUrlSync(
365
+ blobStore: BlobStore,
366
+ data: string,
367
+ context?: { sessionId?: string; sessionFile?: string },
368
+ ): string {
369
+ const hash = parseBlobRef(data);
370
+ if (!hash) return data;
371
+ const buffer = blobStore.getSync(hash);
372
+ if (!buffer) {
373
+ throw new ResidentBlobMissingError(hash, "imageUrl", context?.sessionId, context?.sessionFile);
374
+ }
375
+ return buffer.toString("utf8");
376
+ }
377
+
378
+ /**
379
+ * FAIL-CLOSED resident variant of {@link resolveImageDataSync}: a missing resident image blob
380
+ * throws `ResidentBlobMissingError` ("imageData") instead of warn-returning a placeholder.
381
+ */
382
+ export function resolveResidentImageDataSync(
383
+ blobStore: BlobStore,
384
+ data: string,
385
+ context?: { sessionId?: string; sessionFile?: string },
386
+ ): string {
387
+ const hash = parseBlobRef(data);
388
+ if (!hash) return data;
389
+ const buffer = blobStore.getSync(hash);
390
+ if (!buffer) {
391
+ throw new ResidentBlobMissingError(hash, "imageData", context?.sessionId, context?.sessionFile);
392
+ }
393
+ return buffer.toString("base64");
394
+ }