@gajae-code/coding-agent 0.5.0 → 0.5.2

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 (194) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +1 -1
  3. package/dist/types/async/job-manager.d.ts +26 -0
  4. package/dist/types/cli/args.d.ts +1 -0
  5. package/dist/types/cli/list-models.d.ts +6 -0
  6. package/dist/types/cli/setup-cli.d.ts +8 -1
  7. package/dist/types/commands/gc.d.ts +26 -0
  8. package/dist/types/commands/setup.d.ts +7 -0
  9. package/dist/types/config/file-lock-gc.d.ts +5 -0
  10. package/dist/types/config/file-lock.d.ts +29 -0
  11. package/dist/types/config/model-registry.d.ts +4 -0
  12. package/dist/types/config/models-config-schema.d.ts +5 -0
  13. package/dist/types/config/settings-schema.d.ts +62 -0
  14. package/dist/types/coordinator/contract.d.ts +1 -1
  15. package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
  16. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
  17. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
  18. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
  19. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
  21. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
  22. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
  23. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
  24. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
  25. package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
  26. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  27. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
  28. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
  29. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
  30. package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
  31. package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
  32. package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
  33. package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
  34. package/dist/types/gjc-runtime/state-writer.d.ts +64 -2
  35. package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
  36. package/dist/types/gjc-runtime/team-runtime.d.ts +5 -0
  37. package/dist/types/gjc-runtime/tmux-common.d.ts +11 -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/gjc-runtime/ultragoal-guard.d.ts +10 -0
  41. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
  42. package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
  43. package/dist/types/harness-control-plane/owner.d.ts +7 -0
  44. package/dist/types/harness-control-plane/storage.d.ts +20 -0
  45. package/dist/types/modes/components/hook-selector.d.ts +7 -1
  46. package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
  47. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  48. package/dist/types/modes/interactive-mode.d.ts +1 -1
  49. package/dist/types/modes/rpc/rpc-mode.d.ts +72 -2
  50. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
  51. package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
  52. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
  53. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
  54. package/dist/types/modes/theme/defaults/index.d.ts +302 -0
  55. package/dist/types/modes/theme/theme.d.ts +1 -0
  56. package/dist/types/modes/types.d.ts +1 -1
  57. package/dist/types/session/agent-session.d.ts +1 -1
  58. package/dist/types/session/blob-store.d.ts +39 -3
  59. package/dist/types/session/history-storage.d.ts +2 -2
  60. package/dist/types/session/session-manager.d.ts +10 -1
  61. package/dist/types/setup/credential-import.d.ts +79 -0
  62. package/dist/types/skill-state/workflow-hud.d.ts +14 -0
  63. package/dist/types/task/executor.d.ts +1 -0
  64. package/dist/types/task/render.d.ts +1 -1
  65. package/dist/types/tools/ask.d.ts +15 -1
  66. package/dist/types/tools/subagent-render.d.ts +7 -1
  67. package/dist/types/tools/subagent.d.ts +27 -0
  68. package/dist/types/tools/ultragoal-ask-guard.d.ts +5 -0
  69. package/dist/types/web/search/index.d.ts +4 -4
  70. package/dist/types/web/search/provider.d.ts +16 -20
  71. package/dist/types/web/search/providers/base.d.ts +2 -1
  72. package/dist/types/web/search/providers/openai-compatible.d.ts +9 -0
  73. package/dist/types/web/search/types.d.ts +14 -2
  74. package/package.json +7 -7
  75. package/scripts/build-binary.ts +7 -0
  76. package/src/async/job-manager.ts +52 -0
  77. package/src/cli/args.ts +5 -0
  78. package/src/cli/auth-broker-cli.ts +1 -0
  79. package/src/cli/fast-help.ts +2 -0
  80. package/src/cli/list-models.ts +13 -1
  81. package/src/cli/setup-cli.ts +138 -3
  82. package/src/cli.ts +1 -0
  83. package/src/commands/gc.ts +22 -0
  84. package/src/commands/harness.ts +7 -3
  85. package/src/commands/setup.ts +5 -1
  86. package/src/commands/ultragoal.ts +3 -1
  87. package/src/config/file-lock-gc.ts +193 -0
  88. package/src/config/file-lock.ts +66 -10
  89. package/src/config/model-profile-activation.ts +15 -3
  90. package/src/config/model-profiles.ts +39 -30
  91. package/src/config/model-registry.ts +21 -1
  92. package/src/config/models-config-schema.ts +1 -0
  93. package/src/config/settings-schema.ts +62 -0
  94. package/src/coordinator/contract.ts +1 -0
  95. package/src/coordinator-mcp/server.ts +459 -3
  96. package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
  97. package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
  98. package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
  99. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
  100. package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
  101. package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
  102. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
  103. package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
  104. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
  105. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
  106. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
  107. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
  108. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
  109. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
  110. package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
  111. package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
  112. package/src/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
  113. package/src/defaults/gjc-defaults.ts +7 -0
  114. package/src/defaults/gjc-grok-cli.ts +22 -0
  115. package/src/extensibility/extensions/index.ts +1 -0
  116. package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
  117. package/src/gjc-runtime/deep-interview-recorder.ts +457 -0
  118. package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
  119. package/src/gjc-runtime/deep-interview-state.ts +324 -0
  120. package/src/gjc-runtime/gc-render.ts +70 -0
  121. package/src/gjc-runtime/gc-runtime.ts +403 -0
  122. package/src/gjc-runtime/launch-tmux.ts +3 -4
  123. package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
  124. package/src/gjc-runtime/ralplan-runtime.ts +232 -19
  125. package/src/gjc-runtime/state-renderer.ts +12 -3
  126. package/src/gjc-runtime/state-runtime.ts +48 -30
  127. package/src/gjc-runtime/state-writer.ts +254 -7
  128. package/src/gjc-runtime/team-gc.ts +49 -0
  129. package/src/gjc-runtime/team-runtime.ts +179 -2
  130. package/src/gjc-runtime/tmux-common.ts +14 -0
  131. package/src/gjc-runtime/tmux-gc.ts +177 -0
  132. package/src/gjc-runtime/tmux-sessions.ts +49 -1
  133. package/src/gjc-runtime/ultragoal-guard.ts +155 -0
  134. package/src/gjc-runtime/ultragoal-runtime.ts +1239 -31
  135. package/src/gjc-runtime/workflow-manifest.generated.json +44 -0
  136. package/src/gjc-runtime/workflow-manifest.ts +12 -0
  137. package/src/harness-control-plane/gc-adapter.ts +184 -0
  138. package/src/harness-control-plane/owner.ts +14 -2
  139. package/src/harness-control-plane/rpc-adapter.ts +1 -1
  140. package/src/harness-control-plane/storage.ts +70 -0
  141. package/src/hooks/skill-state.ts +121 -2
  142. package/src/internal-urls/docs-index.generated.ts +22 -12
  143. package/src/lsp/defaults.json +1 -0
  144. package/src/main.ts +18 -3
  145. package/src/modes/acp/acp-agent.ts +4 -2
  146. package/src/modes/bridge/bridge-mode.ts +2 -1
  147. package/src/modes/components/history-search.ts +5 -2
  148. package/src/modes/components/hook-selector.ts +19 -0
  149. package/src/modes/components/model-selector.ts +51 -8
  150. package/src/modes/components/provider-onboarding-selector.ts +6 -1
  151. package/src/modes/components/status-line/segments.ts +1 -1
  152. package/src/modes/controllers/command-controller.ts +25 -6
  153. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  154. package/src/modes/controllers/selector-controller.ts +81 -1
  155. package/src/modes/interactive-mode.ts +11 -1
  156. package/src/modes/rpc/rpc-mode.ts +266 -34
  157. package/src/modes/shared/agent-wire/command-dispatch.ts +281 -261
  158. package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
  159. package/src/modes/shared/agent-wire/host-tool-bridge.ts +3 -0
  160. package/src/modes/shared/agent-wire/session-registry.ts +109 -0
  161. package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
  162. package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
  163. package/src/modes/shared/agent-wire/unattended-session.ts +32 -2
  164. package/src/modes/theme/defaults/claude-code.json +100 -0
  165. package/src/modes/theme/defaults/codex.json +100 -0
  166. package/src/modes/theme/defaults/index.ts +6 -0
  167. package/src/modes/theme/defaults/opencode.json +102 -0
  168. package/src/modes/theme/theme.ts +2 -2
  169. package/src/modes/types.ts +1 -1
  170. package/src/prompts/agents/executor.md +5 -2
  171. package/src/sdk.ts +29 -4
  172. package/src/session/agent-session.ts +99 -19
  173. package/src/session/blob-store.ts +59 -3
  174. package/src/session/history-storage.ts +32 -11
  175. package/src/session/session-manager.ts +72 -20
  176. package/src/setup/credential-import.ts +429 -0
  177. package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
  178. package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
  179. package/src/skill-state/workflow-hud.ts +106 -10
  180. package/src/slash-commands/builtin-registry.ts +3 -2
  181. package/src/task/executor.ts +16 -1
  182. package/src/task/render.ts +18 -7
  183. package/src/tools/ask.ts +59 -2
  184. package/src/tools/cron.ts +1 -1
  185. package/src/tools/job.ts +3 -2
  186. package/src/tools/monitor.ts +36 -1
  187. package/src/tools/subagent-render.ts +128 -29
  188. package/src/tools/subagent.ts +173 -9
  189. package/src/tools/ultragoal-ask-guard.ts +39 -0
  190. package/src/web/search/index.ts +25 -25
  191. package/src/web/search/provider.ts +178 -87
  192. package/src/web/search/providers/base.ts +2 -1
  193. package/src/web/search/providers/openai-compatible.ts +151 -0
  194. package/src/web/search/types.ts +47 -22
package/src/sdk.ts CHANGED
@@ -32,7 +32,7 @@ import {
32
32
  Snowflake,
33
33
  } from "@gajae-code/utils";
34
34
 
35
- import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled } from "./async";
35
+ import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled, jobElapsedMs } from "./async";
36
36
  import { loadCapability } from "./capability";
37
37
  import { type Rule, ruleCapability, setActiveRules } from "./capability/rule";
38
38
  import { ModelRegistry } from "./config/model-registry";
@@ -50,6 +50,7 @@ import { CursorExecHandlers } from "./cursor";
50
50
  import "./discovery";
51
51
  import { resolveConfigValue } from "./config/resolve-config-value";
52
52
  import { getEmbeddedDefaultGjcSkills } from "./defaults/gjc-defaults";
53
+ import { BUNDLED_GROK_BUILD_EXTENSION_ID, getBundledGrokBuildExtensionFactory } from "./defaults/gjc-grok-cli";
53
54
  import { initializeWithSettings } from "./discovery";
54
55
  import { disposeAllKernelSessions, disposeKernelSessionsByOwner } from "./eval/py/executor";
55
56
  import { TtsrManager } from "./export/ttsr";
@@ -114,6 +115,7 @@ import {
114
115
  FindTool,
115
116
  getSearchTools,
116
117
  HIDDEN_TOOLS,
118
+ isConfigurableSearchProviderId,
117
119
  isSearchProviderPreference,
118
120
  type LspStartupServerInfo,
119
121
  loadSshTool,
@@ -123,6 +125,7 @@ import {
123
125
  SearchTool,
124
126
  setPreferredImageProvider,
125
127
  setPreferredSearchProvider,
128
+ setSearchFallbackProviders,
126
129
  type Tool,
127
130
  type ToolSession,
128
131
  WebSearchTool,
@@ -132,6 +135,7 @@ import {
132
135
  import { ToolContextStore } from "./tools/context";
133
136
  import { getImageGenTools } from "./tools/image-gen";
134
137
  import { wrapToolWithMetaNotice } from "./tools/output-meta";
138
+ import { guardToolForUltragoalAsk } from "./tools/ultragoal-ask-guard";
135
139
  import { EventBus } from "./utils/event-bus";
136
140
  import { buildNamedToolChoice, buildNamedToolChoiceResult } from "./utils/tool-choice";
137
141
  import { buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree";
@@ -864,6 +868,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
864
868
  if (typeof webSearchProvider === "string" && isSearchProviderPreference(webSearchProvider)) {
865
869
  setPreferredSearchProvider(webSearchProvider);
866
870
  }
871
+ const webSearchFallback = settings.get("web_search.fallback");
872
+ if (Array.isArray(webSearchFallback)) {
873
+ setSearchFallbackProviders(
874
+ webSearchFallback.filter(value => typeof value === "string" && isConfigurableSearchProviderId(value)),
875
+ );
876
+ }
867
877
 
868
878
  const imageProvider = settings.get("providers.image");
869
879
  if (
@@ -1124,7 +1134,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1124
1134
  const formattedResult = await formatAsyncResultForFollowUp(result);
1125
1135
  if (asyncJobManager!.isDeliverySuppressed(jobId)) return;
1126
1136
 
1127
- const durationMs = job ? Math.max(0, Date.now() - job.startTime) : undefined;
1137
+ const durationMs = job ? jobElapsedMs(job) : undefined;
1128
1138
  session.yieldQueue.enqueue<AsyncResultEntry>("async-result", {
1129
1139
  jobId,
1130
1140
  result: formattedResult,
@@ -1341,13 +1351,26 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1341
1351
  }
1342
1352
 
1343
1353
  // Extension/module discovery is quarantined; retain only the private
1344
- // runtime needed for explicitly supplied SDK extensions and custom tools.
1354
+ // runtime needed for bundled product extensions, explicitly supplied SDK
1355
+ // extension factories, and custom tools. Filesystem extension paths remain
1356
+ // ignored here even when options.additionalExtensionPaths is supplied.
1345
1357
  const extensionsResult: LoadExtensionsResult = options.preloadedExtensions ?? {
1346
1358
  extensions: [],
1347
1359
  errors: [],
1348
1360
  runtime: new ExtensionRuntime(),
1349
1361
  };
1350
1362
 
1363
+ if (!extensionsResult.extensions.some(extension => extension.path === BUNDLED_GROK_BUILD_EXTENSION_ID)) {
1364
+ const bundledGrokExtension = await loadExtensionFromFactory(
1365
+ getBundledGrokBuildExtensionFactory(),
1366
+ cwd,
1367
+ eventBus,
1368
+ extensionsResult.runtime,
1369
+ BUNDLED_GROK_BUILD_EXTENSION_ID,
1370
+ );
1371
+ extensionsResult.extensions.push(bundledGrokExtension);
1372
+ }
1373
+
1351
1374
  // Load inline extensions from factories
1352
1375
  if (inlineExtensions.length > 0) {
1353
1376
  for (let i = 0; i < inlineExtensions.length; i++) {
@@ -1792,7 +1815,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1792
1815
 
1793
1816
  const initialTools = initialToolNames
1794
1817
  .map(name => toolRegistry.get(name))
1795
- .filter((tool): tool is AgentTool => tool !== undefined);
1818
+ .filter((tool): tool is AgentTool => tool !== undefined)
1819
+ // AgentSession tool wrapping is not installed until after Agent construction.
1820
+ .map(tool => guardToolForUltragoalAsk(tool, () => sessionManager.getCwd()));
1796
1821
 
1797
1822
  const openaiWebsocketSetting = settings.get("providers.openaiWebsockets") ?? "off";
1798
1823
  const preferOpenAICodexWebsockets =
@@ -240,6 +240,7 @@ import { normalizeLocalScheme, resolveToCwd } from "../tools/path-utils";
240
240
  import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from "../tools/todo-write";
241
241
  import { ToolAbortError, ToolError } from "../tools/tool-errors";
242
242
  import { clampTimeout } from "../tools/tool-timeouts";
243
+ import { guardToolForUltragoalAsk } from "../tools/ultragoal-ask-guard";
243
244
  import { parseCommandArgs } from "../utils/command-args";
244
245
  import { type EditMode, resolveEditMode } from "../utils/edit-mode";
245
246
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
@@ -322,7 +323,10 @@ function isUnderProjectGjc(cwd: string, targetPath: string): boolean {
322
323
 
323
324
  /** Listener function for agent session events */
324
325
  export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
325
- export type AsyncJobSnapshotItem = Pick<AsyncJob, "id" | "type" | "status" | "label" | "startTime" | "metadata">;
326
+ export type AsyncJobSnapshotItem = Pick<
327
+ AsyncJob,
328
+ "id" | "type" | "status" | "label" | "startTime" | "endTime" | "metadata"
329
+ >;
326
330
 
327
331
  export interface AsyncJobSnapshot {
328
332
  running: AsyncJobSnapshotItem[];
@@ -903,6 +907,7 @@ export class AgentSession {
903
907
  // Compaction state
904
908
  #compactionAbortController: AbortController | undefined = undefined;
905
909
  #autoCompactionAbortController: AbortController | undefined = undefined;
910
+ #prePromptContextCheckPromise: Promise<void> | undefined = undefined;
906
911
 
907
912
  // Branch summarization state
908
913
  #branchSummaryAbortController: AbortController | undefined = undefined;
@@ -1182,6 +1187,7 @@ export class AgentSession {
1182
1187
  };
1183
1188
  this.agent.setProviderResponseInterceptor(this.#onResponse);
1184
1189
  this.agent.setRawSseEventInterceptor(this.#onSseEvent);
1190
+ this.#setGuardedAgentTools(this.agent.state.tools);
1185
1191
  this.yieldQueue = new YieldQueue({
1186
1192
  isStreaming: () => this.isStreaming,
1187
1193
  injectStreaming: message => this.agent.followUp(message),
@@ -1563,6 +1569,7 @@ export class AgentSession {
1563
1569
  status: job.status,
1564
1570
  label: job.label,
1565
1571
  startTime: job.startTime,
1572
+ endTime: job.endTime,
1566
1573
  metadata: job.metadata,
1567
1574
  }));
1568
1575
  const recent = manager.getRecentJobs(options?.recentLimit ?? 5, ownerFilter).map(job => ({
@@ -1571,6 +1578,7 @@ export class AgentSession {
1571
1578
  status: job.status,
1572
1579
  label: job.label,
1573
1580
  startTime: job.startTime,
1581
+ endTime: job.endTime,
1574
1582
  metadata: job.metadata,
1575
1583
  }));
1576
1584
  const delivery = manager.getDeliveryState(ownerFilter);
@@ -3684,6 +3692,16 @@ export class AgentSession {
3684
3692
  }) as T;
3685
3693
  }
3686
3694
 
3695
+ #prepareToolForExecution<T extends AgentTool>(tool: T): T {
3696
+ return this.#wrapToolForDeepInterviewMutationGuard(
3697
+ this.#wrapToolForAcpPermission(guardToolForUltragoalAsk(tool, () => this.sessionManager.getCwd())),
3698
+ );
3699
+ }
3700
+
3701
+ #setGuardedAgentTools(tools: AgentTool[]): void {
3702
+ this.agent.setTools(tools.map(tool => this.#prepareToolForExecution(tool)));
3703
+ }
3704
+
3687
3705
  async #applyActiveToolsByName(
3688
3706
  toolNames: string[],
3689
3707
  options?: { persistMCPSelection?: boolean; previousSelectedMCPToolNames?: string[] },
@@ -3695,7 +3713,7 @@ export class AgentSession {
3695
3713
  for (const name of toolNames) {
3696
3714
  const tool = this.#toolRegistry.get(name);
3697
3715
  if (tool) {
3698
- tools.push(this.#wrapToolForDeepInterviewMutationGuard(this.#wrapToolForAcpPermission(tool)));
3716
+ tools.push(tool);
3699
3717
  validToolNames.push(name);
3700
3718
  }
3701
3719
  }
@@ -3712,7 +3730,7 @@ export class AgentSession {
3712
3730
  this.#selectedDiscoveredToolNames.delete(name);
3713
3731
  }
3714
3732
  }
3715
- this.agent.setTools(tools);
3733
+ this.#setGuardedAgentTools(tools);
3716
3734
 
3717
3735
  // Active tool set changed → discoverable tool list (which excludes already-active tools)
3718
3736
  // is now stale. Invalidate before any prompt-template hook reads the discovery list.
@@ -3970,6 +3988,9 @@ export class AgentSession {
3970
3988
  if (uniqueToolNames.size !== nextToolNames.length) {
3971
3989
  throw new Error("RPC host tool names must be unique");
3972
3990
  }
3991
+ if (uniqueToolNames.has("ask")) {
3992
+ throw new Error('RPC host tool "ask" is reserved and cannot be supplied by the host');
3993
+ }
3973
3994
 
3974
3995
  for (const name of uniqueToolNames) {
3975
3996
  if (this.#toolRegistry.has(name) && !this.#rpcHostToolNames.has(name)) {
@@ -4297,11 +4318,8 @@ export class AgentSession {
4297
4318
  this.#toolRegistry.set(finalTool.name, finalTool);
4298
4319
 
4299
4320
  if (!this.getActiveToolNames().includes(finalTool.name)) {
4300
- const activeTools = [
4301
- ...this.agent.state.tools,
4302
- this.#wrapToolForDeepInterviewMutationGuard(this.#wrapToolForAcpPermission(finalTool)),
4303
- ];
4304
- this.agent.setTools(activeTools);
4321
+ const activeTools = [...this.agent.state.tools, finalTool];
4322
+ this.#setGuardedAgentTools(activeTools);
4305
4323
  this.#invalidateDiscoveryCaches();
4306
4324
  void this.refreshBaseSystemPrompt().catch(error => {
4307
4325
  logger.warn("Failed to refresh system prompt after workflow gate ask tool registration", {
@@ -4333,9 +4351,8 @@ export class AgentSession {
4333
4351
  const activeToolNames = this.getActiveToolNames();
4334
4352
  const activeTools = activeToolNames
4335
4353
  .map(name => this.#toolRegistry.get(name))
4336
- .filter((tool): tool is AgentTool => tool !== undefined)
4337
- .map(tool => this.#wrapToolForAcpPermission(tool));
4338
- this.agent.setTools(activeTools);
4354
+ .filter((tool): tool is AgentTool => tool !== undefined);
4355
+ this.#setGuardedAgentTools(activeTools);
4339
4356
  }
4340
4357
 
4341
4358
  getCheckpointState(): CheckpointState | undefined {
@@ -4754,7 +4771,11 @@ export class AgentSession {
4754
4771
  await this.#checkCompaction(lastAssistant, false);
4755
4772
  }
4756
4773
  if (!options?.skipCompactionCheck) {
4757
- await this.#checkEstimatedContextBeforePrompt();
4774
+ await this.#checkEstimatedContextBeforePrompt([
4775
+ ...(options?.prependMessages ?? []),
4776
+ message,
4777
+ ...this.#pendingNextTurnMessages,
4778
+ ]);
4758
4779
  }
4759
4780
 
4760
4781
  // Build messages array (session context, eager todo prelude, then active prompt message)
@@ -5219,7 +5240,9 @@ export class AgentSession {
5219
5240
  }
5220
5241
  await this.#syncSkillPromptActiveStateSafely(appMessage, true);
5221
5242
  try {
5222
- await this.agent.prompt(appMessage);
5243
+ await this.#promptWithMessage(appMessage, this.#getCustomMessageTextContent(appMessage), {
5244
+ skipPostPromptRecoveryWait: true,
5245
+ });
5223
5246
  } finally {
5224
5247
  await this.#syncSkillPromptActiveStateSafely(appMessage, false);
5225
5248
  }
@@ -5243,7 +5266,9 @@ export class AgentSession {
5243
5266
  }
5244
5267
  await this.#syncSkillPromptActiveStateSafely(appMessage, true);
5245
5268
  try {
5246
- await this.agent.prompt(appMessage);
5269
+ await this.#promptWithMessage(appMessage, this.#getCustomMessageTextContent(appMessage), {
5270
+ skipPostPromptRecoveryWait: true,
5271
+ });
5247
5272
  } finally {
5248
5273
  await this.#syncSkillPromptActiveStateSafely(appMessage, false);
5249
5274
  }
@@ -6546,7 +6571,23 @@ export class AgentSession {
6546
6571
  }
6547
6572
  }
6548
6573
 
6549
- async #checkEstimatedContextBeforePrompt(): Promise<void> {
6574
+ async #checkEstimatedContextBeforePrompt(pendingMessages: readonly AgentMessage[] = []): Promise<void> {
6575
+ if (this.#prePromptContextCheckPromise) {
6576
+ await this.#prePromptContextCheckPromise;
6577
+ }
6578
+
6579
+ const checkPromise = this.#checkEstimatedContextBeforePromptOnce(pendingMessages);
6580
+ this.#prePromptContextCheckPromise = checkPromise;
6581
+ try {
6582
+ await checkPromise;
6583
+ } finally {
6584
+ if (this.#prePromptContextCheckPromise === checkPromise) {
6585
+ this.#prePromptContextCheckPromise = undefined;
6586
+ }
6587
+ }
6588
+ }
6589
+
6590
+ async #checkEstimatedContextBeforePromptOnce(pendingMessages: readonly AgentMessage[]): Promise<void> {
6550
6591
  const model = this.model;
6551
6592
  if (!model) return;
6552
6593
  const contextWindow = model.contextWindow ?? 0;
@@ -6554,7 +6595,7 @@ export class AgentSession {
6554
6595
  const compactionSettings = this.settings.getGroup("compaction");
6555
6596
  if (!compactionSettings.enabled || compactionSettings.strategy === "off") return;
6556
6597
 
6557
- let contextTokens = this.#estimateContextTokens().tokens;
6598
+ let contextTokens = this.#estimateContextTokensForCompaction(pendingMessages).tokens;
6558
6599
  const maxOutputTokens = model.maxTokens ?? 0;
6559
6600
  if (!shouldCompact(contextTokens, contextWindow, compactionSettings, maxOutputTokens)) return;
6560
6601
 
@@ -9144,7 +9185,7 @@ export class AgentSession {
9144
9185
  error: String(mcpError),
9145
9186
  });
9146
9187
  this.#selectedMCPToolNames = new Set(previousSelectedMCPToolNames);
9147
- this.agent.setTools(previousTools);
9188
+ this.#setGuardedAgentTools(previousTools);
9148
9189
  this.#baseSystemPrompt = previousBaseSystemPrompt;
9149
9190
  this.agent.setSystemPrompt(previousSystemPrompt);
9150
9191
  }
@@ -9597,6 +9638,21 @@ export class AgentSession {
9597
9638
  */
9598
9639
  #estimateContextTokens(): {
9599
9640
  tokens: number;
9641
+ } {
9642
+ return this.#estimateContextTokensWith(message => this.#estimateMessageDisplayTokens(message));
9643
+ }
9644
+
9645
+ #estimateContextTokensForCompaction(pendingMessages: readonly AgentMessage[]): {
9646
+ tokens: number;
9647
+ } {
9648
+ const estimate = this.#estimateContextTokensWith(message => this.#estimateMessageNativeContextTokens(message));
9649
+ return {
9650
+ tokens: estimate.tokens + this.#estimateMessagesNativeContextTokens(pendingMessages),
9651
+ };
9652
+ }
9653
+
9654
+ #estimateContextTokensWith(estimateMessage: (message: AgentMessage) => number): {
9655
+ tokens: number;
9600
9656
  } {
9601
9657
  const messages = this.messages;
9602
9658
 
@@ -9619,7 +9675,7 @@ export class AgentSession {
9619
9675
  // No usage data - estimate all messages
9620
9676
  let estimated = 0;
9621
9677
  for (const message of messages) {
9622
- estimated += estimateMessageTokensHeuristic(message);
9678
+ estimated += estimateMessage(message);
9623
9679
  }
9624
9680
  return {
9625
9681
  tokens: estimated,
@@ -9629,7 +9685,7 @@ export class AgentSession {
9629
9685
  const usageTokens = calculatePromptTokens(lastUsage);
9630
9686
  let trailingTokens = 0;
9631
9687
  for (let i = lastUsageIndex + 1; i < messages.length; i++) {
9632
- trailingTokens += estimateMessageTokensHeuristic(messages[i]);
9688
+ trailingTokens += estimateMessage(messages[i]);
9633
9689
  }
9634
9690
 
9635
9691
  return {
@@ -9637,6 +9693,30 @@ export class AgentSession {
9637
9693
  };
9638
9694
  }
9639
9695
 
9696
+ #estimateMessagesNativeContextTokens(messages: readonly AgentMessage[]): number {
9697
+ let tokens = 0;
9698
+ for (const message of messages) {
9699
+ tokens += this.#estimateMessageNativeContextTokens(message);
9700
+ }
9701
+ return tokens;
9702
+ }
9703
+
9704
+ #estimateMessageDisplayTokens(message: AgentMessage): number {
9705
+ let tokens = 0;
9706
+ for (const llmMessage of convertToLlm([message])) {
9707
+ tokens += estimateMessageTokensHeuristic(llmMessage);
9708
+ }
9709
+ return tokens;
9710
+ }
9711
+
9712
+ #estimateMessageNativeContextTokens(message: AgentMessage): number {
9713
+ let tokens = 0;
9714
+ for (const llmMessage of convertToLlm([message])) {
9715
+ tokens += estimateTokens(llmMessage);
9716
+ }
9717
+ return tokens;
9718
+ }
9719
+
9640
9720
  /**
9641
9721
  * Export session to HTML.
9642
9722
  * @param outputPath Optional output path (defaults to session directory)
@@ -267,7 +267,13 @@ export function externalizeImageDataSync(blobStore: BlobStore, base64Data: strin
267
267
  /**
268
268
  * Resolve an externalized provider image data URL back to its original string.
269
269
  * If the data is not a blob reference, returns it unchanged.
270
- * 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.
271
277
  */
272
278
  export async function resolveImageDataUrl(blobStore: BlobStore, data: string): Promise<string> {
273
279
  const hash = parseBlobRef(data);
@@ -284,7 +290,11 @@ export async function resolveImageDataUrl(blobStore: BlobStore, data: string): P
284
290
  /**
285
291
  * Resolve a blob reference back to base64 data.
286
292
  * If the data is not a blob reference, returns it unchanged.
287
- * 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.
288
298
  */
289
299
  export async function resolveImageData(blobStore: BlobStore, data: string): Promise<string> {
290
300
  const hash = parseBlobRef(data);
@@ -322,7 +332,14 @@ export function resolveImageDataSync(blobStore: BlobStore, data: string): string
322
332
  return buffer.toString("base64");
323
333
  }
324
334
 
325
- /** Synchronously resolve a blob reference back to utf8 text. */
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
+ */
326
343
  export function resolveTextBlobSync(
327
344
  blobStore: BlobStore,
328
345
  data: string,
@@ -336,3 +353,42 @@ export function resolveTextBlobSync(
336
353
  }
337
354
  return buffer.toString("utf8");
338
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
+ }
@@ -67,10 +67,14 @@ export class HistoryStorage {
67
67
  // Prepared statements
68
68
  #insertRowStmt: Statement;
69
69
  #recentStmt: Statement;
70
+ #recentByCwdStmt: Statement;
70
71
  #searchStmt: Statement;
72
+ #searchByCwdStmt: Statement;
71
73
  #lastPromptStmt: Statement;
72
74
  // Cache substring-fallback prepared statements keyed by token count.
73
75
  #substringStmts = new Map<number, Statement>();
76
+ // Cache cwd-filtered substring-fallback statements keyed by token count.
77
+ #substringCwdStmts = new Map<number, Statement>();
74
78
 
75
79
  // In-memory cache of last prompt to avoid sync DB reads on add
76
80
  #lastPromptCache: string | null = null;
@@ -94,6 +98,7 @@ CREATE TABLE IF NOT EXISTS history (
94
98
  cwd TEXT
95
99
  );
96
100
  CREATE INDEX IF NOT EXISTS idx_history_created_at ON history(created_at DESC);
101
+ CREATE INDEX IF NOT EXISTS idx_history_cwd_created_at ON history(cwd, created_at DESC);
97
102
 
98
103
  CREATE VIRTUAL TABLE IF NOT EXISTS history_fts USING fts5(prompt, content='history', content_rowid='id');
99
104
 
@@ -117,9 +122,15 @@ CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
117
122
  this.#recentStmt = this.#db.prepare(
118
123
  "SELECT id, prompt, created_at, cwd FROM history ORDER BY created_at DESC, id DESC LIMIT ?",
119
124
  );
125
+ this.#recentByCwdStmt = this.#db.prepare(
126
+ "SELECT id, prompt, created_at, cwd FROM history WHERE cwd = ? ORDER BY created_at DESC, id DESC LIMIT ?",
127
+ );
120
128
  this.#searchStmt = this.#db.prepare(
121
129
  "SELECT h.id, h.prompt, h.created_at, h.cwd FROM history_fts f JOIN history h ON h.id = f.rowid WHERE history_fts MATCH ? ORDER BY h.created_at DESC, h.id DESC LIMIT ?",
122
130
  );
131
+ this.#searchByCwdStmt = this.#db.prepare(
132
+ "SELECT h.id, h.prompt, h.created_at, h.cwd FROM history_fts f JOIN history h ON h.id = f.rowid WHERE history_fts MATCH ? AND h.cwd = ? ORDER BY h.created_at DESC, h.id DESC LIMIT ?",
133
+ );
123
134
  this.#lastPromptStmt = this.#db.prepare("SELECT prompt FROM history ORDER BY id DESC LIMIT 1");
124
135
 
125
136
  this.#insertRowStmt = this.#db.prepare("INSERT INTO history (prompt, cwd) VALUES (?, ?)");
@@ -158,12 +169,14 @@ CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
158
169
  });
159
170
  }
160
171
 
161
- getRecent(limit: number): HistoryEntry[] {
172
+ getRecent(limit: number, cwd?: string): HistoryEntry[] {
162
173
  const safeLimit = this.#normalizeLimit(limit);
163
174
  if (safeLimit === 0) return [];
164
175
 
165
176
  try {
166
- const rows = this.#recentStmt.all(safeLimit) as HistoryRow[];
177
+ const rows = (
178
+ cwd === undefined ? this.#recentStmt.all(safeLimit) : this.#recentByCwdStmt.all(cwd, safeLimit)
179
+ ) as HistoryRow[];
167
180
  return rows.map(row => this.#toEntry(row));
168
181
  } catch (error) {
169
182
  logger.error("HistoryStorage getRecent failed", { error: String(error) });
@@ -171,7 +184,7 @@ CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
171
184
  }
172
185
  }
173
186
 
174
- search(query: string, limit: number): HistoryEntry[] {
187
+ search(query: string, limit: number, cwd?: string): HistoryEntry[] {
175
188
  const safeLimit = this.#normalizeLimit(limit);
176
189
  if (safeLimit === 0) return [];
177
190
 
@@ -184,7 +197,11 @@ CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
184
197
  const ftsQuery = tokens.map(tok => `"${tok.replace(/"/g, '""')}"*`).join(" ");
185
198
  let ftsRows: HistoryRow[] = [];
186
199
  try {
187
- ftsRows = this.#searchStmt.all(ftsQuery, safeLimit) as HistoryRow[];
200
+ ftsRows = (
201
+ cwd === undefined
202
+ ? this.#searchStmt.all(ftsQuery, safeLimit)
203
+ : this.#searchByCwdStmt.all(ftsQuery, cwd, safeLimit)
204
+ ) as HistoryRow[];
188
205
  } catch (error) {
189
206
  // Malformed FTS expression - fall through to substring path.
190
207
  logger.debug("HistoryStorage FTS query failed, using substring only", { error: String(error) });
@@ -199,7 +216,7 @@ CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
199
216
  // by safeLimit, ordered by recency - no full-table load into JS.
200
217
  let subRows: HistoryRow[] = [];
201
218
  try {
202
- subRows = this.#searchSubstring(tokens, safeLimit);
219
+ subRows = this.#searchSubstring(tokens, safeLimit, cwd);
203
220
  } catch (error) {
204
221
  logger.error("HistoryStorage substring search failed", { error: String(error) });
205
222
  }
@@ -250,6 +267,7 @@ CREATE TABLE history (
250
267
  cwd TEXT
251
268
  );
252
269
  CREATE INDEX IF NOT EXISTS idx_history_created_at ON history(created_at DESC);
270
+ CREATE INDEX IF NOT EXISTS idx_history_cwd_created_at ON history(cwd, created_at DESC);
253
271
  INSERT INTO history (id, prompt, created_at, cwd)
254
272
  SELECT id, prompt, created_at, cwd
255
273
  FROM history_legacy;
@@ -282,21 +300,24 @@ END;
282
300
  .filter(tok => tok.length > 0);
283
301
  }
284
302
 
285
- #searchSubstring(tokens: string[], limit: number): HistoryRow[] {
286
- const stmt = this.#getSubstringStmt(tokens.length);
303
+ #searchSubstring(tokens: string[], limit: number, cwd?: string): HistoryRow[] {
304
+ const stmt = this.#getSubstringStmt(tokens.length, cwd !== undefined);
287
305
  const params: unknown[] = tokens.map(tok => `%${escapeLikePattern(tok)}%`);
306
+ if (cwd !== undefined) params.push(cwd);
288
307
  params.push(limit);
289
308
  return stmt.all(...(params as [string, ...unknown[]])) as HistoryRow[];
290
309
  }
291
310
 
292
- #getSubstringStmt(tokenCount: number): Statement {
293
- let stmt = this.#substringStmts.get(tokenCount);
311
+ #getSubstringStmt(tokenCount: number, withCwd: boolean): Statement {
312
+ const cache = withCwd ? this.#substringCwdStmts : this.#substringStmts;
313
+ let stmt = cache.get(tokenCount);
294
314
  if (stmt) return stmt;
295
315
  const whereClause = Array(tokenCount).fill("prompt LIKE ? ESCAPE '\\' COLLATE NOCASE").join(" AND ");
316
+ const cwdClause = withCwd ? " AND cwd = ?" : "";
296
317
  stmt = this.#db.prepare(
297
- `SELECT id, prompt, created_at, cwd FROM history WHERE ${whereClause} ORDER BY created_at DESC, id DESC LIMIT ?`,
318
+ `SELECT id, prompt, created_at, cwd FROM history WHERE ${whereClause}${cwdClause} ORDER BY created_at DESC, id DESC LIMIT ?`,
298
319
  );
299
- this.#substringStmts.set(tokenCount, stmt);
320
+ cache.set(tokenCount, stmt);
300
321
  return stmt;
301
322
  }
302
323