@gajae-code/coding-agent 0.16.1 → 0.16.4
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.
- package/CHANGELOG.md +70 -0
- package/dist/types/cli/args.d.ts +2 -2
- package/dist/types/cli/root-flags.d.ts +4 -2
- package/dist/types/cli.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/config/autorouting-tier-map.d.ts +3 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/model-resolver.d.ts +9 -0
- package/dist/types/config/settings-schema.d.ts +29 -1
- package/dist/types/discovery/claude-plugins.d.ts +3 -1
- package/dist/types/discovery/helpers.d.ts +2 -0
- package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +1 -1
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -2
- package/dist/types/modes/components/settings-selector.d.ts +6 -1
- package/dist/types/modes/components/status-line/command.d.ts +35 -0
- package/dist/types/modes/components/status-line/index.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +5 -0
- package/dist/types/modes/components/tool-status-header.d.ts +14 -0
- package/dist/types/modes/controllers/event-controller.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/runtime-mcp/client.d.ts +9 -1
- package/dist/types/runtime-mcp/manager.d.ts +7 -1
- package/dist/types/runtime-mcp/pool.d.ts +22 -3
- package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
- package/dist/types/runtime-mcp/types.d.ts +22 -0
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
- package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
- package/dist/types/sdk/cli/rows.d.ts +7 -0
- package/dist/types/sdk/host/events.d.ts +1 -0
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/session-runtime.d.ts +23 -0
- package/dist/types/sdk/session-api-key.d.ts +26 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +9 -0
- package/dist/types/setup/paseo/announce.d.ts +111 -0
- package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +2 -1
- package/dist/types/tools/fetch.d.ts +7 -0
- package/dist/types/tools/index.d.ts +8 -2
- package/dist/types/tools/path-utils.d.ts +2 -0
- package/dist/types/tools/search.d.ts +6 -0
- package/dist/types/utils/external-editor.d.ts +5 -0
- package/package.json +7 -7
- package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
- package/scripts/generate-sdk-operation-inventory.ts +9 -0
- package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
- package/src/cli/args.ts +20 -9
- package/src/cli/root-flags.ts +5 -3
- package/src/config/autorouting-tier-map.ts +1 -0
- package/src/config/file-lock.ts +181 -17
- package/src/config/model-preset-registry.ts +5 -3
- package/src/config/model-profiles.ts +40 -0
- package/src/config/model-registry.ts +57 -11
- package/src/config/model-resolver.ts +48 -0
- package/src/config/settings-schema.ts +33 -1
- package/src/config/settings.ts +1 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
- package/src/discovery/builtin.ts +13 -4
- package/src/discovery/claude-plugins.ts +12 -4
- package/src/discovery/helpers.ts +10 -2
- package/src/extensibility/gjc-plugins/compiler.ts +208 -58
- package/src/extensibility/gjc-plugins/installer.ts +13 -12
- package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
- package/src/extensibility/gjc-plugins/paths.ts +13 -0
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
- package/src/extensibility/runtime-skill-discovery.ts +77 -151
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skills.ts +44 -16
- package/src/gjc-runtime/repository-binding.ts +1 -1
- package/src/gjc-runtime/session-state-lock.ts +94 -6
- package/src/internal-urls/docs-index.generated.ts +10 -10
- package/src/main.ts +72 -34
- package/src/modes/DESIGN.md +54 -0
- package/src/modes/acp/acp-agent.ts +29 -14
- package/src/modes/components/assistant-message.ts +22 -9
- package/src/modes/components/model-selector.ts +166 -33
- package/src/modes/components/settings-selector.ts +1075 -235
- package/src/modes/components/status-line/command.ts +190 -0
- package/src/modes/components/status-line/index.ts +1 -0
- package/src/modes/components/status-line/segments.ts +32 -5
- package/src/modes/components/status-line/types.ts +5 -0
- package/src/modes/components/tool-status-header.ts +393 -21
- package/src/modes/controllers/command-controller.ts +2 -0
- package/src/modes/controllers/event-controller.ts +148 -5
- package/src/modes/controllers/selector-controller.ts +21 -0
- package/src/modes/interactive-mode.ts +47 -1
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/system/system-prompt.md +8 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/job.md +1 -1
- package/src/prompts/tools/search.md +2 -0
- package/src/runtime-mcp/client.ts +60 -17
- package/src/runtime-mcp/manager.ts +171 -14
- package/src/runtime-mcp/pool.test.ts +209 -0
- package/src/runtime-mcp/pool.ts +224 -23
- package/src/runtime-mcp/transports/stdio.ts +158 -24
- package/src/runtime-mcp/types.ts +24 -0
- package/src/sdk/acp/adapter.ts +1 -1
- package/src/sdk/broker/broker.ts +5 -4
- package/src/sdk/bus/chat-daemon-control.ts +1 -1
- package/src/sdk/bus/chat-daemon-runtime.ts +44 -1
- package/src/sdk/bus/slack-daemon.ts +92 -2
- package/src/sdk/bus/slack-live-provider.ts +12 -0
- package/src/sdk/bus/slack-provider.ts +5 -0
- package/src/sdk/cli/rows.ts +47 -2
- package/src/sdk/cli/session-cli.ts +47 -23
- package/src/sdk/host/events.ts +25 -2
- package/src/sdk/host/host.ts +4 -1
- package/src/sdk/host/session-runtime.test.ts +36 -25
- package/src/sdk/host/session-runtime.ts +279 -43
- package/src/sdk/protocol/operation-inventory.generated.json +66 -0
- package/src/sdk/router/session-router.ts +60 -37
- package/src/sdk/session-api-key.ts +94 -0
- package/src/sdk/session.ts +73 -30
- package/src/sdk/transport/serve-cli.ts +3 -3
- package/src/session/agent-session.ts +123 -38
- package/src/session/contribution-prep.ts +214 -21
- package/src/setup/paseo/announce.ts +450 -0
- package/src/setup/paseo/setup-deps.ts +15 -3
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/system-prompt.ts +4 -0
- package/src/task/executor.ts +2 -1
- package/src/task/index.ts +17 -4
- package/src/tools/bash.ts +50 -21
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +29 -1
- package/src/tools/index.ts +8 -2
- package/src/tools/job.ts +8 -2
- package/src/tools/monitor.ts +8 -2
- package/src/tools/path-utils.ts +7 -0
- package/src/tools/search.ts +116 -58
- package/src/tools/skill.ts +22 -2
- package/src/tools/subagent.ts +3 -1
- package/src/tools/tool-catalog.generated.ts +9 -2
- package/src/utils/external-editor.ts +16 -2
- package/src/web/insane/bridge.ts +113 -23
- package/src/web/search/providers/perplexity.ts +102 -41
package/src/tools/bash.ts
CHANGED
|
@@ -1107,7 +1107,12 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1107
1107
|
manager.failNow(jobId, jobGeneration, "Bash job owner was torn down.");
|
|
1108
1108
|
});
|
|
1109
1109
|
void completion.promise.finally(unregisterOwnerCleanup);
|
|
1110
|
-
registerOwnedIfLineaged(
|
|
1110
|
+
registerOwnedIfLineaged(
|
|
1111
|
+
manager,
|
|
1112
|
+
options.toolCallId,
|
|
1113
|
+
jobId,
|
|
1114
|
+
this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? undefined,
|
|
1115
|
+
);
|
|
1111
1116
|
|
|
1112
1117
|
return {
|
|
1113
1118
|
jobId,
|
|
@@ -1132,27 +1137,29 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1132
1137
|
* running (review thread P1).
|
|
1133
1138
|
*/
|
|
1134
1139
|
#resolveOwnedJobManager(): AsyncJobManager | undefined {
|
|
1135
|
-
const endpointId = this.session.getSessionId?.() ?? undefined;
|
|
1140
|
+
const endpointId = this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? undefined;
|
|
1136
1141
|
if (this.session.getAsyncJobManager) return this.session.getAsyncJobManager();
|
|
1137
1142
|
return AsyncJobManager.forEndpoint(endpointId) ?? AsyncJobManager.instance();
|
|
1138
1143
|
}
|
|
1139
1144
|
|
|
1140
1145
|
/**
|
|
1141
1146
|
* Whether a queued user steer may fold the running foreground wait. Mirrors
|
|
1142
|
-
* the loop's
|
|
1143
|
-
*
|
|
1144
|
-
*
|
|
1145
|
-
*
|
|
1146
|
-
*
|
|
1147
|
+
* the loop's steer admission: `busyPromptMode=queue` never admits a steer
|
|
1148
|
+
* into the busy run, so it never folds. `toolInterruptPolicy` is
|
|
1149
|
+
* deliberately NOT a gate: `finish_tools` means "do not kill the batch to
|
|
1150
|
+
* deliver a steer", and a fold kills nothing. It is the one way to deliver
|
|
1151
|
+
* the steer now AND let the command finish, so it applies under both
|
|
1152
|
+
* policies; the policy only decides what the loop does with sibling tools
|
|
1153
|
+
* after the fold returns. The auto-background setting is likewise not
|
|
1154
|
+
* consulted: like the chord, a steer fold is a user action.
|
|
1147
1155
|
*
|
|
1148
|
-
* Fails closed: a session that cannot
|
|
1149
|
-
*
|
|
1156
|
+
* Fails closed: a session that cannot report newly admitted steering or
|
|
1157
|
+
* accept a fold request is not steer-foldable.
|
|
1150
1158
|
*/
|
|
1151
1159
|
#steerFoldEnabled(): boolean {
|
|
1152
|
-
const { waitForUserSteering,
|
|
1153
|
-
if (!waitForUserSteering || !
|
|
1154
|
-
|
|
1155
|
-
return getToolInterruptPolicy() === "abort_tools";
|
|
1160
|
+
const { waitForUserSteering, requestForegroundBashBackground } = this.session;
|
|
1161
|
+
if (!waitForUserSteering || !requestForegroundBashBackground) return false;
|
|
1162
|
+
return this.session.settings.get("busyPromptMode") === "steer";
|
|
1156
1163
|
}
|
|
1157
1164
|
|
|
1158
1165
|
/**
|
|
@@ -1161,8 +1168,8 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1161
1168
|
* when the wait starts, or one that arrives inside the grace window, never
|
|
1162
1169
|
* folds: the command finishes normally and that steer is consumed at the
|
|
1163
1170
|
* ordinary tool boundary. The watcher keeps observing so a later qualifying
|
|
1164
|
-
* steer still folds, and re-checks the
|
|
1165
|
-
* `busyPromptMode
|
|
1171
|
+
* steer still folds, and re-checks the gate at that moment so a
|
|
1172
|
+
* `busyPromptMode` change during the command is honored.
|
|
1166
1173
|
* Returns a stop function; call it when the wait settles. No-op when steer
|
|
1167
1174
|
* folding is gated off at start.
|
|
1168
1175
|
*/
|
|
@@ -1679,7 +1686,12 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1679
1686
|
// Monitor jobs are exact owned background work of the turn that started
|
|
1680
1687
|
// them: register the five-tuple so scope:"owned" terminal abort stops the
|
|
1681
1688
|
// monitor too (review thread P2).
|
|
1682
|
-
registerOwnedIfLineaged(
|
|
1689
|
+
registerOwnedIfLineaged(
|
|
1690
|
+
manager,
|
|
1691
|
+
opts.toolCallId,
|
|
1692
|
+
jobId,
|
|
1693
|
+
this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? undefined,
|
|
1694
|
+
);
|
|
1683
1695
|
currentJobId = jobId;
|
|
1684
1696
|
return { jobId, label, commandCwd: prepared.commandCwd };
|
|
1685
1697
|
}
|
|
@@ -1881,19 +1893,31 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1881
1893
|
}
|
|
1882
1894
|
if (waitResult.kind === "completed") {
|
|
1883
1895
|
ownedManager.acknowledgeDeliveries([job.jobId]);
|
|
1884
|
-
unregisterForegroundOwnedBash(
|
|
1896
|
+
unregisterForegroundOwnedBash(
|
|
1897
|
+
ownedManager,
|
|
1898
|
+
job.jobId,
|
|
1899
|
+
this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? "local",
|
|
1900
|
+
);
|
|
1885
1901
|
return waitResult.result;
|
|
1886
1902
|
}
|
|
1887
1903
|
if (waitResult.kind === "failed") {
|
|
1888
1904
|
ownedManager.acknowledgeDeliveries([job.jobId]);
|
|
1889
|
-
unregisterForegroundOwnedBash(
|
|
1905
|
+
unregisterForegroundOwnedBash(
|
|
1906
|
+
ownedManager,
|
|
1907
|
+
job.jobId,
|
|
1908
|
+
this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? "local",
|
|
1909
|
+
);
|
|
1890
1910
|
throw waitResult.error;
|
|
1891
1911
|
}
|
|
1892
1912
|
if (waitResult.kind === "aborted") {
|
|
1893
1913
|
ownedManager.cancel(job.jobId);
|
|
1894
1914
|
const terminal = await job.completion;
|
|
1895
1915
|
ownedManager.acknowledgeDeliveries([job.jobId]);
|
|
1896
|
-
unregisterForegroundOwnedBash(
|
|
1916
|
+
unregisterForegroundOwnedBash(
|
|
1917
|
+
ownedManager,
|
|
1918
|
+
job.jobId,
|
|
1919
|
+
this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? "local",
|
|
1920
|
+
);
|
|
1897
1921
|
if (terminal.kind === "failed") {
|
|
1898
1922
|
throw new ToolAbortError(
|
|
1899
1923
|
formatManagedAbortFailure(terminal.error, terminal.result, job.getLatestText()),
|
|
@@ -2355,7 +2379,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2355
2379
|
// already ran above, so exactly ONE remote handle exists and is retained
|
|
2356
2380
|
// across the fold.
|
|
2357
2381
|
const bridgeManager = ownedManager;
|
|
2358
|
-
const bridgeEndpointId = this.session.getSessionId?.() ?? "local";
|
|
2382
|
+
const bridgeEndpointId = this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? "local";
|
|
2359
2383
|
const bridgeLabel = command.length > 120 ? `${command.slice(0, 117)}...` : command;
|
|
2360
2384
|
const bridgeCompletion = Promise.withResolvers<ManagedBashJobCompletion>();
|
|
2361
2385
|
let bridgeForegroundSettled = false;
|
|
@@ -2619,7 +2643,12 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2619
2643
|
throw error;
|
|
2620
2644
|
}
|
|
2621
2645
|
const ptyGeneration = ptyManager.getJob(ptyJobId)?.generation ?? ptyJobId;
|
|
2622
|
-
registerOwnedIfLineaged(
|
|
2646
|
+
registerOwnedIfLineaged(
|
|
2647
|
+
ptyManager,
|
|
2648
|
+
toolCallId,
|
|
2649
|
+
ptyJobId,
|
|
2650
|
+
this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? undefined,
|
|
2651
|
+
);
|
|
2623
2652
|
const unregisterPtyOwnerCleanup = ptyManager.registerOwnerCleanup(ptyOwnerId, () => {
|
|
2624
2653
|
killPtyOnce();
|
|
2625
2654
|
ptyManager.failNow(ptyJobId, ptyGeneration, ownerTeardownText());
|
package/src/tools/cron.ts
CHANGED
|
@@ -592,7 +592,7 @@ export class CronTool implements AgentTool<typeof cronSchema, CronToolDetails> {
|
|
|
592
592
|
}
|
|
593
593
|
|
|
594
594
|
#resolveOwnedJobManager(): AsyncJobManager | undefined {
|
|
595
|
-
const endpointId = this.session.getSessionId?.() ?? undefined;
|
|
595
|
+
const endpointId = this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? undefined;
|
|
596
596
|
return (
|
|
597
597
|
this.session.getAsyncJobManager?.() ?? AsyncJobManager.forEndpoint(endpointId) ?? AsyncJobManager.instance()
|
|
598
598
|
);
|
package/src/tools/fetch.ts
CHANGED
|
@@ -1167,13 +1167,36 @@ interface ReadUrlCacheEntry {
|
|
|
1167
1167
|
wrappedPreambleChars?: number;
|
|
1168
1168
|
}
|
|
1169
1169
|
|
|
1170
|
+
// Entries hold whole-page outputs and image payloads, so this map must stay
|
|
1171
|
+
// bounded in long-lived hosts (TUI, SDK broker). Each fetch caches one entry
|
|
1172
|
+
// under up to two keys (requested + final URL). FIFO eviction is sufficient:
|
|
1173
|
+
// a miss simply refetches, so correctness never depends on a hit.
|
|
1174
|
+
export const READ_URL_CACHE_MAX_KEYS = 64;
|
|
1175
|
+
|
|
1170
1176
|
const readUrlCache = new Map<string, ReadUrlCacheEntry>();
|
|
1171
1177
|
|
|
1172
1178
|
function getReadUrlCacheKey(session: ToolSession, requestedUrl: string, raw: boolean): string {
|
|
1173
|
-
const
|
|
1179
|
+
const sessionFile = session.getSessionFile();
|
|
1180
|
+
const sessionId = session.getSessionId?.();
|
|
1181
|
+
const scope = sessionFile ? `file:${sessionFile}` : sessionId ? `session:${sessionId}` : `cwd:${session.cwd}`;
|
|
1174
1182
|
return `${scope}::${raw ? "raw" : "rendered"}::${normalizeUrl(requestedUrl)}`;
|
|
1175
1183
|
}
|
|
1176
1184
|
|
|
1185
|
+
/** Test-only seam for deterministic module-global cache assertions. */
|
|
1186
|
+
export const readUrlCacheTestHooks = {
|
|
1187
|
+
get size(): number {
|
|
1188
|
+
return readUrlCache.size;
|
|
1189
|
+
},
|
|
1190
|
+
get retainedOutputChars(): number {
|
|
1191
|
+
let total = 0;
|
|
1192
|
+
for (const entry of new Set(readUrlCache.values())) total += entry.output.length;
|
|
1193
|
+
return total;
|
|
1194
|
+
},
|
|
1195
|
+
reset(): void {
|
|
1196
|
+
readUrlCache.clear();
|
|
1197
|
+
},
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1177
1200
|
async function readArtifactOutput(session: ToolSession, artifactId: string): Promise<string | null> {
|
|
1178
1201
|
const artifactsDir = session.getArtifactsDir?.();
|
|
1179
1202
|
if (!artifactsDir) return null;
|
|
@@ -1224,6 +1247,11 @@ async function ensureReadUrlCacheArtifact(session: ToolSession, entry: ReadUrlCa
|
|
|
1224
1247
|
function cacheReadUrlEntry(session: ToolSession, requestedUrl: string, raw: boolean, entry: ReadUrlCacheEntry): void {
|
|
1225
1248
|
readUrlCache.set(getReadUrlCacheKey(session, requestedUrl, raw), entry);
|
|
1226
1249
|
readUrlCache.set(getReadUrlCacheKey(session, entry.details.finalUrl, raw), entry);
|
|
1250
|
+
while (readUrlCache.size > READ_URL_CACHE_MAX_KEYS) {
|
|
1251
|
+
const oldest = readUrlCache.keys().next().value;
|
|
1252
|
+
if (oldest === undefined) break;
|
|
1253
|
+
readUrlCache.delete(oldest);
|
|
1254
|
+
}
|
|
1227
1255
|
}
|
|
1228
1256
|
|
|
1229
1257
|
async function buildReadUrlCacheEntry(
|
package/src/tools/index.ts
CHANGED
|
@@ -242,8 +242,6 @@ export interface ToolSession {
|
|
|
242
242
|
hasForegroundBashBackgroundRequestHandler?: () => boolean;
|
|
243
243
|
/** Request that a foreground wait fold into a background job, if supported. `reason` names the trigger; `adapter` targets a specific wait instead of the newest registration. */
|
|
244
244
|
requestForegroundBashBackground?: (reason?: FoldReason, adapter?: FoldAdapter) => Promise<boolean>;
|
|
245
|
-
/** Current tool interrupt policy; `finish_tools` lets the running tool finish, so a steer must not fold it. */
|
|
246
|
-
getToolInterruptPolicy?: () => "abort_tools" | "finish_tools";
|
|
247
245
|
|
|
248
246
|
/** Get the session-owned or inherited async job manager. */
|
|
249
247
|
getAsyncJobManager?: () => AsyncJobManager | undefined;
|
|
@@ -251,6 +249,14 @@ export interface ToolSession {
|
|
|
251
249
|
waitForUserSteering?: (signal: AbortSignal) => Promise<void>;
|
|
252
250
|
/** Get session ID */
|
|
253
251
|
getSessionId?: () => string | null;
|
|
252
|
+
/**
|
|
253
|
+
* Opaque async-job ownership/lookup endpoint key. This is not a workflow or
|
|
254
|
+
* path identity: workflow/session consumers must use getSessionId(). Only
|
|
255
|
+
* AsyncJobManager ownership and endpoint-first lookup may use this accessor
|
|
256
|
+
* (subagent-inherited managers are registered under the parent's endpoint;
|
|
257
|
+
* review thread P1).
|
|
258
|
+
*/
|
|
259
|
+
getAsyncEndpointId?: () => string | null;
|
|
254
260
|
/** Get credential-selection session identity. */
|
|
255
261
|
getCredentialSessionId?: () => string | null;
|
|
256
262
|
/** 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(
|
|
108
|
+
AsyncJobManager.forEndpoint(
|
|
109
|
+
this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? undefined,
|
|
110
|
+
) ??
|
|
109
111
|
AsyncJobManager.instance();
|
|
110
112
|
if (!manager) {
|
|
111
113
|
return {
|
|
@@ -396,7 +398,11 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
|
396
398
|
const generation = manager.getJob?.(job.id)?.generation;
|
|
397
399
|
if (!generation) continue;
|
|
398
400
|
// The endpoint disambiguates concurrent sessions' same job ids (review P1).
|
|
399
|
-
const registration = lookupOwnedRegistration(
|
|
401
|
+
const registration = lookupOwnedRegistration(
|
|
402
|
+
job.id,
|
|
403
|
+
generation,
|
|
404
|
+
this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? "local",
|
|
405
|
+
);
|
|
400
406
|
if (registration) unregisterOwnedRegistration(registration);
|
|
401
407
|
}
|
|
402
408
|
|
package/src/tools/monitor.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
? {
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -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/search.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
155
|
-
let archive = archiveCache.get(archiveAbs);
|
|
156
|
-
if (!archive) {
|
|
178
|
+
let extracted: ExtractedArchiveFile;
|
|
157
179
|
try {
|
|
158
|
-
|
|
180
|
+
extracted = await archive.readFile(member.subPath);
|
|
159
181
|
} catch (err) {
|
|
160
|
-
unreadable.push(`${entry} (
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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 => ({
|
|
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
|
-
|
|
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
|
-
|
|
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 },
|