@gajae-code/coding-agent 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +1 -1
- package/dist/types/async/job-manager.d.ts +26 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/list-models.d.ts +6 -0
- package/dist/types/cli/setup-cli.d.ts +8 -1
- package/dist/types/commands/gc.d.ts +26 -0
- package/dist/types/commands/setup.d.ts +7 -0
- package/dist/types/config/file-lock-gc.d.ts +5 -0
- package/dist/types/config/file-lock.d.ts +29 -0
- 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/coordinator/contract.d.ts +1 -1
- package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
- package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
- package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
- package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +64 -2
- package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +11 -0
- package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
- 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/harness-control-plane/gc-adapter.d.ts +3 -0
- package/dist/types/harness-control-plane/owner.d.ts +7 -0
- package/dist/types/harness-control-plane/storage.d.ts +20 -0
- package/dist/types/modes/components/hook-selector.d.ts +7 -1
- package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +72 -2
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
- package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
- 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/session/agent-session.d.ts +1 -1
- package/dist/types/session/blob-store.d.ts +39 -3
- package/dist/types/session/history-storage.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +10 -1
- package/dist/types/setup/credential-import.d.ts +79 -0
- package/dist/types/skill-state/workflow-hud.d.ts +14 -0
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/render.d.ts +1 -1
- package/dist/types/tools/ask.d.ts +15 -1
- package/dist/types/tools/subagent-render.d.ts +7 -1
- package/dist/types/tools/subagent.d.ts +27 -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 +52 -0
- package/src/cli/args.ts +5 -0
- package/src/cli/auth-broker-cli.ts +1 -0
- package/src/cli/fast-help.ts +2 -0
- package/src/cli/list-models.ts +13 -1
- package/src/cli/setup-cli.ts +138 -3
- package/src/cli.ts +1 -0
- package/src/commands/gc.ts +22 -0
- package/src/commands/harness.ts +7 -3
- package/src/commands/setup.ts +5 -1
- package/src/commands/ultragoal.ts +3 -1
- package/src/config/file-lock-gc.ts +193 -0
- package/src/config/file-lock.ts +66 -10
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +39 -30
- 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/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/server.ts +459 -3
- package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
- package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
- package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
- package/src/defaults/gjc-defaults.ts +7 -0
- package/src/defaults/gjc-grok-cli.ts +22 -0
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +457 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
- package/src/gjc-runtime/deep-interview-state.ts +324 -0
- package/src/gjc-runtime/gc-render.ts +70 -0
- package/src/gjc-runtime/gc-runtime.ts +403 -0
- package/src/gjc-runtime/launch-tmux.ts +3 -4
- package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
- package/src/gjc-runtime/ralplan-runtime.ts +232 -19
- package/src/gjc-runtime/state-renderer.ts +12 -3
- package/src/gjc-runtime/state-runtime.ts +48 -30
- package/src/gjc-runtime/state-writer.ts +254 -7
- package/src/gjc-runtime/team-gc.ts +49 -0
- package/src/gjc-runtime/team-runtime.ts +179 -2
- package/src/gjc-runtime/tmux-common.ts +14 -0
- package/src/gjc-runtime/tmux-gc.ts +177 -0
- package/src/gjc-runtime/tmux-sessions.ts +49 -1
- package/src/gjc-runtime/ultragoal-guard.ts +155 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1239 -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/gc-adapter.ts +184 -0
- package/src/harness-control-plane/owner.ts +14 -2
- package/src/harness-control-plane/rpc-adapter.ts +1 -1
- package/src/harness-control-plane/storage.ts +70 -0
- package/src/hooks/skill-state.ts +121 -2
- package/src/internal-urls/docs-index.generated.ts +22 -12
- package/src/lsp/defaults.json +1 -0
- package/src/main.ts +18 -3
- package/src/modes/acp/acp-agent.ts +4 -2
- package/src/modes/bridge/bridge-mode.ts +2 -1
- package/src/modes/components/history-search.ts +5 -2
- package/src/modes/components/hook-selector.ts +19 -0
- package/src/modes/components/model-selector.ts +51 -8
- package/src/modes/components/provider-onboarding-selector.ts +6 -1
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/controllers/command-controller.ts +25 -6
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +81 -1
- package/src/modes/interactive-mode.ts +11 -1
- package/src/modes/rpc/rpc-mode.ts +266 -34
- package/src/modes/shared/agent-wire/command-dispatch.ts +281 -261
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +3 -0
- package/src/modes/shared/agent-wire/session-registry.ts +109 -0
- package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
- package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
- package/src/modes/shared/agent-wire/unattended-session.ts +32 -2
- 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/prompts/agents/executor.md +5 -2
- package/src/sdk.ts +29 -4
- package/src/session/agent-session.ts +99 -19
- package/src/session/blob-store.ts +59 -3
- package/src/session/history-storage.ts +32 -11
- package/src/session/session-manager.ts +72 -20
- package/src/setup/credential-import.ts +429 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
- package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
- package/src/skill-state/workflow-hud.ts +106 -10
- package/src/slash-commands/builtin-registry.ts +3 -2
- package/src/task/executor.ts +16 -1
- package/src/task/render.ts +18 -7
- package/src/tools/ask.ts +59 -2
- package/src/tools/cron.ts +1 -1
- package/src/tools/job.ts +3 -2
- package/src/tools/monitor.ts +36 -1
- package/src/tools/subagent-render.ts +128 -29
- package/src/tools/subagent.ts +173 -9
- 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
package/src/sdk.ts
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
Snowflake,
|
|
33
33
|
} from "@gajae-code/utils";
|
|
34
34
|
|
|
35
|
-
import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled } from "./async";
|
|
35
|
+
import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled, jobElapsedMs } from "./async";
|
|
36
36
|
import { loadCapability } from "./capability";
|
|
37
37
|
import { type Rule, ruleCapability, setActiveRules } from "./capability/rule";
|
|
38
38
|
import { ModelRegistry } from "./config/model-registry";
|
|
@@ -50,6 +50,7 @@ import { CursorExecHandlers } from "./cursor";
|
|
|
50
50
|
import "./discovery";
|
|
51
51
|
import { resolveConfigValue } from "./config/resolve-config-value";
|
|
52
52
|
import { getEmbeddedDefaultGjcSkills } from "./defaults/gjc-defaults";
|
|
53
|
+
import { BUNDLED_GROK_BUILD_EXTENSION_ID, getBundledGrokBuildExtensionFactory } from "./defaults/gjc-grok-cli";
|
|
53
54
|
import { initializeWithSettings } from "./discovery";
|
|
54
55
|
import { disposeAllKernelSessions, disposeKernelSessionsByOwner } from "./eval/py/executor";
|
|
55
56
|
import { TtsrManager } from "./export/ttsr";
|
|
@@ -114,6 +115,7 @@ import {
|
|
|
114
115
|
FindTool,
|
|
115
116
|
getSearchTools,
|
|
116
117
|
HIDDEN_TOOLS,
|
|
118
|
+
isConfigurableSearchProviderId,
|
|
117
119
|
isSearchProviderPreference,
|
|
118
120
|
type LspStartupServerInfo,
|
|
119
121
|
loadSshTool,
|
|
@@ -123,6 +125,7 @@ import {
|
|
|
123
125
|
SearchTool,
|
|
124
126
|
setPreferredImageProvider,
|
|
125
127
|
setPreferredSearchProvider,
|
|
128
|
+
setSearchFallbackProviders,
|
|
126
129
|
type Tool,
|
|
127
130
|
type ToolSession,
|
|
128
131
|
WebSearchTool,
|
|
@@ -132,6 +135,7 @@ import {
|
|
|
132
135
|
import { ToolContextStore } from "./tools/context";
|
|
133
136
|
import { getImageGenTools } from "./tools/image-gen";
|
|
134
137
|
import { wrapToolWithMetaNotice } from "./tools/output-meta";
|
|
138
|
+
import { guardToolForUltragoalAsk } from "./tools/ultragoal-ask-guard";
|
|
135
139
|
import { EventBus } from "./utils/event-bus";
|
|
136
140
|
import { buildNamedToolChoice, buildNamedToolChoiceResult } from "./utils/tool-choice";
|
|
137
141
|
import { buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree";
|
|
@@ -864,6 +868,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
864
868
|
if (typeof webSearchProvider === "string" && isSearchProviderPreference(webSearchProvider)) {
|
|
865
869
|
setPreferredSearchProvider(webSearchProvider);
|
|
866
870
|
}
|
|
871
|
+
const webSearchFallback = settings.get("web_search.fallback");
|
|
872
|
+
if (Array.isArray(webSearchFallback)) {
|
|
873
|
+
setSearchFallbackProviders(
|
|
874
|
+
webSearchFallback.filter(value => typeof value === "string" && isConfigurableSearchProviderId(value)),
|
|
875
|
+
);
|
|
876
|
+
}
|
|
867
877
|
|
|
868
878
|
const imageProvider = settings.get("providers.image");
|
|
869
879
|
if (
|
|
@@ -1124,7 +1134,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1124
1134
|
const formattedResult = await formatAsyncResultForFollowUp(result);
|
|
1125
1135
|
if (asyncJobManager!.isDeliverySuppressed(jobId)) return;
|
|
1126
1136
|
|
|
1127
|
-
const durationMs = job ?
|
|
1137
|
+
const durationMs = job ? jobElapsedMs(job) : undefined;
|
|
1128
1138
|
session.yieldQueue.enqueue<AsyncResultEntry>("async-result", {
|
|
1129
1139
|
jobId,
|
|
1130
1140
|
result: formattedResult,
|
|
@@ -1341,13 +1351,26 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1341
1351
|
}
|
|
1342
1352
|
|
|
1343
1353
|
// Extension/module discovery is quarantined; retain only the private
|
|
1344
|
-
// runtime needed for
|
|
1354
|
+
// runtime needed for bundled product extensions, explicitly supplied SDK
|
|
1355
|
+
// extension factories, and custom tools. Filesystem extension paths remain
|
|
1356
|
+
// ignored here even when options.additionalExtensionPaths is supplied.
|
|
1345
1357
|
const extensionsResult: LoadExtensionsResult = options.preloadedExtensions ?? {
|
|
1346
1358
|
extensions: [],
|
|
1347
1359
|
errors: [],
|
|
1348
1360
|
runtime: new ExtensionRuntime(),
|
|
1349
1361
|
};
|
|
1350
1362
|
|
|
1363
|
+
if (!extensionsResult.extensions.some(extension => extension.path === BUNDLED_GROK_BUILD_EXTENSION_ID)) {
|
|
1364
|
+
const bundledGrokExtension = await loadExtensionFromFactory(
|
|
1365
|
+
getBundledGrokBuildExtensionFactory(),
|
|
1366
|
+
cwd,
|
|
1367
|
+
eventBus,
|
|
1368
|
+
extensionsResult.runtime,
|
|
1369
|
+
BUNDLED_GROK_BUILD_EXTENSION_ID,
|
|
1370
|
+
);
|
|
1371
|
+
extensionsResult.extensions.push(bundledGrokExtension);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1351
1374
|
// Load inline extensions from factories
|
|
1352
1375
|
if (inlineExtensions.length > 0) {
|
|
1353
1376
|
for (let i = 0; i < inlineExtensions.length; i++) {
|
|
@@ -1792,7 +1815,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1792
1815
|
|
|
1793
1816
|
const initialTools = initialToolNames
|
|
1794
1817
|
.map(name => toolRegistry.get(name))
|
|
1795
|
-
.filter((tool): tool is AgentTool => tool !== undefined)
|
|
1818
|
+
.filter((tool): tool is AgentTool => tool !== undefined)
|
|
1819
|
+
// AgentSession tool wrapping is not installed until after Agent construction.
|
|
1820
|
+
.map(tool => guardToolForUltragoalAsk(tool, () => sessionManager.getCwd()));
|
|
1796
1821
|
|
|
1797
1822
|
const openaiWebsocketSetting = settings.get("providers.openaiWebsockets") ?? "off";
|
|
1798
1823
|
const preferOpenAICodexWebsockets =
|
|
@@ -240,6 +240,7 @@ import { normalizeLocalScheme, resolveToCwd } from "../tools/path-utils";
|
|
|
240
240
|
import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from "../tools/todo-write";
|
|
241
241
|
import { ToolAbortError, ToolError } from "../tools/tool-errors";
|
|
242
242
|
import { clampTimeout } from "../tools/tool-timeouts";
|
|
243
|
+
import { guardToolForUltragoalAsk } from "../tools/ultragoal-ask-guard";
|
|
243
244
|
import { parseCommandArgs } from "../utils/command-args";
|
|
244
245
|
import { type EditMode, resolveEditMode } from "../utils/edit-mode";
|
|
245
246
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
@@ -322,7 +323,10 @@ function isUnderProjectGjc(cwd: string, targetPath: string): boolean {
|
|
|
322
323
|
|
|
323
324
|
/** Listener function for agent session events */
|
|
324
325
|
export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
|
|
325
|
-
export type AsyncJobSnapshotItem = Pick<
|
|
326
|
+
export type AsyncJobSnapshotItem = Pick<
|
|
327
|
+
AsyncJob,
|
|
328
|
+
"id" | "type" | "status" | "label" | "startTime" | "endTime" | "metadata"
|
|
329
|
+
>;
|
|
326
330
|
|
|
327
331
|
export interface AsyncJobSnapshot {
|
|
328
332
|
running: AsyncJobSnapshotItem[];
|
|
@@ -903,6 +907,7 @@ export class AgentSession {
|
|
|
903
907
|
// Compaction state
|
|
904
908
|
#compactionAbortController: AbortController | undefined = undefined;
|
|
905
909
|
#autoCompactionAbortController: AbortController | undefined = undefined;
|
|
910
|
+
#prePromptContextCheckPromise: Promise<void> | undefined = undefined;
|
|
906
911
|
|
|
907
912
|
// Branch summarization state
|
|
908
913
|
#branchSummaryAbortController: AbortController | undefined = undefined;
|
|
@@ -1182,6 +1187,7 @@ export class AgentSession {
|
|
|
1182
1187
|
};
|
|
1183
1188
|
this.agent.setProviderResponseInterceptor(this.#onResponse);
|
|
1184
1189
|
this.agent.setRawSseEventInterceptor(this.#onSseEvent);
|
|
1190
|
+
this.#setGuardedAgentTools(this.agent.state.tools);
|
|
1185
1191
|
this.yieldQueue = new YieldQueue({
|
|
1186
1192
|
isStreaming: () => this.isStreaming,
|
|
1187
1193
|
injectStreaming: message => this.agent.followUp(message),
|
|
@@ -1563,6 +1569,7 @@ export class AgentSession {
|
|
|
1563
1569
|
status: job.status,
|
|
1564
1570
|
label: job.label,
|
|
1565
1571
|
startTime: job.startTime,
|
|
1572
|
+
endTime: job.endTime,
|
|
1566
1573
|
metadata: job.metadata,
|
|
1567
1574
|
}));
|
|
1568
1575
|
const recent = manager.getRecentJobs(options?.recentLimit ?? 5, ownerFilter).map(job => ({
|
|
@@ -1571,6 +1578,7 @@ export class AgentSession {
|
|
|
1571
1578
|
status: job.status,
|
|
1572
1579
|
label: job.label,
|
|
1573
1580
|
startTime: job.startTime,
|
|
1581
|
+
endTime: job.endTime,
|
|
1574
1582
|
metadata: job.metadata,
|
|
1575
1583
|
}));
|
|
1576
1584
|
const delivery = manager.getDeliveryState(ownerFilter);
|
|
@@ -3684,6 +3692,16 @@ export class AgentSession {
|
|
|
3684
3692
|
}) as T;
|
|
3685
3693
|
}
|
|
3686
3694
|
|
|
3695
|
+
#prepareToolForExecution<T extends AgentTool>(tool: T): T {
|
|
3696
|
+
return this.#wrapToolForDeepInterviewMutationGuard(
|
|
3697
|
+
this.#wrapToolForAcpPermission(guardToolForUltragoalAsk(tool, () => this.sessionManager.getCwd())),
|
|
3698
|
+
);
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
#setGuardedAgentTools(tools: AgentTool[]): void {
|
|
3702
|
+
this.agent.setTools(tools.map(tool => this.#prepareToolForExecution(tool)));
|
|
3703
|
+
}
|
|
3704
|
+
|
|
3687
3705
|
async #applyActiveToolsByName(
|
|
3688
3706
|
toolNames: string[],
|
|
3689
3707
|
options?: { persistMCPSelection?: boolean; previousSelectedMCPToolNames?: string[] },
|
|
@@ -3695,7 +3713,7 @@ export class AgentSession {
|
|
|
3695
3713
|
for (const name of toolNames) {
|
|
3696
3714
|
const tool = this.#toolRegistry.get(name);
|
|
3697
3715
|
if (tool) {
|
|
3698
|
-
tools.push(
|
|
3716
|
+
tools.push(tool);
|
|
3699
3717
|
validToolNames.push(name);
|
|
3700
3718
|
}
|
|
3701
3719
|
}
|
|
@@ -3712,7 +3730,7 @@ export class AgentSession {
|
|
|
3712
3730
|
this.#selectedDiscoveredToolNames.delete(name);
|
|
3713
3731
|
}
|
|
3714
3732
|
}
|
|
3715
|
-
this
|
|
3733
|
+
this.#setGuardedAgentTools(tools);
|
|
3716
3734
|
|
|
3717
3735
|
// Active tool set changed → discoverable tool list (which excludes already-active tools)
|
|
3718
3736
|
// is now stale. Invalidate before any prompt-template hook reads the discovery list.
|
|
@@ -3970,6 +3988,9 @@ export class AgentSession {
|
|
|
3970
3988
|
if (uniqueToolNames.size !== nextToolNames.length) {
|
|
3971
3989
|
throw new Error("RPC host tool names must be unique");
|
|
3972
3990
|
}
|
|
3991
|
+
if (uniqueToolNames.has("ask")) {
|
|
3992
|
+
throw new Error('RPC host tool "ask" is reserved and cannot be supplied by the host');
|
|
3993
|
+
}
|
|
3973
3994
|
|
|
3974
3995
|
for (const name of uniqueToolNames) {
|
|
3975
3996
|
if (this.#toolRegistry.has(name) && !this.#rpcHostToolNames.has(name)) {
|
|
@@ -4297,11 +4318,8 @@ export class AgentSession {
|
|
|
4297
4318
|
this.#toolRegistry.set(finalTool.name, finalTool);
|
|
4298
4319
|
|
|
4299
4320
|
if (!this.getActiveToolNames().includes(finalTool.name)) {
|
|
4300
|
-
const activeTools = [
|
|
4301
|
-
|
|
4302
|
-
this.#wrapToolForDeepInterviewMutationGuard(this.#wrapToolForAcpPermission(finalTool)),
|
|
4303
|
-
];
|
|
4304
|
-
this.agent.setTools(activeTools);
|
|
4321
|
+
const activeTools = [...this.agent.state.tools, finalTool];
|
|
4322
|
+
this.#setGuardedAgentTools(activeTools);
|
|
4305
4323
|
this.#invalidateDiscoveryCaches();
|
|
4306
4324
|
void this.refreshBaseSystemPrompt().catch(error => {
|
|
4307
4325
|
logger.warn("Failed to refresh system prompt after workflow gate ask tool registration", {
|
|
@@ -4333,9 +4351,8 @@ export class AgentSession {
|
|
|
4333
4351
|
const activeToolNames = this.getActiveToolNames();
|
|
4334
4352
|
const activeTools = activeToolNames
|
|
4335
4353
|
.map(name => this.#toolRegistry.get(name))
|
|
4336
|
-
.filter((tool): tool is AgentTool => tool !== undefined)
|
|
4337
|
-
|
|
4338
|
-
this.agent.setTools(activeTools);
|
|
4354
|
+
.filter((tool): tool is AgentTool => tool !== undefined);
|
|
4355
|
+
this.#setGuardedAgentTools(activeTools);
|
|
4339
4356
|
}
|
|
4340
4357
|
|
|
4341
4358
|
getCheckpointState(): CheckpointState | undefined {
|
|
@@ -4754,7 +4771,11 @@ export class AgentSession {
|
|
|
4754
4771
|
await this.#checkCompaction(lastAssistant, false);
|
|
4755
4772
|
}
|
|
4756
4773
|
if (!options?.skipCompactionCheck) {
|
|
4757
|
-
await this.#checkEstimatedContextBeforePrompt(
|
|
4774
|
+
await this.#checkEstimatedContextBeforePrompt([
|
|
4775
|
+
...(options?.prependMessages ?? []),
|
|
4776
|
+
message,
|
|
4777
|
+
...this.#pendingNextTurnMessages,
|
|
4778
|
+
]);
|
|
4758
4779
|
}
|
|
4759
4780
|
|
|
4760
4781
|
// Build messages array (session context, eager todo prelude, then active prompt message)
|
|
@@ -5219,7 +5240,9 @@ export class AgentSession {
|
|
|
5219
5240
|
}
|
|
5220
5241
|
await this.#syncSkillPromptActiveStateSafely(appMessage, true);
|
|
5221
5242
|
try {
|
|
5222
|
-
await this
|
|
5243
|
+
await this.#promptWithMessage(appMessage, this.#getCustomMessageTextContent(appMessage), {
|
|
5244
|
+
skipPostPromptRecoveryWait: true,
|
|
5245
|
+
});
|
|
5223
5246
|
} finally {
|
|
5224
5247
|
await this.#syncSkillPromptActiveStateSafely(appMessage, false);
|
|
5225
5248
|
}
|
|
@@ -5243,7 +5266,9 @@ export class AgentSession {
|
|
|
5243
5266
|
}
|
|
5244
5267
|
await this.#syncSkillPromptActiveStateSafely(appMessage, true);
|
|
5245
5268
|
try {
|
|
5246
|
-
await this
|
|
5269
|
+
await this.#promptWithMessage(appMessage, this.#getCustomMessageTextContent(appMessage), {
|
|
5270
|
+
skipPostPromptRecoveryWait: true,
|
|
5271
|
+
});
|
|
5247
5272
|
} finally {
|
|
5248
5273
|
await this.#syncSkillPromptActiveStateSafely(appMessage, false);
|
|
5249
5274
|
}
|
|
@@ -6546,7 +6571,23 @@ export class AgentSession {
|
|
|
6546
6571
|
}
|
|
6547
6572
|
}
|
|
6548
6573
|
|
|
6549
|
-
async #checkEstimatedContextBeforePrompt(): Promise<void> {
|
|
6574
|
+
async #checkEstimatedContextBeforePrompt(pendingMessages: readonly AgentMessage[] = []): Promise<void> {
|
|
6575
|
+
if (this.#prePromptContextCheckPromise) {
|
|
6576
|
+
await this.#prePromptContextCheckPromise;
|
|
6577
|
+
}
|
|
6578
|
+
|
|
6579
|
+
const checkPromise = this.#checkEstimatedContextBeforePromptOnce(pendingMessages);
|
|
6580
|
+
this.#prePromptContextCheckPromise = checkPromise;
|
|
6581
|
+
try {
|
|
6582
|
+
await checkPromise;
|
|
6583
|
+
} finally {
|
|
6584
|
+
if (this.#prePromptContextCheckPromise === checkPromise) {
|
|
6585
|
+
this.#prePromptContextCheckPromise = undefined;
|
|
6586
|
+
}
|
|
6587
|
+
}
|
|
6588
|
+
}
|
|
6589
|
+
|
|
6590
|
+
async #checkEstimatedContextBeforePromptOnce(pendingMessages: readonly AgentMessage[]): Promise<void> {
|
|
6550
6591
|
const model = this.model;
|
|
6551
6592
|
if (!model) return;
|
|
6552
6593
|
const contextWindow = model.contextWindow ?? 0;
|
|
@@ -6554,7 +6595,7 @@ export class AgentSession {
|
|
|
6554
6595
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
6555
6596
|
if (!compactionSettings.enabled || compactionSettings.strategy === "off") return;
|
|
6556
6597
|
|
|
6557
|
-
let contextTokens = this.#
|
|
6598
|
+
let contextTokens = this.#estimateContextTokensForCompaction(pendingMessages).tokens;
|
|
6558
6599
|
const maxOutputTokens = model.maxTokens ?? 0;
|
|
6559
6600
|
if (!shouldCompact(contextTokens, contextWindow, compactionSettings, maxOutputTokens)) return;
|
|
6560
6601
|
|
|
@@ -9144,7 +9185,7 @@ export class AgentSession {
|
|
|
9144
9185
|
error: String(mcpError),
|
|
9145
9186
|
});
|
|
9146
9187
|
this.#selectedMCPToolNames = new Set(previousSelectedMCPToolNames);
|
|
9147
|
-
this
|
|
9188
|
+
this.#setGuardedAgentTools(previousTools);
|
|
9148
9189
|
this.#baseSystemPrompt = previousBaseSystemPrompt;
|
|
9149
9190
|
this.agent.setSystemPrompt(previousSystemPrompt);
|
|
9150
9191
|
}
|
|
@@ -9597,6 +9638,21 @@ export class AgentSession {
|
|
|
9597
9638
|
*/
|
|
9598
9639
|
#estimateContextTokens(): {
|
|
9599
9640
|
tokens: number;
|
|
9641
|
+
} {
|
|
9642
|
+
return this.#estimateContextTokensWith(message => this.#estimateMessageDisplayTokens(message));
|
|
9643
|
+
}
|
|
9644
|
+
|
|
9645
|
+
#estimateContextTokensForCompaction(pendingMessages: readonly AgentMessage[]): {
|
|
9646
|
+
tokens: number;
|
|
9647
|
+
} {
|
|
9648
|
+
const estimate = this.#estimateContextTokensWith(message => this.#estimateMessageNativeContextTokens(message));
|
|
9649
|
+
return {
|
|
9650
|
+
tokens: estimate.tokens + this.#estimateMessagesNativeContextTokens(pendingMessages),
|
|
9651
|
+
};
|
|
9652
|
+
}
|
|
9653
|
+
|
|
9654
|
+
#estimateContextTokensWith(estimateMessage: (message: AgentMessage) => number): {
|
|
9655
|
+
tokens: number;
|
|
9600
9656
|
} {
|
|
9601
9657
|
const messages = this.messages;
|
|
9602
9658
|
|
|
@@ -9619,7 +9675,7 @@ export class AgentSession {
|
|
|
9619
9675
|
// No usage data - estimate all messages
|
|
9620
9676
|
let estimated = 0;
|
|
9621
9677
|
for (const message of messages) {
|
|
9622
|
-
estimated +=
|
|
9678
|
+
estimated += estimateMessage(message);
|
|
9623
9679
|
}
|
|
9624
9680
|
return {
|
|
9625
9681
|
tokens: estimated,
|
|
@@ -9629,7 +9685,7 @@ export class AgentSession {
|
|
|
9629
9685
|
const usageTokens = calculatePromptTokens(lastUsage);
|
|
9630
9686
|
let trailingTokens = 0;
|
|
9631
9687
|
for (let i = lastUsageIndex + 1; i < messages.length; i++) {
|
|
9632
|
-
trailingTokens +=
|
|
9688
|
+
trailingTokens += estimateMessage(messages[i]);
|
|
9633
9689
|
}
|
|
9634
9690
|
|
|
9635
9691
|
return {
|
|
@@ -9637,6 +9693,30 @@ export class AgentSession {
|
|
|
9637
9693
|
};
|
|
9638
9694
|
}
|
|
9639
9695
|
|
|
9696
|
+
#estimateMessagesNativeContextTokens(messages: readonly AgentMessage[]): number {
|
|
9697
|
+
let tokens = 0;
|
|
9698
|
+
for (const message of messages) {
|
|
9699
|
+
tokens += this.#estimateMessageNativeContextTokens(message);
|
|
9700
|
+
}
|
|
9701
|
+
return tokens;
|
|
9702
|
+
}
|
|
9703
|
+
|
|
9704
|
+
#estimateMessageDisplayTokens(message: AgentMessage): number {
|
|
9705
|
+
let tokens = 0;
|
|
9706
|
+
for (const llmMessage of convertToLlm([message])) {
|
|
9707
|
+
tokens += estimateMessageTokensHeuristic(llmMessage);
|
|
9708
|
+
}
|
|
9709
|
+
return tokens;
|
|
9710
|
+
}
|
|
9711
|
+
|
|
9712
|
+
#estimateMessageNativeContextTokens(message: AgentMessage): number {
|
|
9713
|
+
let tokens = 0;
|
|
9714
|
+
for (const llmMessage of convertToLlm([message])) {
|
|
9715
|
+
tokens += estimateTokens(llmMessage);
|
|
9716
|
+
}
|
|
9717
|
+
return tokens;
|
|
9718
|
+
}
|
|
9719
|
+
|
|
9640
9720
|
/**
|
|
9641
9721
|
* Export session to HTML.
|
|
9642
9722
|
* @param outputPath Optional output path (defaults to session directory)
|
|
@@ -267,7 +267,13 @@ export function externalizeImageDataSync(blobStore: BlobStore, base64Data: strin
|
|
|
267
267
|
/**
|
|
268
268
|
* Resolve an externalized provider image data URL back to its original string.
|
|
269
269
|
* If the data is not a blob reference, returns it unchanged.
|
|
270
|
-
*
|
|
270
|
+
*
|
|
271
|
+
* LEGACY PERSISTED-IMAGE COMPATIBILITY BOUNDARY: when the persisted blob is missing
|
|
272
|
+
* (e.g. resuming an old session whose image blob was pruned), this warns and returns
|
|
273
|
+
* the reference as-is rather than throwing, so legacy resume degrades gracefully.
|
|
274
|
+
* New resident byte-sensitive TEXT uses the fail-closed path instead
|
|
275
|
+
* (`resolveTextBlobSync` -> `ResidentBlobMissingError`). Do NOT route new byte-sensitive
|
|
276
|
+
* resident data through this warn-and-return path.
|
|
271
277
|
*/
|
|
272
278
|
export async function resolveImageDataUrl(blobStore: BlobStore, data: string): Promise<string> {
|
|
273
279
|
const hash = parseBlobRef(data);
|
|
@@ -284,7 +290,11 @@ export async function resolveImageDataUrl(blobStore: BlobStore, data: string): P
|
|
|
284
290
|
/**
|
|
285
291
|
* Resolve a blob reference back to base64 data.
|
|
286
292
|
* If the data is not a blob reference, returns it unchanged.
|
|
287
|
-
*
|
|
293
|
+
*
|
|
294
|
+
* LEGACY PERSISTED-IMAGE COMPATIBILITY BOUNDARY: when the blob is missing this warns
|
|
295
|
+
* and returns the reference as-is (downstream sees an invalid base64 ref but does not
|
|
296
|
+
* crash), preserving legacy-session resume. Byte-sensitive resident TEXT is fail-closed
|
|
297
|
+
* via `resolveTextBlobSync`; do NOT route new byte-sensitive resident data here.
|
|
288
298
|
*/
|
|
289
299
|
export async function resolveImageData(blobStore: BlobStore, data: string): Promise<string> {
|
|
290
300
|
const hash = parseBlobRef(data);
|
|
@@ -322,7 +332,14 @@ export function resolveImageDataSync(blobStore: BlobStore, data: string): string
|
|
|
322
332
|
return buffer.toString("base64");
|
|
323
333
|
}
|
|
324
334
|
|
|
325
|
-
/**
|
|
335
|
+
/**
|
|
336
|
+
* Synchronously resolve a blob reference back to utf8 text.
|
|
337
|
+
*
|
|
338
|
+
* FAIL-CLOSED byte-sensitive path: a missing resident blob throws
|
|
339
|
+
* `ResidentBlobMissingError` rather than degrading, so a missing resident text blob can
|
|
340
|
+
* never silently leak a `blob:sha256:` ref into provider payloads, UI, or exports.
|
|
341
|
+
* (Contrast the legacy persisted-image warn-and-return resolvers above.)
|
|
342
|
+
*/
|
|
326
343
|
export function resolveTextBlobSync(
|
|
327
344
|
blobStore: BlobStore,
|
|
328
345
|
data: string,
|
|
@@ -336,3 +353,42 @@ export function resolveTextBlobSync(
|
|
|
336
353
|
}
|
|
337
354
|
return buffer.toString("utf8");
|
|
338
355
|
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* FAIL-CLOSED resident variant of {@link resolveImageDataUrlSync}: a missing resident
|
|
359
|
+
* image-data-url blob throws `ResidentBlobMissingError` ("imageUrl") instead of warn-returning,
|
|
360
|
+
* so resident byte-sensitive provider image data can never leak a `blob:sha256:` ref into
|
|
361
|
+
* materialized entries, context, or provider payloads. The warn-and-return `resolveImageDataUrl*`
|
|
362
|
+
* resolvers remain ONLY for legacy persisted-image resume.
|
|
363
|
+
*/
|
|
364
|
+
export function resolveResidentImageDataUrlSync(
|
|
365
|
+
blobStore: BlobStore,
|
|
366
|
+
data: string,
|
|
367
|
+
context?: { sessionId?: string; sessionFile?: string },
|
|
368
|
+
): string {
|
|
369
|
+
const hash = parseBlobRef(data);
|
|
370
|
+
if (!hash) return data;
|
|
371
|
+
const buffer = blobStore.getSync(hash);
|
|
372
|
+
if (!buffer) {
|
|
373
|
+
throw new ResidentBlobMissingError(hash, "imageUrl", context?.sessionId, context?.sessionFile);
|
|
374
|
+
}
|
|
375
|
+
return buffer.toString("utf8");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* FAIL-CLOSED resident variant of {@link resolveImageDataSync}: a missing resident image blob
|
|
380
|
+
* throws `ResidentBlobMissingError` ("imageData") instead of warn-returning a placeholder.
|
|
381
|
+
*/
|
|
382
|
+
export function resolveResidentImageDataSync(
|
|
383
|
+
blobStore: BlobStore,
|
|
384
|
+
data: string,
|
|
385
|
+
context?: { sessionId?: string; sessionFile?: string },
|
|
386
|
+
): string {
|
|
387
|
+
const hash = parseBlobRef(data);
|
|
388
|
+
if (!hash) return data;
|
|
389
|
+
const buffer = blobStore.getSync(hash);
|
|
390
|
+
if (!buffer) {
|
|
391
|
+
throw new ResidentBlobMissingError(hash, "imageData", context?.sessionId, context?.sessionFile);
|
|
392
|
+
}
|
|
393
|
+
return buffer.toString("base64");
|
|
394
|
+
}
|
|
@@ -67,10 +67,14 @@ export class HistoryStorage {
|
|
|
67
67
|
// Prepared statements
|
|
68
68
|
#insertRowStmt: Statement;
|
|
69
69
|
#recentStmt: Statement;
|
|
70
|
+
#recentByCwdStmt: Statement;
|
|
70
71
|
#searchStmt: Statement;
|
|
72
|
+
#searchByCwdStmt: Statement;
|
|
71
73
|
#lastPromptStmt: Statement;
|
|
72
74
|
// Cache substring-fallback prepared statements keyed by token count.
|
|
73
75
|
#substringStmts = new Map<number, Statement>();
|
|
76
|
+
// Cache cwd-filtered substring-fallback statements keyed by token count.
|
|
77
|
+
#substringCwdStmts = new Map<number, Statement>();
|
|
74
78
|
|
|
75
79
|
// In-memory cache of last prompt to avoid sync DB reads on add
|
|
76
80
|
#lastPromptCache: string | null = null;
|
|
@@ -94,6 +98,7 @@ CREATE TABLE IF NOT EXISTS history (
|
|
|
94
98
|
cwd TEXT
|
|
95
99
|
);
|
|
96
100
|
CREATE INDEX IF NOT EXISTS idx_history_created_at ON history(created_at DESC);
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_history_cwd_created_at ON history(cwd, created_at DESC);
|
|
97
102
|
|
|
98
103
|
CREATE VIRTUAL TABLE IF NOT EXISTS history_fts USING fts5(prompt, content='history', content_rowid='id');
|
|
99
104
|
|
|
@@ -117,9 +122,15 @@ CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
|
|
|
117
122
|
this.#recentStmt = this.#db.prepare(
|
|
118
123
|
"SELECT id, prompt, created_at, cwd FROM history ORDER BY created_at DESC, id DESC LIMIT ?",
|
|
119
124
|
);
|
|
125
|
+
this.#recentByCwdStmt = this.#db.prepare(
|
|
126
|
+
"SELECT id, prompt, created_at, cwd FROM history WHERE cwd = ? ORDER BY created_at DESC, id DESC LIMIT ?",
|
|
127
|
+
);
|
|
120
128
|
this.#searchStmt = this.#db.prepare(
|
|
121
129
|
"SELECT h.id, h.prompt, h.created_at, h.cwd FROM history_fts f JOIN history h ON h.id = f.rowid WHERE history_fts MATCH ? ORDER BY h.created_at DESC, h.id DESC LIMIT ?",
|
|
122
130
|
);
|
|
131
|
+
this.#searchByCwdStmt = this.#db.prepare(
|
|
132
|
+
"SELECT h.id, h.prompt, h.created_at, h.cwd FROM history_fts f JOIN history h ON h.id = f.rowid WHERE history_fts MATCH ? AND h.cwd = ? ORDER BY h.created_at DESC, h.id DESC LIMIT ?",
|
|
133
|
+
);
|
|
123
134
|
this.#lastPromptStmt = this.#db.prepare("SELECT prompt FROM history ORDER BY id DESC LIMIT 1");
|
|
124
135
|
|
|
125
136
|
this.#insertRowStmt = this.#db.prepare("INSERT INTO history (prompt, cwd) VALUES (?, ?)");
|
|
@@ -158,12 +169,14 @@ CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
|
|
|
158
169
|
});
|
|
159
170
|
}
|
|
160
171
|
|
|
161
|
-
getRecent(limit: number): HistoryEntry[] {
|
|
172
|
+
getRecent(limit: number, cwd?: string): HistoryEntry[] {
|
|
162
173
|
const safeLimit = this.#normalizeLimit(limit);
|
|
163
174
|
if (safeLimit === 0) return [];
|
|
164
175
|
|
|
165
176
|
try {
|
|
166
|
-
const rows =
|
|
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
|
|