@gajae-code/coding-agent 0.5.1 → 0.5.3
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 +31 -0
- package/README.md +1 -1
- package/dist/types/async/job-manager.d.ts +6 -0
- package/dist/types/cli/setup-cli.d.ts +8 -1
- package/dist/types/commands/setup.d.ts +7 -0
- package/dist/types/config/file-lock.d.ts +24 -2
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +62 -0
- package/dist/types/dap/client.d.ts +2 -1
- package/dist/types/edit/read-file.d.ts +6 -0
- package/dist/types/eval/js/context-manager.d.ts +3 -0
- package/dist/types/eval/js/executor.d.ts +1 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +64 -2
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +7 -1
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +56 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/theme/defaults/index.d.ts +302 -0
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/runtime/process-lifecycle.d.ts +108 -0
- package/dist/types/runtime-mcp/transports/stdio.d.ts +1 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +17 -1
- package/dist/types/session/artifacts.d.ts +4 -1
- package/dist/types/session/history-storage.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +10 -1
- package/dist/types/session/streaming-output.d.ts +5 -0
- package/dist/types/setup/credential-import.d.ts +79 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +76 -0
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/render.d.ts +1 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +9 -0
- package/dist/types/tools/sqlite-reader.d.ts +2 -1
- package/dist/types/tools/subagent-render.d.ts +7 -1
- package/dist/types/tools/subagent.d.ts +21 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +5 -0
- package/dist/types/web/search/index.d.ts +4 -4
- package/dist/types/web/search/provider.d.ts +16 -20
- package/dist/types/web/search/providers/base.d.ts +2 -1
- package/dist/types/web/search/providers/openai-compatible.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +14 -2
- package/package.json +7 -7
- package/scripts/build-binary.ts +7 -0
- package/src/async/job-manager.ts +153 -39
- package/src/cli/args.ts +2 -0
- package/src/cli/fast-help.ts +2 -0
- package/src/cli/setup-cli.ts +138 -3
- package/src/commands/setup.ts +5 -1
- package/src/commands/ultragoal.ts +3 -1
- package/src/config/file-lock-gc.ts +14 -2
- package/src/config/file-lock.ts +63 -13
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +15 -15
- package/src/config/model-registry.ts +21 -1
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +62 -0
- package/src/dap/client.ts +105 -64
- package/src/dap/session.ts +44 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
- package/src/edit/read-file.ts +19 -1
- package/src/eval/js/context-manager.ts +228 -65
- package/src/eval/js/executor.ts +2 -0
- package/src/eval/js/index.ts +1 -0
- package/src/eval/js/worker-core.ts +10 -6
- package/src/eval/py/executor.ts +68 -19
- package/src/eval/py/kernel.ts +46 -22
- package/src/eval/py/runner.py +68 -14
- package/src/exec/bash-executor.ts +49 -13
- package/src/gjc-runtime/deep-interview-recorder.ts +40 -0
- package/src/gjc-runtime/launch-tmux.ts +3 -4
- package/src/gjc-runtime/ralplan-runtime.ts +174 -12
- package/src/gjc-runtime/state-runtime.ts +2 -1
- package/src/gjc-runtime/state-writer.ts +254 -7
- package/src/gjc-runtime/tmux-gc.ts +88 -38
- package/src/gjc-runtime/tmux-sessions.ts +44 -6
- package/src/gjc-runtime/ultragoal-guard.ts +155 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1227 -31
- package/src/gjc-runtime/workflow-manifest.generated.json +44 -0
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/harness-control-plane/owner.ts +3 -2
- package/src/harness-control-plane/rpc-adapter.ts +1 -1
- package/src/hooks/skill-state.ts +121 -2
- package/src/internal-urls/artifact-protocol.ts +10 -1
- package/src/internal-urls/docs-index.generated.ts +14 -10
- package/src/lsp/client.ts +64 -26
- package/src/lsp/defaults.json +1 -0
- package/src/lsp/index.ts +2 -1
- package/src/lsp/lspmux.ts +33 -9
- package/src/lsp/types.ts +2 -0
- package/src/main.ts +14 -4
- package/src/modes/acp/acp-agent.ts +4 -2
- package/src/modes/bridge/bridge-mode.ts +23 -1
- package/src/modes/components/assistant-message.ts +10 -2
- package/src/modes/components/bash-execution.ts +5 -1
- package/src/modes/components/eval-execution.ts +5 -1
- package/src/modes/components/history-search.ts +5 -2
- package/src/modes/components/model-selector.ts +60 -2
- package/src/modes/components/oauth-selector.ts +5 -0
- package/src/modes/components/provider-onboarding-selector.ts +6 -1
- package/src/modes/components/runtime-mcp-add-wizard.ts +58 -7
- package/src/modes/components/skill-message.ts +24 -16
- package/src/modes/components/tool-execution.ts +6 -0
- package/src/modes/controllers/extension-ui-controller.ts +33 -6
- package/src/modes/controllers/input-controller.ts +5 -0
- package/src/modes/controllers/selector-controller.ts +86 -2
- package/src/modes/interactive-mode.ts +11 -1
- package/src/modes/rpc/rpc-mode.ts +132 -18
- package/src/modes/shared/agent-wire/command-dispatch.ts +5 -2
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +3 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
- package/src/modes/theme/defaults/claude-code.json +100 -0
- package/src/modes/theme/defaults/codex.json +100 -0
- package/src/modes/theme/defaults/index.ts +6 -0
- package/src/modes/theme/defaults/opencode.json +102 -0
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/agents/executor.md +5 -2
- package/src/runtime/process-lifecycle.ts +400 -0
- package/src/runtime-mcp/manager.ts +164 -50
- package/src/runtime-mcp/transports/http.ts +12 -11
- package/src/runtime-mcp/transports/stdio.ts +64 -38
- package/src/runtime-mcp/types.ts +3 -0
- package/src/sdk.ts +39 -1
- package/src/session/agent-session.ts +190 -33
- package/src/session/artifacts.ts +17 -2
- package/src/session/blob-store.ts +36 -2
- package/src/session/history-storage.ts +32 -11
- package/src/session/session-manager.ts +99 -31
- package/src/session/streaming-output.ts +54 -3
- package/src/setup/credential-import.ts +429 -0
- package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
- package/src/slash-commands/builtin-registry.ts +30 -3
- package/src/slash-commands/helpers/fast-status-report.ts +111 -0
- package/src/task/executor.ts +7 -1
- package/src/task/render.ts +18 -7
- package/src/tools/archive-reader.ts +10 -1
- package/src/tools/ask.ts +4 -2
- package/src/tools/bash.ts +11 -4
- package/src/tools/browser/tab-supervisor.ts +22 -0
- package/src/tools/browser.ts +38 -4
- package/src/tools/cron.ts +1 -1
- package/src/tools/read.ts +11 -12
- package/src/tools/sqlite-reader.ts +19 -5
- package/src/tools/subagent-render.ts +119 -29
- package/src/tools/subagent.ts +147 -7
- package/src/tools/ultragoal-ask-guard.ts +39 -0
- package/src/web/search/index.ts +25 -25
- package/src/web/search/provider.ts +178 -87
- package/src/web/search/providers/base.ts +2 -1
- package/src/web/search/providers/openai-compatible.ts +151 -0
- package/src/web/search/types.ts +47 -22
|
@@ -41,6 +41,8 @@ import {
|
|
|
41
41
|
calculatePromptTokens,
|
|
42
42
|
collectEntriesForBranchSummary,
|
|
43
43
|
compact,
|
|
44
|
+
type EmergencyCompactionSample,
|
|
45
|
+
emergencyCompactionReason,
|
|
44
46
|
estimateMessageTokensHeuristic,
|
|
45
47
|
estimateTokens,
|
|
46
48
|
generateBranchSummary,
|
|
@@ -142,6 +144,7 @@ import { onAppendOnlyModeChanged } from "../config/settings";
|
|
|
142
144
|
import { RawSseDebugBuffer } from "../debug/raw-sse-buffer";
|
|
143
145
|
import { loadCapability } from "../discovery";
|
|
144
146
|
import { expandApplyPatchToEntries, normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit";
|
|
147
|
+
import { disposeVmContextsByOwner } from "../eval/js/context-manager";
|
|
145
148
|
import {
|
|
146
149
|
disposeKernelSessionsByOwner,
|
|
147
150
|
executePython as executePythonCommand,
|
|
@@ -234,12 +237,14 @@ import {
|
|
|
234
237
|
import type { ToolSession } from "../tools";
|
|
235
238
|
import { AskTool } from "../tools/ask";
|
|
236
239
|
import { assertEditableFile } from "../tools/auto-generated-guard";
|
|
240
|
+
import { releaseTabsForOwner } from "../tools/browser/tab-supervisor";
|
|
237
241
|
import type { CheckpointState } from "../tools/checkpoint";
|
|
238
242
|
import { outputMeta, wrapToolWithMetaNotice } from "../tools/output-meta";
|
|
239
243
|
import { normalizeLocalScheme, resolveToCwd } from "../tools/path-utils";
|
|
240
244
|
import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from "../tools/todo-write";
|
|
241
245
|
import { ToolAbortError, ToolError } from "../tools/tool-errors";
|
|
242
246
|
import { clampTimeout } from "../tools/tool-timeouts";
|
|
247
|
+
import { guardToolForUltragoalAsk } from "../tools/ultragoal-ask-guard";
|
|
243
248
|
import { parseCommandArgs } from "../utils/command-args";
|
|
244
249
|
import { type EditMode, resolveEditMode } from "../utils/edit-mode";
|
|
245
250
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
@@ -906,6 +911,7 @@ export class AgentSession {
|
|
|
906
911
|
// Compaction state
|
|
907
912
|
#compactionAbortController: AbortController | undefined = undefined;
|
|
908
913
|
#autoCompactionAbortController: AbortController | undefined = undefined;
|
|
914
|
+
#resourceSampler: () => EmergencyCompactionSample = () => this.#defaultResourceSample();
|
|
909
915
|
#prePromptContextCheckPromise: Promise<void> | undefined = undefined;
|
|
910
916
|
|
|
911
917
|
// Branch summarization state
|
|
@@ -1186,6 +1192,7 @@ export class AgentSession {
|
|
|
1186
1192
|
};
|
|
1187
1193
|
this.agent.setProviderResponseInterceptor(this.#onResponse);
|
|
1188
1194
|
this.agent.setRawSseEventInterceptor(this.#onSseEvent);
|
|
1195
|
+
this.#setGuardedAgentTools(this.agent.state.tools);
|
|
1189
1196
|
this.yieldQueue = new YieldQueue({
|
|
1190
1197
|
isStreaming: () => this.isStreaming,
|
|
1191
1198
|
injectStreaming: message => this.agent.followUp(message),
|
|
@@ -3185,6 +3192,13 @@ export class AgentSession {
|
|
|
3185
3192
|
}
|
|
3186
3193
|
}
|
|
3187
3194
|
await shutdownAllLspClients();
|
|
3195
|
+
// F13: release only THIS session's browser tabs on dispose (kill:false → remote
|
|
3196
|
+
// browsers disconnect, headless close gracefully). Scoped by the session id the
|
|
3197
|
+
// browser tool tagged tabs with, so other live sessions' tabs are untouched.
|
|
3198
|
+
// No-op when this session opened no tabs. Failure is logged, not thrown.
|
|
3199
|
+
await releaseTabsForOwner(this.sessionManager.getSessionId()).catch((error: unknown) =>
|
|
3200
|
+
logger.warn("session dispose: releaseTabsForOwner failed", { error }),
|
|
3201
|
+
);
|
|
3188
3202
|
const pythonExecutionsSettled = await this.#prepareEvalExecutionsForDispose();
|
|
3189
3203
|
if (!pythonExecutionsSettled) {
|
|
3190
3204
|
logger.warn(
|
|
@@ -3192,6 +3206,7 @@ export class AgentSession {
|
|
|
3192
3206
|
);
|
|
3193
3207
|
}
|
|
3194
3208
|
await disposeKernelSessionsByOwner(this.#evalKernelOwnerId);
|
|
3209
|
+
await disposeVmContextsByOwner(this.#evalKernelOwnerId);
|
|
3195
3210
|
this.#releasePowerAssertion();
|
|
3196
3211
|
await this.sessionManager.close();
|
|
3197
3212
|
this.#closeAllProviderSessions("dispose");
|
|
@@ -3690,6 +3705,16 @@ export class AgentSession {
|
|
|
3690
3705
|
}) as T;
|
|
3691
3706
|
}
|
|
3692
3707
|
|
|
3708
|
+
#prepareToolForExecution<T extends AgentTool>(tool: T): T {
|
|
3709
|
+
return this.#wrapToolForDeepInterviewMutationGuard(
|
|
3710
|
+
this.#wrapToolForAcpPermission(guardToolForUltragoalAsk(tool, () => this.sessionManager.getCwd())),
|
|
3711
|
+
);
|
|
3712
|
+
}
|
|
3713
|
+
|
|
3714
|
+
#setGuardedAgentTools(tools: AgentTool[]): void {
|
|
3715
|
+
this.agent.setTools(tools.map(tool => this.#prepareToolForExecution(tool)));
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3693
3718
|
async #applyActiveToolsByName(
|
|
3694
3719
|
toolNames: string[],
|
|
3695
3720
|
options?: { persistMCPSelection?: boolean; previousSelectedMCPToolNames?: string[] },
|
|
@@ -3701,7 +3726,7 @@ export class AgentSession {
|
|
|
3701
3726
|
for (const name of toolNames) {
|
|
3702
3727
|
const tool = this.#toolRegistry.get(name);
|
|
3703
3728
|
if (tool) {
|
|
3704
|
-
tools.push(
|
|
3729
|
+
tools.push(tool);
|
|
3705
3730
|
validToolNames.push(name);
|
|
3706
3731
|
}
|
|
3707
3732
|
}
|
|
@@ -3718,7 +3743,7 @@ export class AgentSession {
|
|
|
3718
3743
|
this.#selectedDiscoveredToolNames.delete(name);
|
|
3719
3744
|
}
|
|
3720
3745
|
}
|
|
3721
|
-
this
|
|
3746
|
+
this.#setGuardedAgentTools(tools);
|
|
3722
3747
|
|
|
3723
3748
|
// Active tool set changed → discoverable tool list (which excludes already-active tools)
|
|
3724
3749
|
// is now stale. Invalidate before any prompt-template hook reads the discovery list.
|
|
@@ -3976,6 +4001,9 @@ export class AgentSession {
|
|
|
3976
4001
|
if (uniqueToolNames.size !== nextToolNames.length) {
|
|
3977
4002
|
throw new Error("RPC host tool names must be unique");
|
|
3978
4003
|
}
|
|
4004
|
+
if (uniqueToolNames.has("ask")) {
|
|
4005
|
+
throw new Error('RPC host tool "ask" is reserved and cannot be supplied by the host');
|
|
4006
|
+
}
|
|
3979
4007
|
|
|
3980
4008
|
for (const name of uniqueToolNames) {
|
|
3981
4009
|
if (this.#toolRegistry.has(name) && !this.#rpcHostToolNames.has(name)) {
|
|
@@ -4303,11 +4331,8 @@ export class AgentSession {
|
|
|
4303
4331
|
this.#toolRegistry.set(finalTool.name, finalTool);
|
|
4304
4332
|
|
|
4305
4333
|
if (!this.getActiveToolNames().includes(finalTool.name)) {
|
|
4306
|
-
const activeTools = [
|
|
4307
|
-
|
|
4308
|
-
this.#wrapToolForDeepInterviewMutationGuard(this.#wrapToolForAcpPermission(finalTool)),
|
|
4309
|
-
];
|
|
4310
|
-
this.agent.setTools(activeTools);
|
|
4334
|
+
const activeTools = [...this.agent.state.tools, finalTool];
|
|
4335
|
+
this.#setGuardedAgentTools(activeTools);
|
|
4311
4336
|
this.#invalidateDiscoveryCaches();
|
|
4312
4337
|
void this.refreshBaseSystemPrompt().catch(error => {
|
|
4313
4338
|
logger.warn("Failed to refresh system prompt after workflow gate ask tool registration", {
|
|
@@ -4339,9 +4364,8 @@ export class AgentSession {
|
|
|
4339
4364
|
const activeToolNames = this.getActiveToolNames();
|
|
4340
4365
|
const activeTools = activeToolNames
|
|
4341
4366
|
.map(name => this.#toolRegistry.get(name))
|
|
4342
|
-
.filter((tool): tool is AgentTool => tool !== undefined)
|
|
4343
|
-
|
|
4344
|
-
this.agent.setTools(activeTools);
|
|
4367
|
+
.filter((tool): tool is AgentTool => tool !== undefined);
|
|
4368
|
+
this.#setGuardedAgentTools(activeTools);
|
|
4345
4369
|
}
|
|
4346
4370
|
|
|
4347
4371
|
getCheckpointState(): CheckpointState | undefined {
|
|
@@ -6005,6 +6029,44 @@ export class AgentSession {
|
|
|
6005
6029
|
);
|
|
6006
6030
|
}
|
|
6007
6031
|
|
|
6032
|
+
/**
|
|
6033
|
+
* True when the configured `serviceTier` resolves to `"priority"` for the
|
|
6034
|
+
* given model `provider`. Returns false for scoped tiers that don't match
|
|
6035
|
+
* (e.g. `"openai-only"` on an anthropic provider) and when `provider` is
|
|
6036
|
+
* undefined. This is the canonical provider-aware fast-mode predicate.
|
|
6037
|
+
*/
|
|
6038
|
+
isFastForProvider(provider?: string): boolean {
|
|
6039
|
+
// Fast mode applies to a concrete model's provider. With no provider
|
|
6040
|
+
// (no model selected) it cannot apply, even under an unscoped `priority`
|
|
6041
|
+
// tier that `resolveServiceTier` would otherwise pass through.
|
|
6042
|
+
if (provider === undefined) return false;
|
|
6043
|
+
return resolveServiceTier(this.serviceTier, provider) === "priority";
|
|
6044
|
+
}
|
|
6045
|
+
|
|
6046
|
+
/**
|
|
6047
|
+
* Effective service tier applied to task-tool subagent sessions
|
|
6048
|
+
* (executor/architect/planner/critic). They run under `task.serviceTier`
|
|
6049
|
+
* unless it is `"inherit"`, in which case they inherit the main session
|
|
6050
|
+
* tier — mirroring `createSubagentSettings`.
|
|
6051
|
+
*/
|
|
6052
|
+
#subagentServiceTier(): ServiceTier | undefined {
|
|
6053
|
+
const configured = this.settings.get("task.serviceTier");
|
|
6054
|
+
if (configured === "inherit") return this.serviceTier;
|
|
6055
|
+
if (configured === "none") return undefined;
|
|
6056
|
+
return configured;
|
|
6057
|
+
}
|
|
6058
|
+
|
|
6059
|
+
/**
|
|
6060
|
+
* Provider-aware fast-mode predicate for task-tool subagent roles, evaluated
|
|
6061
|
+
* against the effective subagent tier (`task.serviceTier`) rather than the
|
|
6062
|
+
* main session tier. Use this for `task.agentModelOverrides` role rows so the
|
|
6063
|
+
* ⚡ glyph reflects the tier the subagent actually runs under.
|
|
6064
|
+
*/
|
|
6065
|
+
isFastForSubagentProvider(provider?: string): boolean {
|
|
6066
|
+
if (provider === undefined) return false;
|
|
6067
|
+
return resolveServiceTier(this.#subagentServiceTier(), provider) === "priority";
|
|
6068
|
+
}
|
|
6069
|
+
|
|
6008
6070
|
/**
|
|
6009
6071
|
* True when the configured `serviceTier` resolves to `"priority"` for the
|
|
6010
6072
|
* *currently selected model's provider*. Returns false for scoped tiers
|
|
@@ -6012,7 +6074,7 @@ export class AgentSession {
|
|
|
6012
6074
|
* no model is selected.
|
|
6013
6075
|
*/
|
|
6014
6076
|
isFastModeActive(): boolean {
|
|
6015
|
-
return
|
|
6077
|
+
return this.isFastForProvider(this.model?.provider);
|
|
6016
6078
|
}
|
|
6017
6079
|
|
|
6018
6080
|
setServiceTier(serviceTier: ServiceTier | undefined): void {
|
|
@@ -6576,11 +6638,55 @@ export class AgentSession {
|
|
|
6576
6638
|
}
|
|
6577
6639
|
}
|
|
6578
6640
|
|
|
6641
|
+
/** Test seam: override the emergency-compaction resource sampler so tests never read real RSS. */
|
|
6642
|
+
setResourceSampler(sampler: () => EmergencyCompactionSample): void {
|
|
6643
|
+
this.#resourceSampler = sampler;
|
|
6644
|
+
}
|
|
6645
|
+
|
|
6646
|
+
#defaultResourceSample(): EmergencyCompactionSample {
|
|
6647
|
+
let providerBytes = 0;
|
|
6648
|
+
let imageBytes = 0;
|
|
6649
|
+
for (const message of this.state.messages) {
|
|
6650
|
+
const content = (message as { content?: unknown }).content;
|
|
6651
|
+
if (typeof content === "string") {
|
|
6652
|
+
providerBytes += content.length;
|
|
6653
|
+
} else if (Array.isArray(content)) {
|
|
6654
|
+
for (const block of content) {
|
|
6655
|
+
if (!block || typeof block !== "object") continue;
|
|
6656
|
+
const typed = block as { text?: unknown; data?: unknown };
|
|
6657
|
+
if (typeof typed.text === "string") providerBytes += typed.text.length;
|
|
6658
|
+
if (typeof typed.data === "string") {
|
|
6659
|
+
imageBytes += typed.data.length;
|
|
6660
|
+
providerBytes += typed.data.length;
|
|
6661
|
+
}
|
|
6662
|
+
}
|
|
6663
|
+
}
|
|
6664
|
+
}
|
|
6665
|
+
return {
|
|
6666
|
+
heapUsedBytes: process.memoryUsage().heapUsed,
|
|
6667
|
+
providerBytes,
|
|
6668
|
+
messageCount: this.state.messages.length,
|
|
6669
|
+
imageBytes,
|
|
6670
|
+
};
|
|
6671
|
+
}
|
|
6672
|
+
|
|
6579
6673
|
async #checkEstimatedContextBeforePromptOnce(pendingMessages: readonly AgentMessage[]): Promise<void> {
|
|
6580
6674
|
const model = this.model;
|
|
6581
6675
|
if (!model) return;
|
|
6582
6676
|
const contextWindow = model.contextWindow ?? 0;
|
|
6583
6677
|
if (contextWindow <= 0) return;
|
|
6678
|
+
// F6: non-disableable emergency floor — compact before OOM even when token-based
|
|
6679
|
+
// compaction is disabled or its threshold is set too high (weak-hardware protection).
|
|
6680
|
+
const emergencyReason = emergencyCompactionReason(this.#resourceSampler());
|
|
6681
|
+
if (emergencyReason) {
|
|
6682
|
+
logger.warn("Emergency compaction triggered (resource floor exceeded)", { reason: emergencyReason });
|
|
6683
|
+
await this.#runAutoCompaction("overflow", false, false, {
|
|
6684
|
+
continueAfterMaintenance: false,
|
|
6685
|
+
deferHandoffMaintenance: false,
|
|
6686
|
+
force: true,
|
|
6687
|
+
});
|
|
6688
|
+
return;
|
|
6689
|
+
}
|
|
6584
6690
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
6585
6691
|
if (!compactionSettings.enabled || compactionSettings.strategy === "off") return;
|
|
6586
6692
|
|
|
@@ -7232,7 +7338,17 @@ export class AgentSession {
|
|
|
7232
7338
|
addCandidate(this.#resolveRoleModelFull(role, availableModels, currentModel).model);
|
|
7233
7339
|
}
|
|
7234
7340
|
|
|
7235
|
-
|
|
7341
|
+
// Last-resort fallback: the largest-context model that shares the ACTIVE
|
|
7342
|
+
// model's provider. Scoping this to the current provider keeps auto-
|
|
7343
|
+
// compaction on the user's configured/custom route instead of silently
|
|
7344
|
+
// defaulting to an unrelated provider (e.g. a stray OpenAI credential
|
|
7345
|
+
// with no remaining credit) just because it happens to be in the bundled
|
|
7346
|
+
// catalog. Cross-provider compaction stays possible, but only when the
|
|
7347
|
+
// user opts in explicitly via modelRoles (handled by the loop above).
|
|
7348
|
+
const fallbackProvider = currentModel?.provider;
|
|
7349
|
+
const sortedByContext = [...availableModels]
|
|
7350
|
+
.filter(model => fallbackProvider === undefined || model.provider === fallbackProvider)
|
|
7351
|
+
.sort((a, b) => b.contextWindow - a.contextWindow);
|
|
7236
7352
|
for (const model of sortedByContext) {
|
|
7237
7353
|
if (!seen.has(this.#getModelKey(model))) {
|
|
7238
7354
|
addCandidate(model);
|
|
@@ -7356,11 +7472,13 @@ export class AgentSession {
|
|
|
7356
7472
|
reason: "overflow" | "threshold" | "idle",
|
|
7357
7473
|
willRetry: boolean,
|
|
7358
7474
|
deferred = false,
|
|
7359
|
-
options?: { continueAfterMaintenance?: boolean; deferHandoffMaintenance?: boolean },
|
|
7475
|
+
options?: { continueAfterMaintenance?: boolean; deferHandoffMaintenance?: boolean; force?: boolean },
|
|
7360
7476
|
): Promise<void> {
|
|
7361
7477
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
7362
|
-
|
|
7363
|
-
|
|
7478
|
+
// `force` is the non-disableable emergency floor (F6): it bypasses the user's
|
|
7479
|
+
// disabled/off settings so a resource-floor breach still compacts before OOM.
|
|
7480
|
+
if (!options?.force && compactionSettings.strategy === "off") return;
|
|
7481
|
+
if (!options?.force && reason !== "idle" && !compactionSettings.enabled) return;
|
|
7364
7482
|
const generation = this.#promptGeneration;
|
|
7365
7483
|
if (
|
|
7366
7484
|
options?.deferHandoffMaintenance !== false &&
|
|
@@ -9174,7 +9292,7 @@ export class AgentSession {
|
|
|
9174
9292
|
error: String(mcpError),
|
|
9175
9293
|
});
|
|
9176
9294
|
this.#selectedMCPToolNames = new Set(previousSelectedMCPToolNames);
|
|
9177
|
-
this
|
|
9295
|
+
this.#setGuardedAgentTools(previousTools);
|
|
9178
9296
|
this.#baseSystemPrompt = previousBaseSystemPrompt;
|
|
9179
9297
|
this.agent.setSystemPrompt(previousSystemPrompt);
|
|
9180
9298
|
}
|
|
@@ -9497,17 +9615,15 @@ export class AgentSession {
|
|
|
9497
9615
|
*/
|
|
9498
9616
|
getSessionStats(): SessionStats {
|
|
9499
9617
|
const state = this.state;
|
|
9500
|
-
|
|
9501
|
-
|
|
9502
|
-
|
|
9503
|
-
|
|
9618
|
+
let userMessages = 0;
|
|
9619
|
+
let assistantMessages = 0;
|
|
9620
|
+
let toolResults = 0;
|
|
9504
9621
|
let toolCalls = 0;
|
|
9505
9622
|
let totalInput = 0;
|
|
9506
9623
|
let totalOutput = 0;
|
|
9507
9624
|
let totalCacheRead = 0;
|
|
9508
9625
|
let totalCacheWrite = 0;
|
|
9509
9626
|
let totalCost = 0;
|
|
9510
|
-
|
|
9511
9627
|
let totalPremiumRequests = 0;
|
|
9512
9628
|
const getTaskToolUsage = (details: unknown): Usage | undefined => {
|
|
9513
9629
|
if (!details || typeof details !== "object") return undefined;
|
|
@@ -9517,8 +9633,13 @@ export class AgentSession {
|
|
|
9517
9633
|
return usage as Usage;
|
|
9518
9634
|
};
|
|
9519
9635
|
|
|
9636
|
+
// Single pass over messages (replaces three role filters plus a separate usage
|
|
9637
|
+
// loop) so per-turn stats stay O(messages + assistant content blocks), not O(4N).
|
|
9520
9638
|
for (const message of state.messages) {
|
|
9521
|
-
if (message.role === "
|
|
9639
|
+
if (message.role === "user") {
|
|
9640
|
+
userMessages += 1;
|
|
9641
|
+
} else if (message.role === "assistant") {
|
|
9642
|
+
assistantMessages += 1;
|
|
9522
9643
|
const assistantMsg = message as AssistantMessage;
|
|
9523
9644
|
toolCalls += assistantMsg.content.filter(c => c.type === "toolCall").length;
|
|
9524
9645
|
totalInput += assistantMsg.usage.input;
|
|
@@ -9527,17 +9648,18 @@ export class AgentSession {
|
|
|
9527
9648
|
totalCacheWrite += assistantMsg.usage.cacheWrite;
|
|
9528
9649
|
totalPremiumRequests += assistantMsg.usage.premiumRequests ?? 0;
|
|
9529
9650
|
totalCost += assistantMsg.usage.cost.total;
|
|
9530
|
-
}
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
|
|
9536
|
-
|
|
9537
|
-
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
|
|
9651
|
+
} else if (message.role === "toolResult") {
|
|
9652
|
+
toolResults += 1;
|
|
9653
|
+
if (message.toolName === "task") {
|
|
9654
|
+
const usage = getTaskToolUsage(message.details);
|
|
9655
|
+
if (usage) {
|
|
9656
|
+
totalInput += usage.input;
|
|
9657
|
+
totalOutput += usage.output;
|
|
9658
|
+
totalCacheRead += usage.cacheRead;
|
|
9659
|
+
totalCacheWrite += usage.cacheWrite;
|
|
9660
|
+
totalPremiumRequests += usage.premiumRequests ?? 0;
|
|
9661
|
+
totalCost += usage.cost.total;
|
|
9662
|
+
}
|
|
9541
9663
|
}
|
|
9542
9664
|
}
|
|
9543
9665
|
}
|
|
@@ -9698,11 +9820,46 @@ export class AgentSession {
|
|
|
9698
9820
|
return tokens;
|
|
9699
9821
|
}
|
|
9700
9822
|
|
|
9823
|
+
#nativeTokenCache = new WeakMap<AgentMessage, { len: number; tokens: number }>();
|
|
9824
|
+
|
|
9825
|
+
/** Cheap content-size signal to invalidate the native token cache on mutation (growth). */
|
|
9826
|
+
/**
|
|
9827
|
+
* Cheap content-size signal to invalidate the native token cache on mutation. Recursively
|
|
9828
|
+
* sums string lengths across the whole message (depth-bounded), so it covers every
|
|
9829
|
+
* provider-visible shape (text/thinking/tool args, toolResult output, tool names, etc.)
|
|
9830
|
+
* without allocating a serialized copy. A size-preserving in-place edit yields only a
|
|
9831
|
+
* benign estimate drift.
|
|
9832
|
+
*/
|
|
9833
|
+
#messageTokenSize(value: unknown, depth = 0): number {
|
|
9834
|
+
if (depth > 6) return 0;
|
|
9835
|
+
if (typeof value === "string") return value.length;
|
|
9836
|
+
if (typeof value === "number" || typeof value === "boolean") return 8;
|
|
9837
|
+
if (Array.isArray(value)) {
|
|
9838
|
+
let size = 0;
|
|
9839
|
+
for (const item of value) size += this.#messageTokenSize(item, depth + 1);
|
|
9840
|
+
return size;
|
|
9841
|
+
}
|
|
9842
|
+
if (value && typeof value === "object") {
|
|
9843
|
+
let size = 0;
|
|
9844
|
+
for (const item of Object.values(value)) size += this.#messageTokenSize(item, depth + 1);
|
|
9845
|
+
return size;
|
|
9846
|
+
}
|
|
9847
|
+
return 0;
|
|
9848
|
+
}
|
|
9849
|
+
|
|
9701
9850
|
#estimateMessageNativeContextTokens(message: AgentMessage): number {
|
|
9851
|
+
// F10/F22: cache the expensive native token count per message object, invalidated by a
|
|
9852
|
+
// cheap content-size signal, so unchanged (stable-size) messages are not re-tokenized on
|
|
9853
|
+
// every pre-prompt estimate. A rare size-preserving in-place edit yields only a benign
|
|
9854
|
+
// token-estimate drift, never wrong output.
|
|
9855
|
+
const len = this.#messageTokenSize(message);
|
|
9856
|
+
const cached = this.#nativeTokenCache.get(message);
|
|
9857
|
+
if (cached && cached.len === len) return cached.tokens;
|
|
9702
9858
|
let tokens = 0;
|
|
9703
9859
|
for (const llmMessage of convertToLlm([message])) {
|
|
9704
9860
|
tokens += estimateTokens(llmMessage);
|
|
9705
9861
|
}
|
|
9862
|
+
this.#nativeTokenCache.set(message, { len, tokens });
|
|
9706
9863
|
return tokens;
|
|
9707
9864
|
}
|
|
9708
9865
|
|
package/src/session/artifacts.ts
CHANGED
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
import * as fs from "node:fs/promises";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
|
|
10
|
+
import { DEFAULT_ARTIFACT_MAX_BYTES, truncateHeadBytes } from "./streaming-output";
|
|
11
|
+
export interface ArtifactSaveOptions {
|
|
12
|
+
maxBytes?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
/**
|
|
11
16
|
* Manages artifact storage for a session.
|
|
12
17
|
*
|
|
@@ -94,9 +99,19 @@ export class ArtifactManager {
|
|
|
94
99
|
* @param toolType Tool name for file extension (e.g., "bash", "read")
|
|
95
100
|
* @returns Artifact ID (numeric string)
|
|
96
101
|
*/
|
|
97
|
-
async save(content: string, toolType: string): Promise<string> {
|
|
102
|
+
async save(content: string, toolType: string, options: ArtifactSaveOptions = {}): Promise<string> {
|
|
98
103
|
const { id, path } = await this.allocatePath(toolType);
|
|
99
|
-
|
|
104
|
+
const maxBytes = Math.max(0, options.maxBytes ?? DEFAULT_ARTIFACT_MAX_BYTES);
|
|
105
|
+
const contentBytes = Buffer.byteLength(content, "utf-8");
|
|
106
|
+
if (contentBytes > maxBytes) {
|
|
107
|
+
const truncated = truncateHeadBytes(content, maxBytes);
|
|
108
|
+
await Bun.write(
|
|
109
|
+
path,
|
|
110
|
+
`${truncated.text}\n[artifact truncated after ${truncated.bytes} bytes; omitted at least ${contentBytes - truncated.bytes} bytes]\n`,
|
|
111
|
+
);
|
|
112
|
+
} else {
|
|
113
|
+
await Bun.write(path, content);
|
|
114
|
+
}
|
|
100
115
|
return id;
|
|
101
116
|
}
|
|
102
117
|
|
|
@@ -167,19 +167,49 @@ export class EphemeralBlobStore extends BlobStore {
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
export class MemoryBlobStore extends BlobStore {
|
|
170
|
+
/**
|
|
171
|
+
* Generous byte/count LRU bound (F8). Content-addressed resident blobs are fail-closed
|
|
172
|
+
* on miss (callers raise/handle {@link ResidentBlobMissingError}), so evicting the
|
|
173
|
+
* least-recently-used entry on an extremely large session is preferable to unbounded
|
|
174
|
+
* RAM growth. The caps sit well above normal usage and only trip on pathological sizes.
|
|
175
|
+
*/
|
|
176
|
+
static readonly #MAX_BYTES = 64 * 1024 * 1024;
|
|
177
|
+
static readonly #MAX_COUNT = 4096;
|
|
178
|
+
|
|
170
179
|
#blobs = new Map<string, Buffer>();
|
|
180
|
+
#bytes = 0;
|
|
171
181
|
|
|
172
182
|
constructor() {
|
|
173
183
|
super(":memory:");
|
|
174
184
|
}
|
|
175
185
|
|
|
186
|
+
#store(hash: string, data: Buffer): void {
|
|
187
|
+
const existing = this.#blobs.get(hash);
|
|
188
|
+
if (existing) {
|
|
189
|
+
this.#blobs.delete(hash);
|
|
190
|
+
this.#bytes -= existing.byteLength;
|
|
191
|
+
}
|
|
192
|
+
this.#blobs.set(hash, data);
|
|
193
|
+
this.#bytes += data.byteLength;
|
|
194
|
+
while (
|
|
195
|
+
(this.#bytes > MemoryBlobStore.#MAX_BYTES || this.#blobs.size > MemoryBlobStore.#MAX_COUNT) &&
|
|
196
|
+
this.#blobs.size > 1
|
|
197
|
+
) {
|
|
198
|
+
const oldest = this.#blobs.keys().next().value;
|
|
199
|
+
if (oldest === undefined) break;
|
|
200
|
+
const evicted = this.#blobs.get(oldest);
|
|
201
|
+
this.#blobs.delete(oldest);
|
|
202
|
+
if (evicted) this.#bytes -= evicted.byteLength;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
176
206
|
async put(data: Buffer): Promise<BlobPutResult> {
|
|
177
207
|
return this.putSync(data);
|
|
178
208
|
}
|
|
179
209
|
|
|
180
210
|
putSync(data: Buffer): BlobPutResult {
|
|
181
211
|
const hash = new Bun.SHA256().update(data).digest("hex");
|
|
182
|
-
this.#
|
|
212
|
+
this.#store(hash, Buffer.from(data));
|
|
183
213
|
return {
|
|
184
214
|
hash,
|
|
185
215
|
path: `memory:${hash}`,
|
|
@@ -195,7 +225,11 @@ export class MemoryBlobStore extends BlobStore {
|
|
|
195
225
|
|
|
196
226
|
getSync(hash: string): Buffer | null {
|
|
197
227
|
const data = this.#blobs.get(hash);
|
|
198
|
-
|
|
228
|
+
if (!data) return null;
|
|
229
|
+
// Refresh LRU recency on hit so hot blobs survive eviction.
|
|
230
|
+
this.#blobs.delete(hash);
|
|
231
|
+
this.#blobs.set(hash, data);
|
|
232
|
+
return Buffer.from(data);
|
|
199
233
|
}
|
|
200
234
|
|
|
201
235
|
async has(hash: string): Promise<boolean> {
|
|
@@ -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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
320
|
+
cache.set(tokenCount, stmt);
|
|
300
321
|
return stmt;
|
|
301
322
|
}
|
|
302
323
|
|