@gajae-code/coding-agent 0.16.3 → 0.16.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 (277) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/dist/types/capability/types.d.ts +4 -0
  3. package/dist/types/cli/args.d.ts +2 -2
  4. package/dist/types/cli/customize-doctor.d.ts +22 -0
  5. package/dist/types/cli/root-flags.d.ts +4 -2
  6. package/dist/types/cli.d.ts +1 -1
  7. package/dist/types/commands/launch.d.ts +1 -1
  8. package/dist/types/config/autorouting-tier-map.d.ts +3 -0
  9. package/dist/types/config/file-lock.d.ts +31 -3
  10. package/dist/types/config/model-registry.d.ts +4 -0
  11. package/dist/types/config/model-resolver.d.ts +9 -0
  12. package/dist/types/config/settings-schema.d.ts +29 -1
  13. package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
  14. package/dist/types/coordinator-mcp/server.d.ts +7 -0
  15. package/dist/types/discovery/claude-plugins.d.ts +3 -1
  16. package/dist/types/discovery/helpers.d.ts +35 -0
  17. package/dist/types/exec/bash-executor.d.ts +17 -1
  18. package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
  19. package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
  20. package/dist/types/exec/bash-shell-signals.d.ts +2 -0
  21. package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
  22. package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
  23. package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
  24. package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
  25. package/dist/types/exec/bash-shell-worker.d.ts +1 -0
  26. package/dist/types/exec/isolated-shell.d.ts +19 -0
  27. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
  28. package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
  29. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  30. package/dist/types/extensibility/extensions/runner.d.ts +46 -6
  31. package/dist/types/extensibility/extensions/types.d.ts +16 -0
  32. package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
  33. package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
  34. package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
  35. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
  36. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  37. package/dist/types/extensibility/hooks/types.d.ts +4 -0
  38. package/dist/types/extensibility/runtime-skill-discovery.d.ts +5 -2
  39. package/dist/types/extensibility/shared-events.d.ts +1 -1
  40. package/dist/types/extensibility/skill-management.d.ts +9 -4
  41. package/dist/types/extensibility/skills.d.ts +2 -0
  42. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
  43. package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
  44. package/dist/types/gjc-runtime/session-state-lock.d.ts +14 -0
  45. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
  46. package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
  47. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  48. package/dist/types/modes/components/bash-execution.d.ts +1 -0
  49. package/dist/types/modes/components/execution-shared.d.ts +1 -0
  50. package/dist/types/modes/components/settings-selector.d.ts +6 -1
  51. package/dist/types/modes/components/status-line/command.d.ts +35 -0
  52. package/dist/types/modes/components/status-line/index.d.ts +1 -0
  53. package/dist/types/modes/components/status-line/types.d.ts +5 -0
  54. package/dist/types/modes/components/tool-status-header.d.ts +14 -0
  55. package/dist/types/modes/controllers/event-controller.d.ts +6 -0
  56. package/dist/types/modes/interactive-mode.d.ts +3 -0
  57. package/dist/types/modes/types.d.ts +7 -0
  58. package/dist/types/runtime-mcp/client.d.ts +9 -1
  59. package/dist/types/runtime-mcp/manager.d.ts +7 -1
  60. package/dist/types/runtime-mcp/pool.d.ts +22 -3
  61. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  62. package/dist/types/runtime-mcp/types.d.ts +22 -0
  63. package/dist/types/sdk/broker/ensure.d.ts +32 -0
  64. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
  65. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  66. package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
  67. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
  68. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
  69. package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
  70. package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
  71. package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
  72. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  73. package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
  74. package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
  75. package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
  76. package/dist/types/sdk/client/client.d.ts +6 -0
  77. package/dist/types/sdk/host/host.d.ts +3 -1
  78. package/dist/types/sdk/host/session-runtime.d.ts +26 -1
  79. package/dist/types/sdk/lifecycle/service.d.ts +1 -0
  80. package/dist/types/sdk/router/session-router.d.ts +10 -2
  81. package/dist/types/sdk/session-api-key.d.ts +26 -0
  82. package/dist/types/sdk/session.d.ts +5 -1
  83. package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
  84. package/dist/types/session/agent-session.d.ts +11 -0
  85. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  86. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  87. package/dist/types/session/messages.d.ts +1 -0
  88. package/dist/types/session/session-manager.d.ts +3 -1
  89. package/dist/types/setup/paseo/announce.d.ts +112 -0
  90. package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
  91. package/dist/types/skill-state/canonical-skills.d.ts +4 -0
  92. package/dist/types/system-prompt.d.ts +10 -0
  93. package/dist/types/task/executor.d.ts +5 -1
  94. package/dist/types/tools/bash.d.ts +1 -0
  95. package/dist/types/tools/fetch.d.ts +7 -0
  96. package/dist/types/tools/github-cache.d.ts +2 -1
  97. package/dist/types/tools/index.d.ts +10 -0
  98. package/dist/types/tools/path-utils.d.ts +2 -0
  99. package/dist/types/tools/search.d.ts +6 -0
  100. package/package.json +9 -7
  101. package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
  102. package/scripts/generate-sdk-operation-inventory.ts +11 -0
  103. package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
  104. package/src/capability/index.ts +14 -2
  105. package/src/capability/types.ts +4 -0
  106. package/src/cli/args.ts +20 -9
  107. package/src/cli/customize-doctor.ts +155 -12
  108. package/src/cli/root-flags.ts +5 -3
  109. package/src/cli.ts +22 -0
  110. package/src/commands/sdk.ts +74 -31
  111. package/src/config/autorouting-tier-map.ts +1 -0
  112. package/src/config/file-lock-gc.ts +40 -2
  113. package/src/config/file-lock.ts +379 -31
  114. package/src/config/model-preset-registry.ts +120 -19
  115. package/src/config/model-profiles.ts +40 -0
  116. package/src/config/model-registry.ts +73 -23
  117. package/src/config/model-resolver.ts +48 -0
  118. package/src/config/settings-schema.ts +33 -1
  119. package/src/config/settings.ts +1 -0
  120. package/src/coordinator-mcp/question-state.ts +33 -2
  121. package/src/coordinator-mcp/server.ts +477 -101
  122. package/src/crash/sanitize.ts +1 -1
  123. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
  124. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
  125. package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
  126. package/src/discovery/builtin.ts +52 -55
  127. package/src/discovery/claude-plugins.ts +12 -4
  128. package/src/discovery/helpers.ts +307 -34
  129. package/src/exec/bash-executor.ts +330 -50
  130. package/src/exec/bash-shell-guardian.ts +388 -0
  131. package/src/exec/bash-shell-runtime-entry.ts +5 -0
  132. package/src/exec/bash-shell-signals.ts +29 -0
  133. package/src/exec/bash-shell-supervisor-entry.ts +3 -0
  134. package/src/exec/bash-shell-supervisor.ts +359 -0
  135. package/src/exec/bash-shell-worker-entry.ts +5 -0
  136. package/src/exec/bash-shell-worker-protocol.ts +36 -0
  137. package/src/exec/bash-shell-worker.ts +116 -0
  138. package/src/exec/isolated-shell.ts +393 -0
  139. package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
  140. package/src/extensibility/extensions/function-hooks.ts +959 -0
  141. package/src/extensibility/extensions/index.ts +22 -0
  142. package/src/extensibility/extensions/loader.ts +53 -2
  143. package/src/extensibility/extensions/runner.ts +895 -264
  144. package/src/extensibility/extensions/types.ts +42 -0
  145. package/src/extensibility/extensions/wrapper.ts +56 -15
  146. package/src/extensibility/gjc-plugins/compiler.ts +228 -60
  147. package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
  148. package/src/extensibility/gjc-plugins/installer.ts +13 -12
  149. package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
  150. package/src/extensibility/gjc-plugins/paths.ts +13 -0
  151. package/src/extensibility/gjc-plugins/registry.ts +113 -1
  152. package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
  153. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
  154. package/src/extensibility/gjc-plugins/schema.ts +121 -4
  155. package/src/extensibility/gjc-plugins/types.ts +25 -0
  156. package/src/extensibility/hooks/loader.ts +133 -16
  157. package/src/extensibility/hooks/types.ts +28 -0
  158. package/src/extensibility/runtime-skill-discovery.ts +149 -190
  159. package/src/extensibility/shared-events.ts +1 -1
  160. package/src/extensibility/skill-management.ts +200 -52
  161. package/src/extensibility/skills.ts +73 -25
  162. package/src/gjc-runtime/empty-delete-gc.ts +94 -4
  163. package/src/gjc-runtime/gc-runtime.ts +29 -0
  164. package/src/gjc-runtime/linux-proc.ts +14 -0
  165. package/src/gjc-runtime/repository-binding.ts +1 -1
  166. package/src/gjc-runtime/session-state-lock.ts +143 -10
  167. package/src/gjc-runtime/session-state-sidecar.ts +27 -11
  168. package/src/gjc-runtime/state-writer.ts +30 -29
  169. package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
  170. package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
  171. package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
  172. package/src/gjc-runtime/workflow-manifest.ts +16 -0
  173. package/src/harness-control-plane/session-lease.ts +2 -2
  174. package/src/internal-urls/docs-index.generated.ts +12 -12
  175. package/src/lsp/config.ts +10 -3
  176. package/src/lsp/index.ts +1 -1
  177. package/src/lsp/path-trust.ts +69 -14
  178. package/src/main.ts +78 -35
  179. package/src/modes/DESIGN.md +54 -0
  180. package/src/modes/acp/acp-agent.ts +16 -3
  181. package/src/modes/components/assistant-message.ts +22 -9
  182. package/src/modes/components/bash-execution.ts +4 -1
  183. package/src/modes/components/execution-shared.ts +4 -1
  184. package/src/modes/components/model-selector.ts +166 -33
  185. package/src/modes/components/session-observer-overlay.ts +1 -1
  186. package/src/modes/components/settings-selector.ts +1075 -235
  187. package/src/modes/components/status-line/command.ts +190 -0
  188. package/src/modes/components/status-line/index.ts +1 -0
  189. package/src/modes/components/status-line/segments.ts +32 -5
  190. package/src/modes/components/status-line/types.ts +5 -0
  191. package/src/modes/components/tool-status-header.ts +396 -22
  192. package/src/modes/controllers/command-controller.ts +3 -0
  193. package/src/modes/controllers/event-controller.ts +151 -7
  194. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  195. package/src/modes/controllers/input-controller.ts +6 -3
  196. package/src/modes/controllers/selector-controller.ts +21 -0
  197. package/src/modes/interactive-mode.ts +53 -6
  198. package/src/modes/print-mode.ts +5 -1
  199. package/src/modes/prompt-action-autocomplete.ts +54 -27
  200. package/src/modes/runtime-init.ts +2 -0
  201. package/src/modes/types.ts +17 -3
  202. package/src/modes/utils/ui-helpers.ts +2 -0
  203. package/src/prompts/system/system-prompt.md +8 -0
  204. package/src/prompts/tools/search.md +2 -0
  205. package/src/prompts/tools/subagent.md +1 -0
  206. package/src/runtime-mcp/client.ts +60 -17
  207. package/src/runtime-mcp/manager.ts +189 -14
  208. package/src/runtime-mcp/pool.test.ts +218 -0
  209. package/src/runtime-mcp/pool.ts +224 -23
  210. package/src/runtime-mcp/transports/stdio.ts +158 -24
  211. package/src/runtime-mcp/types.ts +24 -0
  212. package/src/sdk/broker/broker.ts +124 -32
  213. package/src/sdk/broker/ensure.ts +248 -41
  214. package/src/sdk/broker/lifecycle-ledger.ts +7 -17
  215. package/src/sdk/broker/lifecycle.ts +56 -6
  216. package/src/sdk/broker/startup-failure.ts +15 -6
  217. package/src/sdk/bus/chat-daemon-control.ts +3 -1
  218. package/src/sdk/bus/chat-daemon-runtime.ts +72 -6
  219. package/src/sdk/bus/chat-effect-journal.ts +132 -7
  220. package/src/sdk/bus/conversation-store.ts +29 -0
  221. package/src/sdk/bus/lifecycle-commands.ts +17 -11
  222. package/src/sdk/bus/slack-conversation.ts +1 -0
  223. package/src/sdk/bus/slack-daemon.ts +478 -49
  224. package/src/sdk/bus/slack-live-provider.ts +12 -0
  225. package/src/sdk/bus/slack-provider.ts +5 -0
  226. package/src/sdk/bus/telegram-daemon.ts +16 -2
  227. package/src/sdk/bus/topic-registry.ts +19 -0
  228. package/src/sdk/cli/master-cli.ts +1 -1
  229. package/src/sdk/cli/session-cli.ts +11 -8
  230. package/src/sdk/client/client.ts +66 -17
  231. package/src/sdk/host/host.ts +29 -9
  232. package/src/sdk/host/query/revision-store.ts +19 -13
  233. package/src/sdk/host/reverse-leases.ts +7 -4
  234. package/src/sdk/host/session-runtime.test.ts +52 -10
  235. package/src/sdk/host/session-runtime.ts +383 -44
  236. package/src/sdk/lifecycle/service.ts +4 -0
  237. package/src/sdk/protocol/operation-inventory.generated.json +77 -0
  238. package/src/sdk/router/session-router.ts +89 -38
  239. package/src/sdk/session-api-key.ts +94 -0
  240. package/src/sdk/session.ts +228 -55
  241. package/src/sdk/transport/relay.ts +35 -13
  242. package/src/sdk/transport/serve-cli.ts +3 -3
  243. package/src/session/agent-session.ts +240 -90
  244. package/src/session/contribution-prep.ts +214 -21
  245. package/src/session/fallback-chain-controller.ts +38 -0
  246. package/src/session/internal/managed-session-storage.ts +19 -5
  247. package/src/session/messages.ts +5 -0
  248. package/src/session/session-manager.ts +29 -5
  249. package/src/session-import/redact.ts +5 -1
  250. package/src/setup/paseo/announce.ts +456 -0
  251. package/src/setup/paseo/setup-deps.ts +15 -3
  252. package/src/skill-state/canonical-skills.ts +12 -0
  253. package/src/skills/skills.test.ts +41 -21
  254. package/src/slash-commands/builtin-registry.ts +1 -0
  255. package/src/system-prompt.ts +29 -3
  256. package/src/task/executor.ts +7 -1
  257. package/src/task/index.ts +21 -4
  258. package/src/tools/bash.ts +63 -19
  259. package/src/tools/cron.ts +1 -1
  260. package/src/tools/fetch.ts +29 -1
  261. package/src/tools/gh.ts +26 -3
  262. package/src/tools/github-cache.ts +68 -16
  263. package/src/tools/index.ts +10 -0
  264. package/src/tools/job.ts +21 -4
  265. package/src/tools/monitor.ts +8 -2
  266. package/src/tools/path-utils.ts +7 -0
  267. package/src/tools/read.ts +14 -2
  268. package/src/tools/render-utils.ts +1 -3
  269. package/src/tools/resource-gc.ts +28 -8
  270. package/src/tools/search.ts +116 -58
  271. package/src/tools/skill-discovery.ts +5 -0
  272. package/src/tools/skill.ts +37 -2
  273. package/src/tools/subagent.ts +6 -3
  274. package/src/tools/tool-catalog.generated.ts +10 -3
  275. package/src/tools/ultragoal-ask-guard.ts +2 -2
  276. package/src/web/insane/bridge.ts +113 -23
  277. package/src/web/search/providers/perplexity.ts +102 -41
@@ -203,6 +203,8 @@ export interface ToolSession {
203
203
  * Runtime discovery only; never threaded into capability loading.
204
204
  */
205
205
  home?: string;
206
+ /** Resolver-owned classification for the session's selected agent directory. */
207
+ profileAuthority?: "default" | "custom";
206
208
  skills?: Skill[];
207
209
  /** Currently executing skill prompt, when this tool session is inside one. */
208
210
  getActiveSkillState?: () => Pick<SkillActiveEntry, "skill" | "session_id"> | undefined;
@@ -249,6 +251,14 @@ export interface ToolSession {
249
251
  waitForUserSteering?: (signal: AbortSignal) => Promise<void>;
250
252
  /** Get session ID */
251
253
  getSessionId?: () => string | null;
254
+ /**
255
+ * Opaque async-job ownership/lookup endpoint key. This is not a workflow or
256
+ * path identity: workflow/session consumers must use getSessionId(). Only
257
+ * AsyncJobManager ownership and endpoint-first lookup may use this accessor
258
+ * (subagent-inherited managers are registered under the parent's endpoint;
259
+ * review thread P1).
260
+ */
261
+ getAsyncEndpointId?: () => string | null;
252
262
  /** Get credential-selection session identity. */
253
263
  getCredentialSessionId?: () => string | null;
254
264
  /** Scope-held MCP facade for mcp:// resolution. */
package/src/tools/job.ts CHANGED
@@ -105,7 +105,9 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
105
105
  ): Promise<AgentToolResult<JobToolDetails>> {
106
106
  const manager =
107
107
  this.session.getAsyncJobManager?.() ??
108
- AsyncJobManager.forEndpoint(this.session.getSessionId?.() ?? undefined) ??
108
+ AsyncJobManager.forEndpoint(
109
+ this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? undefined,
110
+ ) ??
109
111
  AsyncJobManager.instance();
110
112
  if (!manager) {
111
113
  return {
@@ -254,10 +256,21 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
254
256
  },
255
257
  });
256
258
  };
257
- const progressTimer = onUpdate ? setInterval(emitProgress, PROGRESS_INTERVAL_MS) : undefined;
258
- emitProgress();
259
+ const { promise: progressError, reject: rejectProgress } = Promise.withResolvers<never>();
260
+ racePromises.push(progressError);
261
+ let progressTimer: Timer | undefined;
259
262
 
260
263
  try {
264
+ progressTimer = onUpdate
265
+ ? setInterval(() => {
266
+ try {
267
+ emitProgress();
268
+ } catch (error) {
269
+ rejectProgress(error);
270
+ }
271
+ }, PROGRESS_INTERVAL_MS)
272
+ : undefined;
273
+ emitProgress();
261
274
  if (signal) {
262
275
  const { promise: abortPromise, resolve: abortResolve } = Promise.withResolvers<void>();
263
276
  const onAbort = () => abortResolve();
@@ -396,7 +409,11 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
396
409
  const generation = manager.getJob?.(job.id)?.generation;
397
410
  if (!generation) continue;
398
411
  // The endpoint disambiguates concurrent sessions' same job ids (review P1).
399
- const registration = lookupOwnedRegistration(job.id, generation, this.session.getSessionId?.() ?? "local");
412
+ const registration = lookupOwnedRegistration(
413
+ job.id,
414
+ generation,
415
+ this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? "local",
416
+ );
400
417
  if (registration) unregisterOwnedRegistration(registration);
401
418
  }
402
419
 
@@ -119,7 +119,9 @@ export class MonitorTool implements AgentTool<typeof monitorSchema, MonitorToolD
119
119
  // different concurrent top-level session (review thread P1).
120
120
  const manager =
121
121
  this.session.getAsyncJobManager?.() ??
122
- AsyncJobManager.forEndpoint(this.session.getSessionId?.() ?? undefined) ??
122
+ AsyncJobManager.forEndpoint(
123
+ this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? undefined,
124
+ ) ??
123
125
  AsyncJobManager.instance();
124
126
  if (!manager) {
125
127
  throw new ToolError("Async execution is disabled; the monitor tool is unavailable in this session.");
@@ -197,7 +199,11 @@ export class MonitorTool implements AgentTool<typeof monitorSchema, MonitorToolD
197
199
  // endpoint-less fallback scan could attach a foreign session's
198
200
  // registration (review thread P2).
199
201
  const registration = generation
200
- ? lookupOwnedRegistration(jobId, generation, this.session.getSessionId?.() ?? "local")
202
+ ? lookupOwnedRegistration(
203
+ jobId,
204
+ generation,
205
+ this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? "local",
206
+ )
201
207
  : undefined;
202
208
  const ownedCompletion = registration
203
209
  ? {
@@ -696,6 +696,8 @@ export interface ToolScopeOptions {
696
696
  surfaceExactFilePaths?: boolean;
697
697
  /** Extra hint appended to "Path not found" when stat fails and the user supplied multiple paths. */
698
698
  multipathStatHint?: string;
699
+ /** Cancels internal-URL resolution and prevents further scope work. */
700
+ signal?: AbortSignal;
699
701
  }
700
702
 
701
703
  export interface ToolScopeResolution {
@@ -719,6 +721,7 @@ export interface ToolScopeResolution {
719
721
  */
720
722
  export async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<ToolScopeResolution> {
721
723
  const { rawPaths: inputs, cwd, internalUrlAction, getArtifactsDir, getAuthorizedArtifactsDirs } = opts;
724
+ opts.signal?.throwIfAborted();
722
725
  const rawPaths = inputs.map(normalizePathLikeInput);
723
726
  if (rawPaths.some(rawPath => rawPath.length === 0)) {
724
727
  throw new ToolError("`paths` must contain non-empty paths or globs");
@@ -727,6 +730,7 @@ export async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<To
727
730
  const resolvedPathInputs: string[] = [];
728
731
  const immutableSourcePaths = new Set<string>();
729
732
  for (const rawPath of rawPaths) {
733
+ opts.signal?.throwIfAborted();
730
734
  if (!internalRouter.canHandle(rawPath)) {
731
735
  resolvedPathInputs.push(rawPath);
732
736
  continue;
@@ -739,7 +743,9 @@ export async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<To
739
743
  getArtifactsDir,
740
744
  getAuthorizedArtifactsDirs,
741
745
  mcpManager: opts.mcpManager,
746
+ signal: opts.signal,
742
747
  });
748
+ opts.signal?.throwIfAborted();
743
749
  if (!resource.sourcePath) {
744
750
  throw new ToolError(`Cannot ${internalUrlAction} internal URL without a backing file: ${rawPath}`);
745
751
  }
@@ -753,6 +759,7 @@ export async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<To
753
759
  let effectivePaths = resolvedPathInputs;
754
760
  if (resolvedPathInputs.length > 1) {
755
761
  const partition = await partitionExistingPaths(resolvedPathInputs, cwd, parseSearchPath);
762
+ opts.signal?.throwIfAborted();
756
763
  if (partition.valid.length === 0) {
757
764
  throw new ToolError(`Path not found: ${partition.missing.join(", ")}`);
758
765
  }
package/src/tools/read.ts CHANGED
@@ -818,10 +818,22 @@ export async function streamLinesFromFile(
818
818
  fitBytes += separatorBytes + currentLineLength;
819
819
  } else {
820
820
  transitionToSplit();
821
- pushSplit(lineIndex, lineText, currentLineLength, currentLineDropped, capturePartialPreview());
821
+ pushSplit(
822
+ lineIndex,
823
+ lineText,
824
+ currentLineLength,
825
+ currentLineDropped,
826
+ currentLineDropped || currentLineLength > ringBudgetBytes ? capturePartialPreview() : undefined,
827
+ );
822
828
  }
823
829
  } else {
824
- pushSplit(lineIndex, lineText, currentLineLength, currentLineDropped, capturePartialPreview());
830
+ pushSplit(
831
+ lineIndex,
832
+ lineText,
833
+ currentLineLength,
834
+ currentLineDropped,
835
+ currentLineDropped || currentLineLength > ringBudgetBytes ? capturePartialPreview() : undefined,
836
+ );
825
837
  }
826
838
  }
827
839
 
@@ -308,6 +308,7 @@ export function formatDiagnostics(
308
308
  // Count total diagnostics for "... X more" calculation
309
309
  const totalParsedDiags = files.reduce((sum, [, diags]) => sum + diags.length, 0);
310
310
  const totalDiags = totalParsedDiags + unparsed.length;
311
+ const willShowAllRemaining = totalDiags <= maxDiags;
311
312
 
312
313
  // Helper to check if this is the very last item in the tree
313
314
  const isTreeEnd = (fileIdx: number, diagIdx: number | null, unparsedIdx: number | null): boolean => {
@@ -329,9 +330,6 @@ export function formatDiagnostics(
329
330
  for (let fi = 0; fi < files.length && diagsShown < maxDiags; fi++) {
330
331
  const [filePath, diagnostics] = files[fi];
331
332
  // File is "last" only if no more files AND no unparsed AND we'll show all diags AND no "... X more"
332
- const remainingDiagsInFile = diagnostics.length;
333
- const remainingDiagsAfter = files.slice(fi + 1).reduce((sum, [, d]) => sum + d.length, 0) + unparsed.length;
334
- const willShowAllRemaining = diagsShown + remainingDiagsInFile + remainingDiagsAfter <= maxDiags;
335
333
  const isLastFileNode = fi === files.length - 1 && unparsed.length === 0 && willShowAllRemaining;
336
334
  const fileBranch = isLastFileNode ? theme.tree.last : theme.tree.branch;
337
335
 
@@ -106,7 +106,10 @@ const defaultDeps: ResourceGcDeps = {
106
106
  };
107
107
 
108
108
  // ── Controller state (process-global; tabs/browsers are module-global too) ──────────────────
109
- const activeSessions = new Map<string, { settings: Settings; cwd: () => string }>();
109
+ const activeSessions = new Map<
110
+ string,
111
+ { settings: Settings; cwd: () => string; memoryGuardGeneration: symbol; memoryGuardEnabled: boolean }
112
+ >();
110
113
  const scheduler = new MemoryGuardHost({
111
114
  run: async () => {
112
115
  await sweepOnce(deps);
@@ -141,12 +144,26 @@ function resolveSessionSweepIntervalMs(settings: Settings): number {
141
144
  export function registerResourceGcSession(reg: ResourceGcRegistration): () => void {
142
145
  const registeredCwd = reg.cwd;
143
146
  const cwd = typeof registeredCwd === "function" ? registeredCwd : () => registeredCwd ?? process.cwd();
144
- activeSessions.set(reg.sessionId, { settings: reg.settings, cwd });
147
+ const registration = {
148
+ settings: reg.settings,
149
+ cwd,
150
+ memoryGuardGeneration: Symbol(),
151
+ memoryGuardEnabled: resolveMemoryGuardPolicy(reg.settings).enabled,
152
+ };
153
+ activeSessions.set(reg.sessionId, registration);
145
154
  const unregisterSchedule = scheduler.register({
146
155
  ownerId: reg.sessionId,
147
156
  intervalMs: resolveSessionSweepIntervalMs(reg.settings),
148
157
  });
149
158
  const unregisterSettings = reg.settings.onChanged(path => {
159
+ if (path === "memoryGuard.enabled") {
160
+ const enabled = resolveMemoryGuardPolicy(reg.settings).enabled;
161
+ if (enabled !== registration.memoryGuardEnabled) {
162
+ // Fence samples already in flight, even if the guard is re-enabled before they settle.
163
+ registration.memoryGuardGeneration = Symbol();
164
+ registration.memoryGuardEnabled = enabled;
165
+ }
166
+ }
150
167
  if (
151
168
  path === "memoryGuard.enabled" ||
152
169
  path === "memoryGuard.checkIntervalMs" ||
@@ -498,8 +515,8 @@ function sweepMemoryPressureGuard(d: ResourceGcDeps): Promise<void> | undefined
498
515
  }
499
516
  async function sweepEnabledMemoryPressureGuard(d: ResourceGcDeps): Promise<void> {
500
517
  const now = d.monotonicNow();
501
- const dueSessions: Array<{ sessionId: string; policy: MemoryGuardPolicy; cwd: string }> = [];
502
- for (const [sessionId, { settings, cwd: resolveCwd }] of activeSessions) {
518
+ const dueSessions: Array<{ sessionId: string; policy: MemoryGuardPolicy; cwd: string; generation: symbol }> = [];
519
+ for (const [sessionId, { settings, cwd: resolveCwd, memoryGuardGeneration }] of activeSessions) {
503
520
  const policy = resolveMemoryGuardPolicy(settings);
504
521
  const cwd = resolveCwd();
505
522
  if (!policy.enabled) {
@@ -511,14 +528,15 @@ async function sweepEnabledMemoryPressureGuard(d: ResourceGcDeps): Promise<void>
511
528
  const lastEvaluated = memoryGuardLastEvaluatedAt.get(sessionId);
512
529
  if (lastEvaluated !== undefined && now - lastEvaluated < policy.checkIntervalMs) continue;
513
530
  memoryGuardLastEvaluatedAt.set(sessionId, now);
514
- dueSessions.push({ sessionId, policy, cwd });
531
+ dueSessions.push({ sessionId, policy, cwd, generation: memoryGuardGeneration });
515
532
  }
516
533
  if (dueSessions.length === 0) return;
517
534
 
518
535
  const snapshot = await d.memorySnapshot();
519
536
  let gcRequested = false;
520
- const gcTelemetry: Array<{ sessionId: string } & Record<string, unknown>> = [];
521
- for (const { sessionId, policy } of dueSessions) {
537
+ const gcTelemetry: Array<{ sessionId: string; generation: symbol } & Record<string, unknown>> = [];
538
+ for (const { sessionId, policy, generation } of dueSessions) {
539
+ if (activeSessions.get(sessionId)?.memoryGuardGeneration !== generation) continue;
522
540
  const pressure = __selectMemoryPressureDomainForTest(snapshot, policy.policyLimitBytes);
523
541
  const limit = resolveEffectiveMemoryLimit({
524
542
  hardCapBytes: pressure.hardCapBytes,
@@ -548,6 +566,7 @@ async function sweepEnabledMemoryPressureGuard(d: ResourceGcDeps): Promise<void>
548
566
  gcRequested = true;
549
567
  gcTelemetry.push({
550
568
  sessionId,
569
+ generation,
551
570
  parentBytes: pressure.parentBytes,
552
571
  totalUsageBytes: pressure.totalUsageBytes,
553
572
  effectiveLimitBytes: limit.effectiveBytes,
@@ -587,7 +606,8 @@ async function sweepEnabledMemoryPressureGuard(d: ResourceGcDeps): Promise<void>
587
606
  if (gcRequested) {
588
607
  try {
589
608
  d.runGc();
590
- for (const { sessionId, ...telemetry } of gcTelemetry) {
609
+ for (const { sessionId, generation, ...telemetry } of gcTelemetry) {
610
+ if (activeSessions.get(sessionId)?.memoryGuardGeneration !== generation) continue;
591
611
  memoryGuardGcActive.add(sessionId);
592
612
  d.logWarn("Memory guard: GC threshold reached", { sessionId, ...telemetry });
593
613
  }
@@ -5,7 +5,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
5
5
  import type { GrepMatch, GrepResult, grep as grepFn } from "@gajae-code/natives";
6
6
  import type { Component } from "@gajae-code/tui";
7
7
  import { Text } from "@gajae-code/tui";
8
- import { prompt, untilAborted } from "@gajae-code/utils";
8
+ import { prompt } from "@gajae-code/utils";
9
9
  import * as z from "zod/v4";
10
10
  import { getFileReadCache } from "../edit/file-read-cache";
11
11
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
@@ -35,7 +35,7 @@ import {
35
35
  PREVIEW_LIMITS,
36
36
  splitGroupsByBlankLine,
37
37
  } from "./render-utils";
38
- import { ToolError } from "./tool-errors";
38
+ import { ToolAbortError, ToolError } from "./tool-errors";
39
39
  import { toolResult } from "./tool-result";
40
40
 
41
41
  let searchNativesLoad:
@@ -71,6 +71,7 @@ const searchSchema = z
71
71
  .number()
72
72
  .optional()
73
73
  .describe("files to skip before collecting results — use to paginate when the prior call hit the file limit"),
74
+ timeout: z.number().min(0.5).max(60).default(5).describe("timeout in seconds (0.5–60)").optional(),
74
75
  })
75
76
  .strict();
76
77
 
@@ -90,6 +91,9 @@ export const SINGLE_FILE_MATCHES = 200;
90
91
  * (DEFAULT_FILE_LIMIT files × MULTI_FILE_PER_FILE_MATCHES matches) plus
91
92
  * pagination headroom so the caller can see total file count. */
92
93
  const INTERNAL_TOTAL_CAP = 2000;
94
+ const DEFAULT_SEARCH_TIMEOUT_MS = 5000;
95
+ const MIN_SEARCH_TIMEOUT_MS = 500;
96
+ const MAX_SEARCH_TIMEOUT_MS = 60_000;
93
97
 
94
98
  /**
95
99
  * Detect a `,` that is not inside a `{…}` brace expansion. Used to catch
@@ -130,6 +134,7 @@ function containsTopLevelComma(entry: string): boolean {
130
134
  async function resolveArchiveSearchPaths(
131
135
  paths: string[],
132
136
  cwd: string,
137
+ signal?: AbortSignal,
133
138
  ): Promise<{
134
139
  resolvedPaths: string[];
135
140
  displayMap: Map<string, string>;
@@ -143,64 +148,71 @@ async function resolveArchiveSearchPaths(
143
148
  const unreadable: string[] = [];
144
149
  let tempDir: string | undefined;
145
150
  const archiveCache = new Map<string, ArchiveReader>();
151
+ const cleanup = async () => {
152
+ const directory = tempDir;
153
+ tempDir = undefined;
154
+ if (directory) await rm(directory, { recursive: true, force: true }).catch(() => {});
155
+ };
146
156
 
147
- for (let idx = 0; idx < paths.length; idx++) {
148
- const entry = paths[idx];
149
- const candidates = parseArchivePathCandidates(entry);
150
- // Longest archive prefix first; we want the one whose member portion is non-empty.
151
- const member = candidates.find(c => c.subPath !== "" && c.archivePath !== entry);
152
- if (!member) continue;
157
+ try {
158
+ for (let idx = 0; idx < paths.length; idx++) {
159
+ signal?.throwIfAborted();
160
+ const entry = paths[idx];
161
+ const candidates = parseArchivePathCandidates(entry);
162
+ // Longest archive prefix first; we want the one whose member portion is non-empty.
163
+ const member = candidates.find(c => c.subPath !== "" && c.archivePath !== entry);
164
+ if (!member) continue;
165
+
166
+ const archiveAbs = resolveReadPath(member.archivePath, cwd);
167
+ let archive = archiveCache.get(archiveAbs);
168
+ if (!archive) {
169
+ try {
170
+ archive = await openArchive(archiveAbs);
171
+ } catch (err) {
172
+ unreadable.push(`${entry} (cannot open archive: ${(err as Error).message})`);
173
+ continue;
174
+ }
175
+ archiveCache.set(archiveAbs, archive);
176
+ }
153
177
 
154
- const archiveAbs = resolveReadPath(member.archivePath, cwd);
155
- let archive = archiveCache.get(archiveAbs);
156
- if (!archive) {
178
+ let extracted: ExtractedArchiveFile;
157
179
  try {
158
- archive = await openArchive(archiveAbs);
180
+ extracted = await archive.readFile(member.subPath);
159
181
  } catch (err) {
160
- unreadable.push(`${entry} (cannot open archive: ${(err as Error).message})`);
182
+ unreadable.push(`${entry} (${(err as Error).message})`);
183
+ continue;
184
+ }
185
+ signal?.throwIfAborted();
186
+ // UTF-8 only — binary members would just produce noise through ripgrep.
187
+ if (extracted.bytes.some(byte => byte === 0)) {
188
+ unreadable.push(`${entry} (binary archive entry)`);
189
+ continue;
190
+ }
191
+ let text: string;
192
+ try {
193
+ text = new TextDecoder("utf-8", { fatal: true }).decode(extracted.bytes);
194
+ } catch {
195
+ unreadable.push(`${entry} (non-UTF-8 archive entry)`);
161
196
  continue;
162
197
  }
163
- archiveCache.set(archiveAbs, archive);
164
- }
165
-
166
- let extracted: ExtractedArchiveFile;
167
- try {
168
- extracted = await archive.readFile(member.subPath);
169
- } catch (err) {
170
- unreadable.push(`${entry} (${(err as Error).message})`);
171
- continue;
172
- }
173
- // UTF-8 only — binary members would just produce noise through ripgrep.
174
- if (extracted.bytes.some(byte => byte === 0)) {
175
- unreadable.push(`${entry} (binary archive entry)`);
176
- continue;
177
- }
178
- let text: string;
179
- try {
180
- text = new TextDecoder("utf-8", { fatal: true }).decode(extracted.bytes);
181
- } catch {
182
- unreadable.push(`${entry} (non-UTF-8 archive entry)`);
183
- continue;
184
- }
185
198
 
186
- if (!tempDir) {
187
- tempDir = await mkdtemp(path.join(tmpdir(), "gjc-search-archive-"));
199
+ if (!tempDir) {
200
+ tempDir = await mkdtemp(path.join(tmpdir(), "gjc-search-archive-"));
201
+ }
202
+ // Per-entry filename keeps the scratch path unique even when two selectors
203
+ // resolve to members with the same basename.
204
+ const safeBase = path.basename(member.subPath).replace(/[^\w.-]+/g, "_") || "entry";
205
+ const tempPath = path.join(tempDir, `${idx}-${safeBase}`);
206
+ await writeFile(tempPath, text);
207
+ resolvedPaths[idx] = tempPath;
208
+ displayMap.set(tempPath, entry);
209
+ displaySet.add(entry);
188
210
  }
189
- // Per-entry filename keeps the scratch path unique even when two selectors
190
- // resolve to members with the same basename.
191
- const safeBase = path.basename(member.subPath).replace(/[^\w.-]+/g, "_") || "entry";
192
- const tempPath = path.join(tempDir, `${idx}-${safeBase}`);
193
- await writeFile(tempPath, text);
194
- resolvedPaths[idx] = tempPath;
195
- displayMap.set(tempPath, entry);
196
- displaySet.add(entry);
211
+ } catch (error) {
212
+ await cleanup();
213
+ throw error;
197
214
  }
198
215
 
199
- const cleanup = async () => {
200
- if (tempDir) {
201
- await rm(tempDir, { recursive: true, force: true }).catch(() => {});
202
- }
203
- };
204
216
  return { resolvedPaths, displayMap, displaySet, unreadable, cleanup };
205
217
  }
206
218
 
@@ -235,6 +247,8 @@ export interface SearchToolOptions {
235
247
  context?: number;
236
248
  /** Per-call total match cap used by Cursor's native Pi grep frame. */
237
249
  totalMatchLimit?: number;
250
+ /** Test or remote override for the native grep implementation. */
251
+ grep?: typeof grepFn;
238
252
  }
239
253
 
240
254
  type SearchParams = z.infer<typeof searchSchema>;
@@ -266,12 +280,23 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
266
280
  _onUpdate?: AgentToolUpdateCallback<SearchToolDetails>,
267
281
  _toolContext?: AgentToolContext,
268
282
  ): Promise<AgentToolResult<SearchToolDetails>> {
269
- const { pattern, i, gitignore, skip } = params;
283
+ const { pattern, i, gitignore, skip, timeout } = params;
270
284
  // Recurring real-world failure mode: models omit `paths` for repo-wide
271
285
  // searches. Default to the working directory instead of failing the call.
272
286
  const paths = params.paths ?? ["."];
287
+ const requestedTimeoutMs = timeout != null ? Math.round(timeout * 1000) : DEFAULT_SEARCH_TIMEOUT_MS;
288
+ const timeoutMs = Math.min(MAX_SEARCH_TIMEOUT_MS, Math.max(MIN_SEARCH_TIMEOUT_MS, requestedTimeoutMs));
289
+ const deadlineAt = Date.now() + timeoutMs;
290
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
291
+ const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
292
+ const throwIfSearchCancelled = (): void => {
293
+ signal?.throwIfAborted();
294
+ if (Date.now() >= deadlineAt) throw new Error("Aborted: Timeout");
295
+ combinedSignal.throwIfAborted();
296
+ };
273
297
 
274
- return untilAborted(signal, async () => {
298
+ try {
299
+ throwIfSearchCancelled();
275
300
  const normalizedPattern = pattern.trim();
276
301
  if (!normalizedPattern) {
277
302
  throw new ToolError("Pattern must not be empty");
@@ -292,8 +317,10 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
292
317
  displaySet: archiveDisplaySet,
293
318
  unreadable: archiveUnreadable,
294
319
  cleanup: cleanupArchiveScratch,
295
- } = await resolveArchiveSearchPaths(paths, this.session.cwd);
320
+ } = await resolveArchiveSearchPaths(paths, this.session.cwd, combinedSignal);
321
+ let completedResult: AgentToolResult<SearchToolDetails> | undefined;
296
322
  try {
323
+ throwIfSearchCancelled();
297
324
  if (archiveUnreadable.length > 0 && resolvedPaths.length === archiveUnreadable.length) {
298
325
  // All inputs were archive selectors we couldn't materialize; surface the
299
326
  // reason instead of a downstream "path not found" from the scope resolver.
@@ -324,7 +351,9 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
324
351
  trackImmutableSources: true,
325
352
  surfaceExactFilePaths: true,
326
353
  multipathStatHint: " (`paths` entries must each exist relative to cwd)",
354
+ signal: combinedSignal,
327
355
  });
356
+ throwIfSearchCancelled();
328
357
  const { searchPath, isDirectory, multiTargets, exactFilePaths, missingPaths, immutableSourcePaths } = scope;
329
358
  // When the only input was an archive selector, surface that selector instead
330
359
  // of the temp scratch path the resolver substituted in.
@@ -345,7 +374,8 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
345
374
  const baseDisplayMode = resolveFileDisplayMode(this.session);
346
375
  const immutableDisplayMode = resolveFileDisplayMode(this.session, { immutable: true });
347
376
 
348
- const { GrepOutputMode, grep } = await searchNatives();
377
+ const { GrepOutputMode, grep: nativeGrep } = await searchNatives();
378
+ const grep = this.options.grep ?? nativeGrep;
349
379
  const effectiveOutputMode = GrepOutputMode.Content;
350
380
  // Multi-scope = more than one file may match. We fetch up to
351
381
  // INTERNAL_TOTAL_CAP matches from native grep, then in JS group by
@@ -363,7 +393,10 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
363
393
  let totalMatches = 0;
364
394
  let filesSearched = 0;
365
395
  const targets = exactFilePaths
366
- ? exactFilePaths.map(filePath => ({ basePath: filePath, glob: undefined as string | undefined }))
396
+ ? exactFilePaths.map(filePath => ({
397
+ basePath: filePath,
398
+ glob: undefined as string | undefined,
399
+ }))
367
400
  : (multiTargets ?? []);
368
401
  for (const target of targets) {
369
402
  const targetResult = await grep(
@@ -381,9 +414,12 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
381
414
  contextAfter: normalizedContextAfter,
382
415
  maxColumns: DEFAULT_MAX_COLUMN,
383
416
  mode: effectiveOutputMode,
417
+ signal: combinedSignal,
418
+ timeoutMs,
384
419
  },
385
420
  undefined,
386
421
  );
422
+ throwIfSearchCancelled();
387
423
  limitReached = limitReached || Boolean(targetResult.limitReached);
388
424
  totalMatches += targetResult.totalMatches;
389
425
  filesSearched += targetResult.filesSearched;
@@ -416,9 +452,12 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
416
452
  contextAfter: normalizedContextAfter,
417
453
  maxColumns: DEFAULT_MAX_COLUMN,
418
454
  mode: effectiveOutputMode,
455
+ signal: combinedSignal,
456
+ timeoutMs,
419
457
  },
420
458
  undefined,
421
459
  );
460
+ throwIfSearchCancelled();
422
461
  }
423
462
  } catch (err) {
424
463
  if (err instanceof Error && err.message.startsWith("regex parse error")) {
@@ -518,7 +557,10 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
518
557
  missingPaths: missingPaths.length > 0 ? missingPaths : undefined,
519
558
  };
520
559
  const text = warningNote ? `No matches found\n${warningNote}` : "No matches found";
521
- return toolResult(details).text(text).done();
560
+ const noMatchResult = toolResult(details).text(text).done();
561
+ await cleanupArchiveScratch();
562
+ throwIfSearchCancelled();
563
+ return noMatchResult;
522
564
  }
523
565
  const outputLines: string[] = [];
524
566
  let linesTruncated = false;
@@ -644,11 +686,25 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
644
686
  if (truncation.truncated) {
645
687
  resultBuilder.truncation(truncation, { direction: "head" });
646
688
  }
647
- return resultBuilder.done();
689
+ throwIfSearchCancelled();
690
+ completedResult = resultBuilder.done();
648
691
  } finally {
649
692
  await cleanupArchiveScratch();
650
693
  }
651
- });
694
+ throwIfSearchCancelled();
695
+ if (!completedResult) throw new Error("Search result assembly did not complete");
696
+ return completedResult;
697
+ } catch (error) {
698
+ const nativeTimedOut = error instanceof Error && error.message === "Aborted: Timeout";
699
+ if ((timeoutSignal.aborted || nativeTimedOut) && !signal?.aborted) {
700
+ const seconds = timeoutMs % 1000 === 0 ? `${timeoutMs / 1000}` : (timeoutMs / 1000).toFixed(1);
701
+ throw new ToolError(`Search timed out after ${seconds}s; increase timeout or narrow paths/pattern`);
702
+ }
703
+ if (combinedSignal.aborted || (error instanceof Error && error.name === "AbortError")) {
704
+ throw new ToolAbortError();
705
+ }
706
+ throw error;
707
+ }
652
708
  }
653
709
  }
654
710
 
@@ -662,6 +718,7 @@ interface SearchRenderArgs {
662
718
  i?: boolean;
663
719
  gitignore?: boolean;
664
720
  skip?: number;
721
+ timeout?: number;
665
722
  }
666
723
 
667
724
  const COLLAPSED_TEXT_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
@@ -674,6 +731,7 @@ export const searchToolRenderer = {
674
731
  if (args.i) meta.push("case:insensitive");
675
732
  if (args.gitignore === false) meta.push("gitignore:false");
676
733
  if (args.skip !== undefined && args.skip > 0) meta.push(`skip:${args.skip}`);
734
+ if (args.timeout !== undefined && args.timeout !== 5) meta.push(`timeout:${args.timeout}s`);
677
735
 
678
736
  const text = renderStatusLine(
679
737
  { icon: "pending", title: "Search", description: args.pattern || "?", meta },
@@ -76,9 +76,14 @@ export class SkillDiscoveryTool implements AgentTool<typeof skillDiscoverySchema
76
76
  ): Promise<AgentToolResult<SkillDiscoveryToolDetails>> {
77
77
  return untilAborted(signal, async () => {
78
78
  const source = input.source ?? "all";
79
+ const agentDir =
80
+ this.#session.getSessionAgentDir?.() ??
81
+ (this.#session.home === undefined ? this.#session.settings.getAgentDir() : undefined);
79
82
  const result = await discoverRuntimeSkills({
80
83
  cwd: this.#session.cwd,
81
84
  home: this.#session.home,
85
+ agentDir,
86
+ profileAuthority: this.#session.profileAuthority,
82
87
  query: input.query,
83
88
  source,
84
89
  limit: input.limit,