@stackmemoryai/stackmemory 1.10.5 → 1.12.0
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/README.md +104 -23
- package/dist/src/cli/claude-sm.js +266 -84
- package/dist/src/cli/codex-sm.js +185 -33
- package/dist/src/cli/commands/bench.js +209 -2
- package/dist/src/cli/commands/cache.js +126 -0
- package/dist/src/cli/commands/daemon.js +41 -0
- package/dist/src/cli/commands/handoff.js +40 -9
- package/dist/src/cli/commands/onboard.js +70 -3
- package/dist/src/cli/commands/optimize.js +117 -0
- package/dist/src/cli/commands/orchestrate.js +230 -5
- package/dist/src/cli/commands/orchestrator.js +312 -24
- package/dist/src/cli/commands/pack.js +322 -0
- package/dist/src/cli/commands/search.js +40 -1
- package/dist/src/cli/commands/setup.js +177 -7
- package/dist/src/cli/commands/skills.js +10 -1
- package/dist/src/cli/commands/state.js +265 -0
- package/dist/src/cli/gemini-sm.js +19 -29
- package/dist/src/cli/index.js +90 -29
- package/dist/src/cli/opencode-sm.js +38 -21
- package/dist/src/cli/utils/determinism-watcher.js +66 -0
- package/dist/src/cli/utils/real-cli-bin.js +44 -0
- package/dist/src/core/cache/content-cache.js +238 -0
- package/dist/src/core/cache/index.js +11 -0
- package/dist/src/core/cache/token-estimator.js +16 -0
- package/dist/src/core/context/frame-database.js +38 -30
- package/dist/src/core/cross-search/cross-project-search.js +269 -0
- package/dist/src/core/{merge → cross-search}/index.js +6 -4
- package/dist/src/core/database/sqlite-adapter.js +0 -83
- package/dist/src/core/extensions/provider-adapter.js +5 -0
- package/dist/src/core/models/model-router.js +22 -2
- package/dist/src/core/monitoring/logger.js +2 -1
- package/dist/src/core/optimization/trace-optimizer.js +413 -0
- package/dist/src/core/provenance/confidence-scorer.js +128 -0
- package/dist/src/core/provenance/index.js +40 -0
- package/dist/src/core/provenance/provenance-store.js +194 -0
- package/dist/src/core/provenance/types.js +82 -0
- package/dist/src/core/session/project-handoff.js +64 -0
- package/dist/src/core/session/session-manager.js +28 -0
- package/dist/src/core/shared-state/canonical-store.js +564 -0
- package/dist/src/core/skill-packs/index.js +18 -0
- package/dist/src/core/skill-packs/parser.js +42 -0
- package/dist/src/core/skill-packs/registry.js +224 -0
- package/dist/src/core/skill-packs/types.js +66 -0
- package/dist/src/core/trace/trace-event-store.js +282 -0
- package/dist/src/core/trace/trace-event.js +4 -0
- package/dist/src/daemon/daemon-config.js +7 -0
- package/dist/src/daemon/services/github-service.js +126 -0
- package/dist/src/daemon/unified-daemon.js +30 -0
- package/dist/src/features/sweep/pty-wrapper.js +13 -5
- package/dist/src/hooks/schemas.js +2 -0
- package/dist/src/integrations/claude-code/subagent-client.js +89 -0
- package/dist/src/integrations/github/pr-state.js +158 -0
- package/dist/src/integrations/linear/client.js +4 -1
- package/dist/src/integrations/mcp/handlers/cross-search-handlers.js +188 -0
- package/dist/src/integrations/mcp/handlers/index.js +40 -59
- package/dist/src/integrations/mcp/server.js +425 -311
- package/dist/src/integrations/mcp/tool-alias-registry.js +370 -0
- package/dist/src/integrations/mcp/tool-definitions.js +98 -229
- package/dist/src/integrations/ralph/context/stackmemory-context-loader.js +3 -40
- package/dist/src/integrations/ralph/learning/pattern-learner.js +1 -20
- package/dist/src/integrations/ralph/swarm/swarm-coordinator.js +0 -2
- package/dist/src/mcp/stackmemory-mcp-server.js +315 -0
- package/dist/src/orchestrators/multimodal/determinism.js +243 -0
- package/dist/src/orchestrators/multimodal/harness.js +147 -77
- package/dist/src/orchestrators/multimodal/providers.js +44 -3
- package/dist/src/utils/hook-installer.js +0 -8
- package/package.json +10 -1
- package/packs/coding/python-fastapi/instructions.md +60 -0
- package/packs/coding/python-fastapi/pack.yaml +28 -0
- package/packs/coding/typescript-react/instructions.md +47 -0
- package/packs/coding/typescript-react/pack.yaml +28 -0
- package/packs/core/commands/capture.md +32 -0
- package/packs/core/commands/learn.md +73 -0
- package/packs/core/commands/next.md +36 -0
- package/packs/core/commands/restart.md +58 -0
- package/packs/core/commands/restore.md +29 -0
- package/packs/core/commands/start.md +57 -0
- package/packs/core/commands/stop.md +65 -0
- package/packs/core/commands/summary.md +40 -0
- package/packs/core/manifest.json +24 -0
- package/packs/ops/decision-recovery/instructions.md +65 -0
- package/packs/ops/decision-recovery/pack.yaml +89 -0
- package/dist/src/cli/commands/team.js +0 -168
- package/dist/src/core/context/shared-context-layer.js +0 -620
- package/dist/src/core/context/stack-merge-resolver.js +0 -748
- package/dist/src/core/merge/conflict-detector.js +0 -430
- package/dist/src/core/merge/resolution-engine.js +0 -557
- package/dist/src/core/merge/stack-diff.js +0 -531
- package/dist/src/core/merge/unified-merge-resolver.js +0 -302
- package/dist/src/integrations/mcp/handlers/cord-handlers.js +0 -397
- package/dist/src/integrations/mcp/handlers/team-handlers.js +0 -211
- /package/dist/src/core/{merge → cache}/types.js +0 -0
|
@@ -13,6 +13,11 @@ import { program } from "commander";
|
|
|
13
13
|
import { v4 as uuidv4 } from "uuid";
|
|
14
14
|
import chalk from "chalk";
|
|
15
15
|
import { initializeTracing, trace } from "../core/trace/index.js";
|
|
16
|
+
import { resolveRealCliBin } from "./utils/real-cli-bin.js";
|
|
17
|
+
import {
|
|
18
|
+
startDeterminismWatcher,
|
|
19
|
+
stopDeterminismWatcher
|
|
20
|
+
} from "./utils/determinism-watcher.js";
|
|
16
21
|
const DEFAULT_SM_CONFIG = {
|
|
17
22
|
defaultWorktree: false,
|
|
18
23
|
defaultTracing: true
|
|
@@ -43,6 +48,7 @@ class OpencodeSM {
|
|
|
43
48
|
config;
|
|
44
49
|
stackmemoryPath;
|
|
45
50
|
smConfig;
|
|
51
|
+
determinismWatcher;
|
|
46
52
|
constructor() {
|
|
47
53
|
this.smConfig = loadSMConfig();
|
|
48
54
|
this.config = {
|
|
@@ -54,6 +60,7 @@ class OpencodeSM {
|
|
|
54
60
|
sessionStartTime: Date.now()
|
|
55
61
|
};
|
|
56
62
|
this.stackmemoryPath = this.findStackMemory();
|
|
63
|
+
this.determinismWatcher = null;
|
|
57
64
|
}
|
|
58
65
|
getRepoRoot() {
|
|
59
66
|
try {
|
|
@@ -127,27 +134,16 @@ class OpencodeSM {
|
|
|
127
134
|
}
|
|
128
135
|
}
|
|
129
136
|
resolveOpencodeBin() {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
"
|
|
139
|
-
|
|
140
|
-
for (const binPath of possiblePaths) {
|
|
141
|
-
if (fs.existsSync(binPath)) {
|
|
142
|
-
return binPath;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
try {
|
|
146
|
-
execSync("which opencode", { stdio: "ignore" });
|
|
147
|
-
return "opencode";
|
|
148
|
-
} catch {
|
|
149
|
-
}
|
|
150
|
-
return null;
|
|
137
|
+
return resolveRealCliBin({
|
|
138
|
+
explicitBin: this.config.opencodeBin,
|
|
139
|
+
envBin: process.env["OPENCODE_BIN"],
|
|
140
|
+
preferredPaths: [
|
|
141
|
+
path.join(os.homedir(), ".opencode", "bin", "opencode"),
|
|
142
|
+
"/usr/local/bin/opencode",
|
|
143
|
+
"/opt/homebrew/bin/opencode"
|
|
144
|
+
],
|
|
145
|
+
pathCommands: ["opencode"]
|
|
146
|
+
});
|
|
151
147
|
}
|
|
152
148
|
setupWorktree() {
|
|
153
149
|
if (!this.config.useWorktree || !this.isGitRepo()) {
|
|
@@ -251,6 +247,23 @@ class OpencodeSM {
|
|
|
251
247
|
);
|
|
252
248
|
}
|
|
253
249
|
}
|
|
250
|
+
startDeterminismWatcher() {
|
|
251
|
+
this.determinismWatcher = startDeterminismWatcher({
|
|
252
|
+
stackmemoryBin: this.stackmemoryPath,
|
|
253
|
+
cwd: process.cwd(),
|
|
254
|
+
task: this.config.task,
|
|
255
|
+
instanceId: this.config.instanceId,
|
|
256
|
+
tool: "opencode"
|
|
257
|
+
});
|
|
258
|
+
if (this.determinismWatcher) {
|
|
259
|
+
const modeLabel = this.determinismWatcher.mode === "targeted" ? "targeted" : "repo-root fallback";
|
|
260
|
+
console.log(chalk.gray(`Determinism: ${modeLabel}`));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
stopDeterminismWatcher() {
|
|
264
|
+
stopDeterminismWatcher(this.determinismWatcher);
|
|
265
|
+
this.determinismWatcher = null;
|
|
266
|
+
}
|
|
254
267
|
async run(args) {
|
|
255
268
|
const opencodeArgs = [];
|
|
256
269
|
let i = 0;
|
|
@@ -354,6 +367,7 @@ class OpencodeSM {
|
|
|
354
367
|
if (this.config.worktreePath) {
|
|
355
368
|
process.env["OPENCODE_WORKTREE_PATH"] = this.config.worktreePath;
|
|
356
369
|
}
|
|
370
|
+
this.startDeterminismWatcher();
|
|
357
371
|
console.log(chalk.gray(`Instance: ${this.config.instanceId}`));
|
|
358
372
|
console.log(chalk.gray(`Working in: ${process.cwd()}`));
|
|
359
373
|
if (this.config.tracingEnabled) {
|
|
@@ -391,6 +405,7 @@ class OpencodeSM {
|
|
|
391
405
|
process.exit(1);
|
|
392
406
|
});
|
|
393
407
|
opencode.on("exit", async (code) => {
|
|
408
|
+
this.stopDeterminismWatcher();
|
|
394
409
|
this.saveContext("OpenCode session ended", {
|
|
395
410
|
action: "session_end",
|
|
396
411
|
exitCode: code
|
|
@@ -411,12 +426,14 @@ class OpencodeSM {
|
|
|
411
426
|
process.exit(code || 0);
|
|
412
427
|
});
|
|
413
428
|
process.on("SIGINT", () => {
|
|
429
|
+
this.stopDeterminismWatcher();
|
|
414
430
|
this.saveContext("OpenCode session interrupted", {
|
|
415
431
|
action: "session_interrupt"
|
|
416
432
|
});
|
|
417
433
|
opencode.kill("SIGINT");
|
|
418
434
|
});
|
|
419
435
|
process.on("SIGTERM", () => {
|
|
436
|
+
this.stopDeterminismWatcher();
|
|
420
437
|
this.saveContext("OpenCode session terminated", {
|
|
421
438
|
action: "session_terminate"
|
|
422
439
|
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
import { spawn } from "child_process";
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import { getDeterminismWatchTargets } from "../../orchestrators/multimodal/determinism.js";
|
|
9
|
+
function shouldAutoStartDeterminismWatcher(cwd) {
|
|
10
|
+
if (process.env["STACKMEMORY_DETERMINISM_AUTO"] === "0") {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
return fs.existsSync(path.join(cwd, ".git"));
|
|
14
|
+
}
|
|
15
|
+
function startDeterminismWatcher(options) {
|
|
16
|
+
if (!shouldAutoStartDeterminismWatcher(options.cwd)) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const runs = process.env["STACKMEMORY_DETERMINISM_RUNS"] || "3";
|
|
20
|
+
const task = options.task || process.env["STACKMEMORY_DETERMINISM_TASK"] || "Determinism probe";
|
|
21
|
+
const targets = getDeterminismWatchTargets(options.cwd);
|
|
22
|
+
const mode = targets.length === 1 && targets[0] === "." ? "repo-root" : "targeted";
|
|
23
|
+
const child = spawn(
|
|
24
|
+
options.stackmemoryBin,
|
|
25
|
+
[
|
|
26
|
+
"bench",
|
|
27
|
+
"determinism",
|
|
28
|
+
"--task",
|
|
29
|
+
task,
|
|
30
|
+
"--runs",
|
|
31
|
+
runs,
|
|
32
|
+
"--watch",
|
|
33
|
+
"--json"
|
|
34
|
+
],
|
|
35
|
+
{
|
|
36
|
+
cwd: options.cwd,
|
|
37
|
+
stdio: "ignore",
|
|
38
|
+
env: {
|
|
39
|
+
...process.env,
|
|
40
|
+
STACKMEMORY_DETERMINISM_PARENT_TOOL: options.tool,
|
|
41
|
+
STACKMEMORY_DETERMINISM_PARENT_INSTANCE: options.instanceId || "",
|
|
42
|
+
STACKMEMORY_DETERMINISM_PARENT_SESSION: options.sessionId || ""
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
return {
|
|
47
|
+
child,
|
|
48
|
+
mode,
|
|
49
|
+
targets
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function stopDeterminismWatcher(handle) {
|
|
53
|
+
const child = handle?.child ?? null;
|
|
54
|
+
if (!child || child.killed) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
child.kill("SIGTERM");
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export {
|
|
63
|
+
shouldAutoStartDeterminismWatcher,
|
|
64
|
+
startDeterminismWatcher,
|
|
65
|
+
stopDeterminismWatcher
|
|
66
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
const DEFAULT_WRAPPER_PATH_SNIPPETS = [
|
|
8
|
+
"/Applications/cmux.app/Contents/Resources/bin/"
|
|
9
|
+
];
|
|
10
|
+
function isWrapperPath(candidate, wrapperPathSnippets) {
|
|
11
|
+
const normalized = candidate.trim();
|
|
12
|
+
return wrapperPathSnippets.some((snippet) => normalized.includes(snippet));
|
|
13
|
+
}
|
|
14
|
+
function resolveRealCliBin(options) {
|
|
15
|
+
if (options.explicitBin?.trim()) {
|
|
16
|
+
return options.explicitBin.trim();
|
|
17
|
+
}
|
|
18
|
+
if (options.envBin?.trim()) {
|
|
19
|
+
return options.envBin.trim();
|
|
20
|
+
}
|
|
21
|
+
const wrapperPathSnippets = options.wrapperPathSnippets || DEFAULT_WRAPPER_PATH_SNIPPETS;
|
|
22
|
+
for (const candidate of options.preferredPaths || []) {
|
|
23
|
+
if (fs.existsSync(candidate) && !isWrapperPath(candidate, wrapperPathSnippets)) {
|
|
24
|
+
return candidate;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
for (const command of options.pathCommands) {
|
|
28
|
+
try {
|
|
29
|
+
const output = execSync(`which -a ${command}`, {
|
|
30
|
+
encoding: "utf8",
|
|
31
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
32
|
+
});
|
|
33
|
+
const resolved = output.split("\n").map((line) => line.trim()).filter(Boolean).find((candidate) => !isWrapperPath(candidate, wrapperPathSnippets));
|
|
34
|
+
if (resolved) {
|
|
35
|
+
return resolved;
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
resolveRealCliBin
|
|
44
|
+
};
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
import { logger } from "../monitoring/logger.js";
|
|
6
|
+
import { estimateTokens, hashContent } from "./token-estimator.js";
|
|
7
|
+
class ContentCache {
|
|
8
|
+
db;
|
|
9
|
+
constructor(db) {
|
|
10
|
+
this.db = db;
|
|
11
|
+
this.initializeSchema();
|
|
12
|
+
}
|
|
13
|
+
// ------------------------------------------------------------------
|
|
14
|
+
// Schema
|
|
15
|
+
// ------------------------------------------------------------------
|
|
16
|
+
initializeSchema() {
|
|
17
|
+
this.db.exec(`
|
|
18
|
+
CREATE TABLE IF NOT EXISTS content_cache (
|
|
19
|
+
hash TEXT PRIMARY KEY,
|
|
20
|
+
content TEXT NOT NULL,
|
|
21
|
+
token_count INTEGER NOT NULL,
|
|
22
|
+
hit_count INTEGER NOT NULL DEFAULT 0,
|
|
23
|
+
first_seen INTEGER NOT NULL,
|
|
24
|
+
last_seen INTEGER NOT NULL,
|
|
25
|
+
source TEXT NOT NULL DEFAULT '',
|
|
26
|
+
metadata TEXT
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_cache_source ON content_cache(source);
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_cache_last_seen ON content_cache(last_seen);
|
|
31
|
+
`);
|
|
32
|
+
const hasFts = this.db.prepare(
|
|
33
|
+
`SELECT name FROM sqlite_master WHERE type='table' AND name='content_cache_fts'`
|
|
34
|
+
).get();
|
|
35
|
+
if (!hasFts) {
|
|
36
|
+
this.db.exec(`
|
|
37
|
+
CREATE VIRTUAL TABLE content_cache_fts
|
|
38
|
+
USING fts5(content, hash UNINDEXED, content_rowid='rowid');
|
|
39
|
+
`);
|
|
40
|
+
}
|
|
41
|
+
logger.debug("ContentCache: schema initialized");
|
|
42
|
+
}
|
|
43
|
+
// ------------------------------------------------------------------
|
|
44
|
+
// Core operations
|
|
45
|
+
// ------------------------------------------------------------------
|
|
46
|
+
/**
|
|
47
|
+
* Look up content by hash. If it exists, increments hit_count and
|
|
48
|
+
* returns the saved tokens. Otherwise returns a miss.
|
|
49
|
+
*/
|
|
50
|
+
lookup(content, source) {
|
|
51
|
+
const hash = hashContent(content);
|
|
52
|
+
const row = this.db.prepare("SELECT * FROM content_cache WHERE hash = ?").get(hash);
|
|
53
|
+
if (!row) {
|
|
54
|
+
return { hit: false, hash, tokensSaved: 0 };
|
|
55
|
+
}
|
|
56
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
57
|
+
this.db.prepare(
|
|
58
|
+
"UPDATE content_cache SET hit_count = hit_count + 1, last_seen = ? WHERE hash = ?"
|
|
59
|
+
).run(now, hash);
|
|
60
|
+
if (source && source !== row.source) {
|
|
61
|
+
this.db.prepare("UPDATE content_cache SET source = ? WHERE hash = ?").run(source, hash);
|
|
62
|
+
}
|
|
63
|
+
const entry = this.rowToEntry({
|
|
64
|
+
...row,
|
|
65
|
+
hit_count: row.hit_count + 1,
|
|
66
|
+
last_seen: now,
|
|
67
|
+
source: source ?? row.source
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
hit: true,
|
|
71
|
+
hash,
|
|
72
|
+
entry,
|
|
73
|
+
tokensSaved: entry.tokenCount
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Insert or update a cache entry. Returns the entry.
|
|
78
|
+
*/
|
|
79
|
+
put(content, source, metadata) {
|
|
80
|
+
const hash = hashContent(content);
|
|
81
|
+
const tokenCount = estimateTokens(content);
|
|
82
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
83
|
+
const src = source ?? "";
|
|
84
|
+
const meta = metadata ? JSON.stringify(metadata) : null;
|
|
85
|
+
const existing = this.db.prepare("SELECT hash FROM content_cache WHERE hash = ?").get(hash);
|
|
86
|
+
if (existing) {
|
|
87
|
+
this.db.prepare(
|
|
88
|
+
"UPDATE content_cache SET hit_count = hit_count + 1, last_seen = ?, source = ?, metadata = ? WHERE hash = ?"
|
|
89
|
+
).run(now, src, meta, hash);
|
|
90
|
+
} else {
|
|
91
|
+
this.db.prepare(
|
|
92
|
+
`INSERT INTO content_cache (hash, content, token_count, hit_count, first_seen, last_seen, source, metadata)
|
|
93
|
+
VALUES (?, ?, ?, 0, ?, ?, ?, ?)`
|
|
94
|
+
).run(hash, content, tokenCount, now, now, src, meta);
|
|
95
|
+
this.db.prepare(`INSERT INTO content_cache_fts (content, hash) VALUES (?, ?)`).run(content, hash);
|
|
96
|
+
}
|
|
97
|
+
return this.getEntry(hash);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Retrieve a single entry by hash.
|
|
101
|
+
*/
|
|
102
|
+
getEntry(hash) {
|
|
103
|
+
const row = this.db.prepare("SELECT * FROM content_cache WHERE hash = ?").get(hash);
|
|
104
|
+
return row ? this.rowToEntry(row) : void 0;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Aggregate cache statistics.
|
|
108
|
+
*/
|
|
109
|
+
getStats() {
|
|
110
|
+
const agg = this.db.prepare(
|
|
111
|
+
`SELECT
|
|
112
|
+
COUNT(*) as total_entries,
|
|
113
|
+
COALESCE(SUM(token_count), 0) as total_tokens_cached,
|
|
114
|
+
COALESCE(SUM(hit_count * token_count), 0) as total_tokens_saved,
|
|
115
|
+
COALESCE(SUM(hit_count), 0) as total_hits,
|
|
116
|
+
COUNT(*) as total_lookups
|
|
117
|
+
FROM content_cache`
|
|
118
|
+
).get();
|
|
119
|
+
const totalHits = agg.total_hits;
|
|
120
|
+
const totalEntries = agg.total_entries;
|
|
121
|
+
const hitRate = totalHits + totalEntries > 0 ? totalHits / (totalHits + totalEntries) : 0;
|
|
122
|
+
const topRows = this.db.prepare(
|
|
123
|
+
`SELECT source, SUM(hit_count * token_count) as tokens_saved
|
|
124
|
+
FROM content_cache
|
|
125
|
+
WHERE source != ''
|
|
126
|
+
GROUP BY source
|
|
127
|
+
ORDER BY tokens_saved DESC
|
|
128
|
+
LIMIT 10`
|
|
129
|
+
).all();
|
|
130
|
+
return {
|
|
131
|
+
totalEntries,
|
|
132
|
+
totalTokensCached: agg.total_tokens_cached,
|
|
133
|
+
totalTokensSaved: agg.total_tokens_saved,
|
|
134
|
+
hitRate,
|
|
135
|
+
topSources: topRows.map((r) => ({
|
|
136
|
+
source: r.source,
|
|
137
|
+
tokensSaved: r.tokens_saved
|
|
138
|
+
}))
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Remove entries older than the given unix timestamp.
|
|
143
|
+
* Returns the number of evicted entries.
|
|
144
|
+
*/
|
|
145
|
+
evict(olderThan) {
|
|
146
|
+
const cutoff = olderThan ?? Math.floor(Date.now() / 1e3);
|
|
147
|
+
this.db.prepare(
|
|
148
|
+
`DELETE FROM content_cache_fts
|
|
149
|
+
WHERE hash IN (SELECT hash FROM content_cache WHERE last_seen < ?)`
|
|
150
|
+
).run(cutoff);
|
|
151
|
+
const result = this.db.prepare("DELETE FROM content_cache WHERE last_seen < ?").run(cutoff);
|
|
152
|
+
if (result.changes > 0) {
|
|
153
|
+
logger.debug(`ContentCache: evicted ${result.changes} entries`);
|
|
154
|
+
}
|
|
155
|
+
return result.changes;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Search cached content via FTS5.
|
|
159
|
+
*/
|
|
160
|
+
search(query, limit = 20) {
|
|
161
|
+
if (!query.trim()) return [];
|
|
162
|
+
const sanitized = this.sanitizeFtsQuery(query);
|
|
163
|
+
const rows = this.db.prepare(
|
|
164
|
+
`SELECT cc.*
|
|
165
|
+
FROM content_cache_fts fts
|
|
166
|
+
JOIN content_cache cc ON cc.hash = fts.hash
|
|
167
|
+
WHERE content_cache_fts MATCH ?
|
|
168
|
+
LIMIT ?`
|
|
169
|
+
).all(sanitized, limit);
|
|
170
|
+
return rows.map((r) => this.rowToEntry(r));
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Remove all entries.
|
|
174
|
+
*/
|
|
175
|
+
clear() {
|
|
176
|
+
this.db.exec("DELETE FROM content_cache_fts");
|
|
177
|
+
this.db.exec("DELETE FROM content_cache");
|
|
178
|
+
logger.debug("ContentCache: cleared");
|
|
179
|
+
}
|
|
180
|
+
// ------------------------------------------------------------------
|
|
181
|
+
// Key-based operations (for input-addressed caching, e.g., tool+args → result)
|
|
182
|
+
// ------------------------------------------------------------------
|
|
183
|
+
/**
|
|
184
|
+
* Look up cached result by an explicit key (e.g., "tool:args-hash").
|
|
185
|
+
* The key is hashed to produce the cache entry hash.
|
|
186
|
+
*/
|
|
187
|
+
lookupByKey(key, source) {
|
|
188
|
+
return this.lookup(key, source);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Store a result under an explicit key.
|
|
192
|
+
* The key is hashed for addressing; the value is stored as content.
|
|
193
|
+
*/
|
|
194
|
+
putByKey(key, value, source, metadata) {
|
|
195
|
+
const hash = hashContent(key);
|
|
196
|
+
const tokenCount = estimateTokens(value);
|
|
197
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
198
|
+
const src = source ?? "";
|
|
199
|
+
const meta = metadata ? JSON.stringify(metadata) : null;
|
|
200
|
+
const existing = this.db.prepare("SELECT hash FROM content_cache WHERE hash = ?").get(hash);
|
|
201
|
+
if (existing) {
|
|
202
|
+
this.db.prepare(
|
|
203
|
+
"UPDATE content_cache SET content = ?, token_count = ?, hit_count = hit_count + 1, last_seen = ?, source = ?, metadata = ? WHERE hash = ?"
|
|
204
|
+
).run(value, tokenCount, now, src, meta, hash);
|
|
205
|
+
} else {
|
|
206
|
+
this.db.prepare(
|
|
207
|
+
`INSERT INTO content_cache (hash, content, token_count, hit_count, first_seen, last_seen, source, metadata)
|
|
208
|
+
VALUES (?, ?, ?, 0, ?, ?, ?, ?)`
|
|
209
|
+
).run(hash, value, tokenCount, now, now, src, meta);
|
|
210
|
+
this.db.prepare(`INSERT INTO content_cache_fts (content, hash) VALUES (?, ?)`).run(value, hash);
|
|
211
|
+
}
|
|
212
|
+
return this.getEntry(hash);
|
|
213
|
+
}
|
|
214
|
+
// ------------------------------------------------------------------
|
|
215
|
+
// Helpers
|
|
216
|
+
// ------------------------------------------------------------------
|
|
217
|
+
rowToEntry(row) {
|
|
218
|
+
return {
|
|
219
|
+
hash: row.hash,
|
|
220
|
+
content: row.content,
|
|
221
|
+
tokenCount: row.token_count,
|
|
222
|
+
hitCount: row.hit_count,
|
|
223
|
+
firstSeen: row.first_seen,
|
|
224
|
+
lastSeen: row.last_seen,
|
|
225
|
+
source: row.source,
|
|
226
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
sanitizeFtsQuery(query) {
|
|
230
|
+
const cleaned = query.replace(/['"()*~^{}\[\]]/g, "");
|
|
231
|
+
const terms = cleaned.split(/\s+/).filter((t) => t && !/^(AND|OR|NOT|NEAR)$/i.test(t));
|
|
232
|
+
if (terms.length === 0) return '""';
|
|
233
|
+
return terms.map((t) => `"${t}"`).join(" ");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
export {
|
|
237
|
+
ContentCache
|
|
238
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
import { ContentCache } from "./content-cache.js";
|
|
6
|
+
import { estimateTokens, hashContent } from "./token-estimator.js";
|
|
7
|
+
export {
|
|
8
|
+
ContentCache,
|
|
9
|
+
estimateTokens,
|
|
10
|
+
hashContent
|
|
11
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
import { createHash } from "crypto";
|
|
6
|
+
function estimateTokens(content) {
|
|
7
|
+
if (!content) return 0;
|
|
8
|
+
return Math.ceil(content.length / 4);
|
|
9
|
+
}
|
|
10
|
+
function hashContent(content) {
|
|
11
|
+
return createHash("sha256").update(content).digest("hex");
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
estimateTokens,
|
|
15
|
+
hashContent
|
|
16
|
+
};
|
|
@@ -97,6 +97,26 @@ class FrameDatabase {
|
|
|
97
97
|
);
|
|
98
98
|
} catch {
|
|
99
99
|
}
|
|
100
|
+
const provenanceCols = [
|
|
101
|
+
{ table: "frames", col: "prov_source", def: "TEXT DEFAULT ''" },
|
|
102
|
+
{ table: "frames", col: "prov_derivation", def: "TEXT DEFAULT '[]'" },
|
|
103
|
+
{ table: "frames", col: "prov_confidence", def: "REAL DEFAULT 1.0" },
|
|
104
|
+
{ table: "frames", col: "prov_superseded_by", def: "TEXT" },
|
|
105
|
+
{
|
|
106
|
+
table: "frames",
|
|
107
|
+
col: "prov_program_version",
|
|
108
|
+
def: "TEXT DEFAULT ''"
|
|
109
|
+
},
|
|
110
|
+
{ table: "anchors", col: "prov_source", def: "TEXT DEFAULT ''" },
|
|
111
|
+
{ table: "anchors", col: "prov_confidence", def: "REAL DEFAULT 1.0" },
|
|
112
|
+
{ table: "anchors", col: "prov_superseded_by", def: "TEXT" }
|
|
113
|
+
];
|
|
114
|
+
for (const { table, col, def } of provenanceCols) {
|
|
115
|
+
try {
|
|
116
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${col} ${def}`);
|
|
117
|
+
} catch {
|
|
118
|
+
}
|
|
119
|
+
}
|
|
100
120
|
this.db.exec(`
|
|
101
121
|
CREATE INDEX IF NOT EXISTS idx_frames_run ON frames(run_id);
|
|
102
122
|
CREATE INDEX IF NOT EXISTS idx_frames_project ON frames(project_id);
|
|
@@ -188,30 +208,6 @@ class FrameDatabase {
|
|
|
188
208
|
CREATE INDEX IF NOT EXISTS idx_entity_temporal
|
|
189
209
|
ON entity_states(entity_name, valid_from DESC);
|
|
190
210
|
`);
|
|
191
|
-
this.db.exec(`
|
|
192
|
-
CREATE TABLE IF NOT EXISTS cord_tasks (
|
|
193
|
-
task_id TEXT PRIMARY KEY,
|
|
194
|
-
parent_id TEXT,
|
|
195
|
-
project_id TEXT NOT NULL,
|
|
196
|
-
run_id TEXT NOT NULL,
|
|
197
|
-
goal TEXT NOT NULL,
|
|
198
|
-
prompt TEXT NOT NULL DEFAULT '',
|
|
199
|
-
result TEXT,
|
|
200
|
-
status TEXT NOT NULL DEFAULT 'pending'
|
|
201
|
-
CHECK (status IN ('pending','active','completed','blocked','asked')),
|
|
202
|
-
context_mode TEXT NOT NULL DEFAULT 'spawn'
|
|
203
|
-
CHECK (context_mode IN ('spawn','fork','ask')),
|
|
204
|
-
blocked_by TEXT NOT NULL DEFAULT '[]',
|
|
205
|
-
depth INTEGER NOT NULL DEFAULT 0,
|
|
206
|
-
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
207
|
-
completed_at INTEGER,
|
|
208
|
-
FOREIGN KEY (parent_id) REFERENCES cord_tasks(task_id)
|
|
209
|
-
);
|
|
210
|
-
CREATE INDEX IF NOT EXISTS idx_cord_tasks_project ON cord_tasks(project_id);
|
|
211
|
-
CREATE INDEX IF NOT EXISTS idx_cord_tasks_parent ON cord_tasks(parent_id);
|
|
212
|
-
CREATE INDEX IF NOT EXISTS idx_cord_tasks_status ON cord_tasks(status);
|
|
213
|
-
CREATE INDEX IF NOT EXISTS idx_cord_tasks_project_status ON cord_tasks(project_id, status);
|
|
214
|
-
`);
|
|
215
211
|
logger.info("Frame database schema initialized");
|
|
216
212
|
} catch (error) {
|
|
217
213
|
throw new DatabaseError(
|
|
@@ -228,8 +224,8 @@ class FrameDatabase {
|
|
|
228
224
|
insertFrame(frame) {
|
|
229
225
|
try {
|
|
230
226
|
const stmt = this.db.prepare(`
|
|
231
|
-
INSERT INTO frames (frame_id, run_id, project_id, parent_frame_id, depth, type, name, state, inputs, outputs, digest_json)
|
|
232
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
227
|
+
INSERT INTO frames (frame_id, run_id, project_id, parent_frame_id, depth, type, name, state, inputs, outputs, digest_json, prov_source, prov_derivation, prov_confidence, prov_program_version)
|
|
228
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
233
229
|
`);
|
|
234
230
|
const result = stmt.run(
|
|
235
231
|
frame.frame_id,
|
|
@@ -242,7 +238,15 @@ class FrameDatabase {
|
|
|
242
238
|
frame.state,
|
|
243
239
|
JSON.stringify(frame.inputs),
|
|
244
240
|
JSON.stringify(frame.outputs),
|
|
245
|
-
JSON.stringify(frame.digest_json)
|
|
241
|
+
JSON.stringify(frame.digest_json),
|
|
242
|
+
frame.type,
|
|
243
|
+
// prov_source: frame type as source
|
|
244
|
+
JSON.stringify([`frame:${frame.type}`]),
|
|
245
|
+
// prov_derivation
|
|
246
|
+
1,
|
|
247
|
+
// prov_confidence: default full confidence
|
|
248
|
+
process.env["npm_package_version"] || ""
|
|
249
|
+
// prov_program_version
|
|
246
250
|
);
|
|
247
251
|
if (result.changes === 0) {
|
|
248
252
|
throw new DatabaseError(
|
|
@@ -473,8 +477,8 @@ class FrameDatabase {
|
|
|
473
477
|
insertAnchor(anchor) {
|
|
474
478
|
try {
|
|
475
479
|
const stmt = this.db.prepare(`
|
|
476
|
-
INSERT INTO anchors (anchor_id, frame_id, project_id, type, text, priority, metadata)
|
|
477
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
480
|
+
INSERT INTO anchors (anchor_id, frame_id, project_id, type, text, priority, metadata, prov_source, prov_confidence)
|
|
481
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
478
482
|
`);
|
|
479
483
|
const result = stmt.run(
|
|
480
484
|
anchor.anchor_id,
|
|
@@ -483,7 +487,11 @@ class FrameDatabase {
|
|
|
483
487
|
anchor.type,
|
|
484
488
|
anchor.text,
|
|
485
489
|
anchor.priority,
|
|
486
|
-
JSON.stringify(anchor.metadata)
|
|
490
|
+
JSON.stringify(anchor.metadata),
|
|
491
|
+
anchor.type,
|
|
492
|
+
// prov_source: anchor type (DECISION, EVENT, etc.)
|
|
493
|
+
1
|
|
494
|
+
// prov_confidence: default full confidence
|
|
487
495
|
);
|
|
488
496
|
if (result.changes === 0) {
|
|
489
497
|
throw new DatabaseError(
|