@stackmemoryai/stackmemory 1.10.4 → 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.
Files changed (95) hide show
  1. package/README.md +104 -23
  2. package/dist/src/cli/claude-sm.js +266 -84
  3. package/dist/src/cli/codex-sm.js +185 -33
  4. package/dist/src/cli/commands/bench.js +209 -2
  5. package/dist/src/cli/commands/cache.js +126 -0
  6. package/dist/src/cli/commands/daemon.js +41 -0
  7. package/dist/src/cli/commands/handoff.js +40 -9
  8. package/dist/src/cli/commands/onboard.js +70 -3
  9. package/dist/src/cli/commands/optimize.js +117 -0
  10. package/dist/src/cli/commands/orchestrate.js +230 -5
  11. package/dist/src/cli/commands/orchestrator.js +312 -24
  12. package/dist/src/cli/commands/pack.js +322 -0
  13. package/dist/src/cli/commands/search.js +40 -1
  14. package/dist/src/cli/commands/setup.js +177 -7
  15. package/dist/src/cli/commands/skills.js +10 -1
  16. package/dist/src/cli/commands/state.js +265 -0
  17. package/dist/src/cli/commands/wiki.js +33 -0
  18. package/dist/src/cli/gemini-sm.js +19 -29
  19. package/dist/src/cli/index.js +90 -29
  20. package/dist/src/cli/opencode-sm.js +38 -21
  21. package/dist/src/cli/utils/determinism-watcher.js +66 -0
  22. package/dist/src/cli/utils/real-cli-bin.js +44 -0
  23. package/dist/src/core/cache/content-cache.js +238 -0
  24. package/dist/src/core/cache/index.js +11 -0
  25. package/dist/src/core/cache/token-estimator.js +16 -0
  26. package/dist/src/core/context/frame-database.js +38 -30
  27. package/dist/src/core/cross-search/cross-project-search.js +269 -0
  28. package/dist/src/core/{merge → cross-search}/index.js +6 -4
  29. package/dist/src/core/database/sqlite-adapter.js +0 -83
  30. package/dist/src/core/extensions/provider-adapter.js +5 -0
  31. package/dist/src/core/models/model-router.js +22 -2
  32. package/dist/src/core/monitoring/logger.js +2 -1
  33. package/dist/src/core/optimization/trace-optimizer.js +413 -0
  34. package/dist/src/core/provenance/confidence-scorer.js +128 -0
  35. package/dist/src/core/provenance/index.js +40 -0
  36. package/dist/src/core/provenance/provenance-store.js +194 -0
  37. package/dist/src/core/provenance/types.js +82 -0
  38. package/dist/src/core/session/project-handoff.js +64 -0
  39. package/dist/src/core/session/session-manager.js +28 -0
  40. package/dist/src/core/shared-state/canonical-store.js +564 -0
  41. package/dist/src/core/skill-packs/index.js +18 -0
  42. package/dist/src/core/skill-packs/parser.js +42 -0
  43. package/dist/src/core/skill-packs/registry.js +224 -0
  44. package/dist/src/core/skill-packs/types.js +66 -0
  45. package/dist/src/core/trace/trace-event-store.js +282 -0
  46. package/dist/src/core/trace/trace-event.js +4 -0
  47. package/dist/src/core/wiki/wiki-compiler.js +219 -0
  48. package/dist/src/daemon/daemon-config.js +7 -0
  49. package/dist/src/daemon/services/github-service.js +126 -0
  50. package/dist/src/daemon/unified-daemon.js +30 -0
  51. package/dist/src/features/sweep/pty-wrapper.js +13 -5
  52. package/dist/src/hooks/schemas.js +2 -0
  53. package/dist/src/integrations/claude-code/subagent-client.js +89 -0
  54. package/dist/src/integrations/github/pr-state.js +158 -0
  55. package/dist/src/integrations/linear/client.js +4 -1
  56. package/dist/src/integrations/mcp/handlers/cross-search-handlers.js +188 -0
  57. package/dist/src/integrations/mcp/handlers/index.js +40 -59
  58. package/dist/src/integrations/mcp/server.js +425 -311
  59. package/dist/src/integrations/mcp/tool-alias-registry.js +370 -0
  60. package/dist/src/integrations/mcp/tool-definitions.js +98 -229
  61. package/dist/src/integrations/ralph/context/stackmemory-context-loader.js +3 -40
  62. package/dist/src/integrations/ralph/learning/pattern-learner.js +1 -20
  63. package/dist/src/integrations/ralph/swarm/swarm-coordinator.js +0 -2
  64. package/dist/src/mcp/stackmemory-mcp-server.js +315 -0
  65. package/dist/src/orchestrators/multimodal/determinism.js +243 -0
  66. package/dist/src/orchestrators/multimodal/harness.js +147 -77
  67. package/dist/src/orchestrators/multimodal/providers.js +44 -3
  68. package/dist/src/utils/hook-installer.js +8 -8
  69. package/package.json +10 -1
  70. package/packs/coding/python-fastapi/instructions.md +60 -0
  71. package/packs/coding/python-fastapi/pack.yaml +28 -0
  72. package/packs/coding/typescript-react/instructions.md +47 -0
  73. package/packs/coding/typescript-react/pack.yaml +28 -0
  74. package/packs/core/commands/capture.md +32 -0
  75. package/packs/core/commands/learn.md +73 -0
  76. package/packs/core/commands/next.md +36 -0
  77. package/packs/core/commands/restart.md +58 -0
  78. package/packs/core/commands/restore.md +29 -0
  79. package/packs/core/commands/start.md +57 -0
  80. package/packs/core/commands/stop.md +65 -0
  81. package/packs/core/commands/summary.md +40 -0
  82. package/packs/core/manifest.json +24 -0
  83. package/packs/ops/decision-recovery/instructions.md +65 -0
  84. package/packs/ops/decision-recovery/pack.yaml +89 -0
  85. package/templates/claude-hooks/doc-ingest.js +76 -0
  86. package/dist/src/cli/commands/team.js +0 -168
  87. package/dist/src/core/context/shared-context-layer.js +0 -620
  88. package/dist/src/core/context/stack-merge-resolver.js +0 -748
  89. package/dist/src/core/merge/conflict-detector.js +0 -430
  90. package/dist/src/core/merge/resolution-engine.js +0 -557
  91. package/dist/src/core/merge/stack-diff.js +0 -531
  92. package/dist/src/core/merge/unified-merge-resolver.js +0 -302
  93. package/dist/src/integrations/mcp/handlers/cord-handlers.js +0 -397
  94. package/dist/src/integrations/mcp/handlers/team-handlers.js +0 -211
  95. /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
- if (this.config.opencodeBin && this.config.opencodeBin.trim()) {
131
- return this.config.opencodeBin.trim();
132
- }
133
- const envBin = process.env["OPENCODE_BIN"];
134
- if (envBin && envBin.trim()) return envBin.trim();
135
- const possiblePaths = [
136
- path.join(os.homedir(), ".opencode", "bin", "opencode"),
137
- "/usr/local/bin/opencode",
138
- "/opt/homebrew/bin/opencode"
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(