@massu/core 1.15.3 → 1.15.5

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.
@@ -0,0 +1,3889 @@
1
+ #!/usr/bin/env node
2
+ import{createRequire as __cr}from"module";const require=__cr(import.meta.url);
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __esm = (fn, res, err) => function __init() {
8
+ if (err) throw err[0];
9
+ try {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ } catch (e) {
12
+ throw err = [e], e;
13
+ }
14
+ };
15
+ var __export = (target, all) => {
16
+ for (var name in all)
17
+ __defProp(target, name, { get: all[name], enumerable: true });
18
+ };
19
+ var __copyProps = (to, from, except, desc) => {
20
+ if (from && typeof from === "object" || typeof from === "function") {
21
+ for (let key of __getOwnPropNames(from))
22
+ if (!__hasOwnProp.call(to, key) && key !== except)
23
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
24
+ }
25
+ return to;
26
+ };
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+
29
+ // src/lib/memory-path.ts
30
+ function encodeMemoryDirName(projectRoot) {
31
+ return projectRoot.replace(/\//g, "-");
32
+ }
33
+ var init_memory_path = __esm({
34
+ "src/lib/memory-path.ts"() {
35
+ "use strict";
36
+ }
37
+ });
38
+
39
+ // src/credentials.ts
40
+ import { homedir } from "os";
41
+ import { resolve } from "path";
42
+ import {
43
+ existsSync,
44
+ readFileSync,
45
+ writeFileSync,
46
+ mkdirSync,
47
+ rmSync,
48
+ chmodSync
49
+ } from "fs";
50
+ function credentialsDir(home = homedir()) {
51
+ return resolve(home, ".massu");
52
+ }
53
+ function credentialsPath(home = homedir()) {
54
+ return resolve(credentialsDir(home), "credentials");
55
+ }
56
+ function readUserCredentials(home = homedir()) {
57
+ try {
58
+ const p = credentialsPath(home);
59
+ if (!existsSync(p)) return void 0;
60
+ const parsed = JSON.parse(readFileSync(p, "utf-8"));
61
+ const key = typeof parsed?.apiKey === "string" ? parsed.apiKey.trim() : "";
62
+ return key.length > 0 ? key : void 0;
63
+ } catch {
64
+ return void 0;
65
+ }
66
+ }
67
+ function isUnresolvedLiteral(v) {
68
+ return /^\$\{[^}]+\}$/.test(v);
69
+ }
70
+ function resolveApiKey(opts = {}) {
71
+ const env = opts.env ?? process.env;
72
+ const home = opts.home ?? homedir();
73
+ const cfg = typeof opts.configApiKey === "string" ? opts.configApiKey.trim() : "";
74
+ if (cfg.length > 0 && !isUnresolvedLiteral(cfg)) {
75
+ return { apiKey: cfg, source: "config" };
76
+ }
77
+ const envRaw = env[MASSU_ENV_API_KEY];
78
+ const envKey = typeof envRaw === "string" ? envRaw.trim() : "";
79
+ if (envKey.length > 0) {
80
+ return { apiKey: envKey, source: "env" };
81
+ }
82
+ const fileKey = readUserCredentials(home);
83
+ if (fileKey) {
84
+ return { apiKey: fileKey, source: "user-file" };
85
+ }
86
+ return { source: "none" };
87
+ }
88
+ function resolveEndpoint(opts = {}) {
89
+ const env = opts.env ?? process.env;
90
+ const cfg = typeof opts.configEndpoint === "string" ? opts.configEndpoint.trim() : "";
91
+ if (cfg.length > 0) return cfg;
92
+ const envRaw = env[MASSU_ENV_CLOUD_ENDPOINT];
93
+ const envEp = typeof envRaw === "string" ? envRaw.trim() : "";
94
+ if (envEp.length > 0) return envEp;
95
+ return DEFAULT_CLOUD_ENDPOINT;
96
+ }
97
+ var MASSU_ENV_API_KEY, MASSU_ENV_CLOUD_ENDPOINT, DEFAULT_CLOUD_ENDPOINT;
98
+ var init_credentials = __esm({
99
+ "src/credentials.ts"() {
100
+ "use strict";
101
+ MASSU_ENV_API_KEY = "MASSU_API_KEY";
102
+ MASSU_ENV_CLOUD_ENDPOINT = "MASSU_CLOUD_ENDPOINT";
103
+ DEFAULT_CLOUD_ENDPOINT = "https://api.massu.ai/v1";
104
+ }
105
+ });
106
+
107
+ // src/config-memory-schema.ts
108
+ import { z } from "zod";
109
+ var MemoryConfigSchema;
110
+ var init_config_memory_schema = __esm({
111
+ "src/config-memory-schema.ts"() {
112
+ "use strict";
113
+ MemoryConfigSchema = z.object({
114
+ recall: z.object({
115
+ enabled: z.boolean().default(true),
116
+ maxTokens: z.number().int().positive().default(1200),
117
+ sources: z.array(
118
+ z.enum([
119
+ "observation",
120
+ "architecture_decision",
121
+ "knowledge_chunk",
122
+ "failure_class"
123
+ ])
124
+ ).default(["observation", "architecture_decision", "knowledge_chunk", "failure_class"]),
125
+ timeoutMs: z.number().int().positive().default(8e3),
126
+ limit: z.number().int().positive().default(8),
127
+ minScore: z.number().min(0).default(0),
128
+ // --- Semantic embedder (plan-living-memory-slice-2a-embedder, P3-001) ---
129
+ // embedEnabled: Tier-1 bundled WASM embedder on by default; false forces
130
+ // Tier-2 (FTS keyword-only) recall.
131
+ // embedEndpoint: optional Tier-0 OpenAI-compatible /v1/embeddings provider
132
+ // (Ollama, LM Studio, vLLM, or any hosted API). When set, embedding egresses ONLY to this
133
+ // operator-chosen endpoint; unset means zero egress (bundled model).
134
+ // embedModel: optional Tier-0 model name sent to that endpoint.
135
+ embedEnabled: z.boolean().default(true),
136
+ embedEndpoint: z.string().url().refine((s) => /^https?:\/\//i.test(s), {
137
+ message: "memory.recall.embedEndpoint must be an http(s) URL"
138
+ }).optional(),
139
+ embedModel: z.string().min(1).optional()
140
+ }).default({}),
141
+ // --- Contradiction / supersede gate (plan-living-memory-slice-2-temporal-model, P5-001) ---
142
+ // When a new high-value memory (decision/correction) is written, find
143
+ // semantically-related existing records and, if the new one contradicts an
144
+ // old one, supersede-don't-delete the old row. Fully fail-open + gated to a
145
+ // small set of high-value types so the hot capture path is untouched.
146
+ contradiction: z.object({
147
+ // Master switch. false → every write is a plain insert (prior behavior).
148
+ enabled: z.boolean().default(true),
149
+ // Optional Tier-0 external judge (OpenAI-compatible). null/unset → local
150
+ // heuristic only, ZERO egress. When set, candidate + related records egress
151
+ // ONLY to this operator-chosen endpoint; any error falls back to heuristic.
152
+ judgeEndpoint: z.string().url().refine((s) => /^https?:\/\//i.test(s), {
153
+ message: "memory.contradiction.judgeEndpoint must be an http(s) URL"
154
+ }).optional(),
155
+ // Cosine similarity above which a correction-flavored new record is treated
156
+ // as superseding a related existing record. Calibrated to all-MiniLM-L6-v2
157
+ // (same-topic contradictions ≈0.65–0.86; related-but-complementary ≈0.47).
158
+ similarityThreshold: z.number().min(0).max(1).default(0.6),
159
+ // Cosine similarity above which a new record is a near-duplicate (NOOP).
160
+ dedupThreshold: z.number().min(0).max(1).default(0.93),
161
+ // Observation types the gate runs for. High-volume 'file_change' is
162
+ // deliberately excluded so the hot path never triggers a hybridSearch.
163
+ gatedTypes: z.array(z.string()).default(["decision", "cr_violation", "failed_attempt"]),
164
+ // When true, superseded rows may still surface in recall with a
165
+ // "(superseded on <date> by #<id>)" annotation instead of being excluded.
166
+ annotateSuperseded: z.boolean().default(false),
167
+ // Time budget (ms) for the contradiction check; exceeded → fail-open (ADD).
168
+ budgetMs: z.number().int().positive().default(800)
169
+ }).default({}),
170
+ // --- Background consolidation, the "sleep-time" pass
171
+ // (plan-living-memory-slice-3-consolidation) ---
172
+ // Keeps memory sharp over years: dedupes, distills dying sessions into
173
+ // durable lessons, spots corrections you keep repeating, reweights by what
174
+ // actually gets used, and retires dead weight by EXPIRING it (never deleting).
175
+ //
176
+ // Runs with ZERO LLM and ZERO network by default — every stage is arithmetic
177
+ // plus the embedding model Massu already bundles. `llmEndpoint` is OPTIONAL
178
+ // and upgrades the prose of session summaries ONLY.
179
+ consolidation: z.object({
180
+ enabled: z.boolean().default(true),
181
+ // Bounded sweep at session end — the automatic path (no scheduler needed).
182
+ sessionSweepEnabled: z.boolean().default(true),
183
+ // OPTIONAL local/remote OpenAI-compatible chat endpoint. UNSET = zero egress.
184
+ // When set, session text egresses ONLY to this endpoint you chose.
185
+ // NOTE: the API key is NEVER configured here — it is read exclusively from
186
+ // the MASSU_MEMORY_LLM_API_KEY env var, so a key can never be committed.
187
+ llmEndpoint: z.string().url().refine((s) => /^https?:\/\//i.test(s), {
188
+ message: "memory.consolidation.llmEndpoint must be an http(s) URL"
189
+ }).optional(),
190
+ // Model NAME/ALIAS sent to llmEndpoint (e.g. "llama3.1:8b").
191
+ llmModel: z.string().optional(),
192
+ // Distill a session once its newest turn is older than this. Must stay
193
+ // INSIDE the 7-day conversation_turns prune window or the raw material is
194
+ // destroyed before it is ever summarized.
195
+ summarizeAfterDays: z.number().int().positive().default(5),
196
+ // Age past which an unprotected, never-retrieved, low-importance row may expire.
197
+ retentionDays: z.number().int().positive().default(90),
198
+ importanceFloor: z.number().int().min(1).max(5).default(2),
199
+ // Types that may NEVER expire, however old.
200
+ protectedTypes: z.array(z.string()).default(["decision", "cr_violation", "incident_near_miss"]),
201
+ // Days the retrieval counter must observe usage BEFORE any expiry is armed.
202
+ // The cold-start guard: on a fresh counter nothing has "ever been
203
+ // retrieved", so without this the first pass would gut the store.
204
+ usageWarmupDays: z.number().int().nonnegative().default(30),
205
+ // Per-pass decay on the windowed hit count, so usefulness must be sustained.
206
+ usageDecay: z.number().min(0).max(1).default(0.9),
207
+ // A record is reweighted at most once per this many days (idempotency).
208
+ reweightIntervalDays: z.number().int().positive().default(1),
209
+ // Recurrences (across >= 2 sessions) before a rule candidate is proposed.
210
+ promoteMinOccurrences: z.number().int().positive().default(3),
211
+ budgetMs: z.number().int().positive().default(3e3),
212
+ // Surface optional upgrades (e.g. "a local model was detected") in chat.
213
+ suggestUpgrades: z.boolean().default(true),
214
+ suggestIntervalDays: z.number().int().positive().default(30)
215
+ }).default({}),
216
+ // A-20 (Slice 4) — memory FILES: the mirror, and the one switch that decides whether
217
+ // Massu may ever WRITE into the user's memory directory.
218
+ //
219
+ // ⛔ `renderEnabled` DEFAULTS TO FALSE. 4B is the first capability in Massu's history
220
+ // that writes files into the place the user keeps their own hand-written prose. A new
221
+ // write capability that arrives switched-on in an `npm update` is a capability nobody
222
+ // consented to. The path is: the advisor OFFERS it in chat -> the user runs
223
+ // `massu memory render --dry-run` and sees exactly what WOULD be written -> the user
224
+ // turns it on. Three deliberate steps, none implicit. A drift-guard pins this default.
225
+ files: z.object({
226
+ // The lossless file<->store mirror (ingest side). Read-only; it writes nothing.
227
+ enabled: z.boolean().default(true),
228
+ // The ONLY flag here that grants a WRITE. Never auto-enable.
229
+ renderEnabled: z.boolean().default(false),
230
+ // Anti-spam: files Massu may render in ONE session.
231
+ renderMaxFilesPerSession: z.number().int().min(0).default(3),
232
+ // Only memories at/above this importance are worth a durable file.
233
+ renderMinImportance: z.number().int().min(1).max(5).default(4),
234
+ // The clearly-labelled MEMORY.md section Massu's pointers live under.
235
+ indexSection: z.string().default("Learned by Massu"),
236
+ // Hard bound on the managed MEMORY.md region. MEMORY.md is auto-loaded into EVERY
237
+ // turn of EVERY session, so an unbounded index is a permanent context tax — and the
238
+ // per-session cap bounds only the RATE, never the total.
239
+ indexMaxLines: z.number().int().min(1).default(50)
240
+ }).default({})
241
+ }).optional();
242
+ }
243
+ });
244
+
245
+ // src/config.ts
246
+ import { resolve as resolve2, dirname } from "path";
247
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
248
+ import { homedir as homedir2 } from "os";
249
+ import { parse as parseYaml } from "yaml";
250
+ import { z as z2 } from "zod";
251
+ function findProjectRoot() {
252
+ const cwd = process.cwd();
253
+ let dir = cwd;
254
+ while (true) {
255
+ if (existsSync2(resolve2(dir, "massu.config.yaml"))) {
256
+ return dir;
257
+ }
258
+ const parent = dirname(dir);
259
+ if (parent === dir) break;
260
+ dir = parent;
261
+ }
262
+ dir = cwd;
263
+ while (true) {
264
+ if (existsSync2(resolve2(dir, "package.json"))) {
265
+ return dir;
266
+ }
267
+ if (existsSync2(resolve2(dir, ".git"))) {
268
+ return dir;
269
+ }
270
+ const parent = dirname(dir);
271
+ if (parent === dir) break;
272
+ dir = parent;
273
+ }
274
+ return cwd;
275
+ }
276
+ function getProjectRoot() {
277
+ if (!_projectRoot) {
278
+ _projectRoot = findProjectRoot();
279
+ }
280
+ return _projectRoot;
281
+ }
282
+ function getConfig() {
283
+ if (_config) return _config;
284
+ const root = getProjectRoot();
285
+ const configPath = resolve2(root, "massu.config.yaml");
286
+ let rawYaml = {};
287
+ if (existsSync2(configPath)) {
288
+ const content = readFileSync2(configPath, "utf-8");
289
+ rawYaml = parseYaml(content) ?? {};
290
+ }
291
+ const result = RawConfigSchema.safeParse(rawYaml);
292
+ if (!result.success) {
293
+ const issues = result.error.issues.map((i) => {
294
+ const path = i.path.length > 0 ? i.path.join(".") : "(root)";
295
+ const received = "received" in i && i.received !== void 0 ? ` (received ${JSON.stringify(i.received)})` : "";
296
+ return ` - ${path}: ${i.message}${received}`;
297
+ }).join("\n");
298
+ throw new Error(
299
+ `Invalid massu.config.yaml at ${configPath}:
300
+ ${issues}
301
+ Hint: run \`massu config refresh\` to regenerate a valid config or fix the listed fields manually.`
302
+ );
303
+ }
304
+ const parsed = result.data;
305
+ const projectRoot = parsed.project.root === "auto" || !parsed.project.root ? root : resolve2(root, parsed.project.root);
306
+ const fw = parsed.framework;
307
+ let router = fw.router;
308
+ let orm = fw.orm;
309
+ let ui = fw.ui;
310
+ if (fw.type === "multi" && fw.primary && fw.languages) {
311
+ const primaryEntry = fw.languages[fw.primary];
312
+ if (primaryEntry) {
313
+ if (router === "none" && primaryEntry.router) router = primaryEntry.router;
314
+ if (orm === "none" && primaryEntry.orm) orm = primaryEntry.orm;
315
+ if (ui === "none" && primaryEntry.ui) ui = primaryEntry.ui;
316
+ }
317
+ }
318
+ _config = {
319
+ schema_version: parsed.schema_version,
320
+ project: {
321
+ name: parsed.project.name,
322
+ root: projectRoot
323
+ },
324
+ // Spread `fw` first so zod-`.passthrough()` extras (e.g., `framework.swift`,
325
+ // `framework.python`) survive into the consumer-visible Config. Then override
326
+ // the v2-backcompat-mirrored router/orm/ui values. Without the spread, the
327
+ // variant-resolution `pickVariant` (install-commands.ts) cannot see the
328
+ // top-level passthrough language blocks.
329
+ framework: {
330
+ ...fw,
331
+ router,
332
+ orm,
333
+ ui
334
+ },
335
+ paths: parsed.paths,
336
+ toolPrefix: parsed.toolPrefix,
337
+ dbAccessPattern: parsed.dbAccessPattern,
338
+ knownMismatches: parsed.knownMismatches,
339
+ accessScopes: parsed.accessScopes,
340
+ domains: parsed.domains,
341
+ rules: parsed.rules,
342
+ // P-M-036: customer-authored CR-style governance rules.
343
+ governance_rules: parsed.governance_rules,
344
+ analytics: parsed.analytics,
345
+ governance: parsed.governance,
346
+ security: parsed.security,
347
+ team: parsed.team,
348
+ regression: parsed.regression,
349
+ cloud: parsed.cloud,
350
+ memory: parsed.memory,
351
+ conventions: parsed.conventions,
352
+ autoLearning: parsed.autoLearning,
353
+ python: parsed.python,
354
+ verification: parsed.verification,
355
+ canonical_paths: parsed.canonical_paths,
356
+ verification_types: parsed.verification_types,
357
+ detection: parsed.detection,
358
+ detected: parsed.detected,
359
+ watch: parsed.watch,
360
+ adapters: parsed.adapters,
361
+ telemetry: parsed.telemetry,
362
+ lsp: parsed.lsp
363
+ };
364
+ const resolvedKey = resolveApiKey({ configApiKey: _config.cloud?.apiKey });
365
+ _resolvedApiKeySource = resolvedKey.source;
366
+ const resolvedEndpoint = resolveEndpoint({ configEndpoint: _config.cloud?.endpoint });
367
+ if (resolvedKey.apiKey) {
368
+ _config.cloud = {
369
+ enabled: true,
370
+ sync: { memory: true, analytics: true, audit: true },
371
+ ..._config.cloud,
372
+ apiKey: resolvedKey.apiKey,
373
+ endpoint: resolvedEndpoint
374
+ };
375
+ } else if (_config.cloud) {
376
+ _config.cloud = { ..._config.cloud, endpoint: _config.cloud.endpoint ?? resolvedEndpoint };
377
+ }
378
+ return _config;
379
+ }
380
+ function getResolvedPaths() {
381
+ const config = getConfig();
382
+ const root = getProjectRoot();
383
+ const claudeDirName = config.conventions?.claudeDirName ?? ".claude";
384
+ return {
385
+ codegraphDbPath: resolve2(root, ".codegraph/codegraph.db"),
386
+ dataDbPath: resolve2(root, ".massu/data.db"),
387
+ prismaSchemaPath: resolve2(root, config.paths.schema ?? "prisma/schema.prisma"),
388
+ rootRouterPath: resolve2(root, config.paths.routerRoot ?? "src/server/api/root.ts"),
389
+ routersDir: resolve2(root, config.paths.routers ?? "src/server/api/routers"),
390
+ srcDir: resolve2(root, config.paths.source),
391
+ pathAlias: Object.fromEntries(
392
+ Object.entries(config.paths.aliases).map(([alias, target]) => [
393
+ alias,
394
+ resolve2(root, target)
395
+ ])
396
+ ),
397
+ extensions: [".ts", ".tsx", ".js", ".jsx"],
398
+ indexFiles: ["index.ts", "index.tsx", "index.js", "index.jsx"],
399
+ patternsDir: resolve2(root, claudeDirName, "patterns"),
400
+ claudeMdPath: resolve2(root, claudeDirName, "CLAUDE.md"),
401
+ docsMapPath: resolve2(root, ".massu/docs-map.json"),
402
+ helpSitePath: resolve2(root, "../" + config.project.name + "-help"),
403
+ memoryDbPath: resolve2(root, ".massu/memory.db"),
404
+ knowledgeDbPath: resolve2(root, ".massu/knowledge.db"),
405
+ plansDir: resolve2(root, "docs/plans"),
406
+ docsDir: resolve2(root, "docs"),
407
+ claudeDir: resolve2(root, claudeDirName),
408
+ memoryDir: resolve2(homedir2(), claudeDirName, "projects", encodeMemoryDirName(root), "memory"),
409
+ sessionStatePath: resolve2(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
410
+ sessionArchivePath: resolve2(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
411
+ mcpJsonPath: resolve2(root, ".mcp.json"),
412
+ settingsLocalPath: resolve2(root, claudeDirName, "settings.local.json")
413
+ };
414
+ }
415
+ var DomainConfigSchema, PatternRuleConfigSchema, CostModelSchema, AnalyticsConfigSchema, CustomPatternSchema, GovernanceConfigSchema, SecurityPatternSchema, SecurityConfigSchema, TeamConfigSchema, RegressionConfigSchema, AutoLearningConfigSchema, CloudConfigSchema, ConventionsConfigSchema, PythonDomainConfigSchema, PythonConfigSchema, PathsConfigSchema, LanguageFrameworkEntrySchema, FrameworkConfigSchema, DetectedConfigSchema, VerificationEntrySchema, VerificationConfigSchema, CanonicalPathsSchema, VerificationTypesSchema, DetectionRuleEntrySchema, DetectionConfigSchema, WatchConfigSchema, AdapterLocalPathSchema, AdaptersConfigSchema, TelemetryConfigSchema, LSPConfigSchema, RawConfigSchema, _config, _resolvedApiKeySource, _projectRoot;
416
+ var init_config = __esm({
417
+ "src/config.ts"() {
418
+ "use strict";
419
+ init_memory_path();
420
+ init_credentials();
421
+ init_config_memory_schema();
422
+ DomainConfigSchema = z2.object({
423
+ name: z2.string().default("Unknown"),
424
+ routers: z2.array(z2.string()).default([]),
425
+ pages: z2.array(z2.string()).default([]),
426
+ tables: z2.array(z2.string()).default([]),
427
+ allowedImportsFrom: z2.array(z2.string()).default([])
428
+ });
429
+ PatternRuleConfigSchema = z2.object({
430
+ pattern: z2.string().default("**"),
431
+ rules: z2.array(z2.string()).default([]),
432
+ language: z2.string().optional()
433
+ });
434
+ CostModelSchema = z2.object({
435
+ input_per_million: z2.number(),
436
+ output_per_million: z2.number(),
437
+ cache_read_per_million: z2.number().optional(),
438
+ cache_write_per_million: z2.number().optional()
439
+ });
440
+ AnalyticsConfigSchema = z2.object({
441
+ quality: z2.object({
442
+ weights: z2.record(z2.string(), z2.number()).default({
443
+ bug_found: -5,
444
+ vr_failure: -10,
445
+ incident: -20,
446
+ cr_violation: -3,
447
+ vr_pass: 2,
448
+ clean_commit: 5,
449
+ successful_verification: 3
450
+ }),
451
+ categories: z2.array(z2.string()).default(["security", "architecture", "coupling", "tests", "rule_compliance"])
452
+ }).optional(),
453
+ cost: z2.object({
454
+ models: z2.record(z2.string(), CostModelSchema).default({}),
455
+ currency: z2.string().default("USD")
456
+ }).optional(),
457
+ prompts: z2.object({
458
+ success_indicators: z2.array(z2.string()).default(["committed", "approved", "looks good", "perfect", "great", "thanks"]),
459
+ failure_indicators: z2.array(z2.string()).default(["revert", "wrong", "that's not", "undo", "incorrect"]),
460
+ max_turns_for_success: z2.number().default(2)
461
+ }).optional()
462
+ }).optional();
463
+ CustomPatternSchema = z2.object({
464
+ pattern: z2.string(),
465
+ severity: z2.string(),
466
+ message: z2.string()
467
+ });
468
+ GovernanceConfigSchema = z2.object({
469
+ audit: z2.object({
470
+ formats: z2.array(z2.string()).default(["summary", "detailed", "soc2"]),
471
+ retention_days: z2.number().default(365),
472
+ auto_log: z2.record(z2.string(), z2.boolean()).default({
473
+ code_changes: true,
474
+ rule_enforcement: true,
475
+ approvals: true,
476
+ commits: true
477
+ })
478
+ }).optional(),
479
+ validation: z2.object({
480
+ realtime: z2.boolean().default(true),
481
+ checks: z2.record(z2.string(), z2.boolean()).default({
482
+ rule_compliance: true,
483
+ import_existence: true,
484
+ naming_conventions: true
485
+ }),
486
+ custom_patterns: z2.array(CustomPatternSchema).default([])
487
+ }).optional(),
488
+ adr: z2.object({
489
+ detection_phrases: z2.array(z2.string()).default(["chose", "decided", "switching to", "moving from", "going with"]),
490
+ template: z2.string().default("default"),
491
+ storage: z2.string().default("database"),
492
+ output_dir: z2.string().default("docs/adr")
493
+ }).optional()
494
+ }).optional();
495
+ SecurityPatternSchema = z2.object({
496
+ pattern: z2.string(),
497
+ severity: z2.string(),
498
+ category: z2.string(),
499
+ description: z2.string()
500
+ });
501
+ SecurityConfigSchema = z2.object({
502
+ patterns: z2.array(SecurityPatternSchema).default([]),
503
+ auto_score_on_edit: z2.boolean().default(true),
504
+ score_threshold_alert: z2.number().default(50),
505
+ severity_weights: z2.record(z2.string(), z2.number()).optional(),
506
+ restrictive_licenses: z2.array(z2.string()).optional(),
507
+ dep_alternatives: z2.record(z2.string(), z2.array(z2.string())).optional(),
508
+ dependencies: z2.object({
509
+ package_manager: z2.string().default("npm"),
510
+ blocked_packages: z2.array(z2.string()).default([]),
511
+ preferred_packages: z2.record(z2.string(), z2.string()).default({}),
512
+ max_bundle_size_kb: z2.number().default(500)
513
+ }).optional()
514
+ }).optional();
515
+ TeamConfigSchema = z2.object({
516
+ enabled: z2.boolean().default(false),
517
+ sync_backend: z2.string().default("local"),
518
+ developer_id: z2.string().default("auto"),
519
+ share_by_default: z2.boolean().default(false),
520
+ expertise_weights: z2.object({
521
+ session: z2.number().default(20),
522
+ observation: z2.number().default(10)
523
+ }).optional(),
524
+ privacy: z2.object({
525
+ share_file_paths: z2.boolean().default(true),
526
+ share_code_snippets: z2.boolean().default(false),
527
+ share_observations: z2.boolean().default(true)
528
+ }).optional()
529
+ }).optional();
530
+ RegressionConfigSchema = z2.object({
531
+ test_patterns: z2.array(z2.string()).default([
532
+ "{dir}/__tests__/{name}.test.{ext}",
533
+ "{dir}/{name}.spec.{ext}",
534
+ "tests/{path}.test.{ext}"
535
+ ]),
536
+ test_runner: z2.string().default("npm test"),
537
+ health_thresholds: z2.object({
538
+ healthy: z2.number().default(80),
539
+ warning: z2.number().default(50)
540
+ }).optional()
541
+ }).optional();
542
+ AutoLearningConfigSchema = z2.object({
543
+ enabled: z2.boolean().default(true),
544
+ incidentDir: z2.string().default("docs/incidents"),
545
+ memoryDir: z2.string().default("memory"),
546
+ memoryIndexFile: z2.string().default("MEMORY.md"),
547
+ enforcementHooksDir: z2.string().default("scripts/hooks"),
548
+ fixDetection: z2.object({
549
+ enabled: z2.boolean().default(true),
550
+ lookbackDays: z2.number().default(7),
551
+ signals: z2.array(z2.string()).default([
552
+ "removed_broken_code",
553
+ "added_error_handling",
554
+ "method_name_correction",
555
+ "auth_fix",
556
+ "nil_handling_fix",
557
+ "concurrency_fix",
558
+ "async_pattern_fix",
559
+ "added_missing_import"
560
+ ])
561
+ }).default({}),
562
+ failureClassification: z2.object({
563
+ enabled: z2.boolean().default(true),
564
+ thresholds: z2.object({
565
+ known: z2.number().default(5),
566
+ similar: z2.number().default(3)
567
+ }).default({}),
568
+ scoring: z2.object({
569
+ diffPatternWeight: z2.number().default(3),
570
+ filePatternWeight: z2.number().default(2),
571
+ promptKeywordWeight: z2.number().default(2)
572
+ }).default({})
573
+ }).default({}),
574
+ pipeline: z2.object({
575
+ requireIncidentReport: z2.boolean().default(true),
576
+ requirePreventionRule: z2.boolean().default(true),
577
+ requireEnforcement: z2.boolean().default(true)
578
+ }).default({}),
579
+ // plan-v0.2-interactive-rule-approval P-D-008 / P-D-009: project-configured
580
+ // custom destinations for the rule-candidate funnel. The classifier matches
581
+ // a candidate to one of these entries when none of the framework
582
+ // destinations (pattern-scanner / claude-md-cr / corrections-md) apply.
583
+ customDestinations: z2.array(z2.object({
584
+ name: z2.string(),
585
+ path: z2.string(),
586
+ triggerKeywords: z2.array(z2.string()).default([]),
587
+ template: z2.string()
588
+ })).default([])
589
+ }).optional();
590
+ CloudConfigSchema = z2.object({
591
+ enabled: z2.boolean().default(false),
592
+ apiKey: z2.string().optional(),
593
+ endpoint: z2.string().optional(),
594
+ sync: z2.object({
595
+ memory: z2.boolean().default(true),
596
+ analytics: z2.boolean().default(true),
597
+ audit: z2.boolean().default(true)
598
+ }).default({ memory: true, analytics: true, audit: true })
599
+ }).optional();
600
+ ConventionsConfigSchema = z2.object({
601
+ claudeDirName: z2.string().default(".claude").refine(
602
+ (s) => !s.includes("..") && !s.startsWith("/"),
603
+ { message: 'claudeDirName must not contain ".." or start with "/"' }
604
+ ),
605
+ sessionStatePath: z2.string().default(".claude/session-state/CURRENT.md").refine(
606
+ (s) => !s.includes("..") && !s.startsWith("/"),
607
+ { message: 'sessionStatePath must not contain ".." or start with "/"' }
608
+ ),
609
+ sessionArchivePath: z2.string().default(".claude/session-state/archive").refine(
610
+ (s) => !s.includes("..") && !s.startsWith("/"),
611
+ { message: 'sessionArchivePath must not contain ".." or start with "/"' }
612
+ ),
613
+ knowledgeCategories: z2.array(z2.string()).default([
614
+ "patterns",
615
+ "commands",
616
+ "incidents",
617
+ "reference",
618
+ "protocols",
619
+ "checklists",
620
+ "playbooks",
621
+ "critical",
622
+ "scripts",
623
+ "status",
624
+ "templates",
625
+ "loop-state",
626
+ "session-state",
627
+ "agents"
628
+ ]),
629
+ knowledgeSourceFiles: z2.array(z2.string()).default(["CLAUDE.md", "MEMORY.md", "corrections.md"]),
630
+ excludePatterns: z2.array(z2.string()).default(["/ARCHIVE/", "/SESSION-HISTORY/"])
631
+ }).optional();
632
+ PythonDomainConfigSchema = z2.object({
633
+ name: z2.string(),
634
+ packages: z2.array(z2.string()),
635
+ allowed_imports_from: z2.array(z2.string()).default([])
636
+ });
637
+ PythonConfigSchema = z2.object({
638
+ root: z2.string(),
639
+ alembic_dir: z2.string().optional(),
640
+ domains: z2.array(PythonDomainConfigSchema).default([]),
641
+ exclude_dirs: z2.array(z2.string()).default(["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"])
642
+ }).optional();
643
+ PathsConfigSchema = z2.object({
644
+ source: z2.string().default("src"),
645
+ aliases: z2.record(z2.string(), z2.string()).default({ "@": "src" }),
646
+ monorepo_roots: z2.array(z2.string()).optional(),
647
+ routers: z2.string().optional(),
648
+ routerRoot: z2.string().optional(),
649
+ pages: z2.string().optional(),
650
+ middleware: z2.string().optional(),
651
+ schema: z2.string().optional(),
652
+ components: z2.string().optional(),
653
+ hooks: z2.string().optional()
654
+ });
655
+ LanguageFrameworkEntrySchema = z2.object({
656
+ framework: z2.string().optional(),
657
+ test_framework: z2.string().optional(),
658
+ test: z2.string().optional(),
659
+ runtime: z2.string().optional(),
660
+ orm: z2.string().optional(),
661
+ router: z2.string().optional(),
662
+ ui: z2.string().optional()
663
+ }).passthrough();
664
+ FrameworkConfigSchema = z2.object({
665
+ type: z2.string().default("typescript"),
666
+ primary: z2.string().optional(),
667
+ router: z2.string().default("none"),
668
+ orm: z2.string().default("none"),
669
+ ui: z2.string().default("none"),
670
+ languages: z2.record(z2.string(), LanguageFrameworkEntrySchema).optional()
671
+ }).passthrough();
672
+ DetectedConfigSchema = z2.object({}).passthrough().optional();
673
+ VerificationEntrySchema = z2.object({
674
+ type: z2.string().optional(),
675
+ test: z2.string().optional(),
676
+ syntax: z2.string().optional(),
677
+ lint: z2.string().optional(),
678
+ build: z2.string().optional()
679
+ }).passthrough();
680
+ VerificationConfigSchema = z2.record(z2.string(), VerificationEntrySchema).optional();
681
+ CanonicalPathsSchema = z2.record(z2.string(), z2.string()).optional();
682
+ VerificationTypesSchema = z2.record(z2.string(), z2.string()).optional();
683
+ DetectionRuleEntrySchema = z2.object({
684
+ signals: z2.array(z2.string()).default([]),
685
+ priority: z2.number().optional()
686
+ }).passthrough();
687
+ DetectionConfigSchema = z2.object({
688
+ rules: z2.record(
689
+ z2.string(),
690
+ // language
691
+ z2.record(z2.string(), DetectionRuleEntrySchema)
692
+ // framework -> rule entry
693
+ ).optional(),
694
+ signal_weights: z2.record(z2.string(), z2.number()).optional(),
695
+ disable_builtin: z2.boolean().optional()
696
+ }).passthrough().optional();
697
+ WatchConfigSchema = z2.object({
698
+ debounce_ms: z2.number().int().positive().default(3e3),
699
+ storm_threshold: z2.number().int().positive().default(50),
700
+ deep_storm_threshold: z2.number().int().positive().default(500),
701
+ hard_timeout_ms: z2.number().int().positive().default(3e5),
702
+ scope: z2.enum(["paths", "full"]).default("paths"),
703
+ // Plan 3a hotfix 2026-05-02: refuse to start if the watch surface
704
+ // exceeds this many files. Prevents the misconfig pattern where
705
+ // `paths.source_dirs` includes `.` or otherwise expands to a 60K+
706
+ // file tree, producing 30-100% steady CPU. Override via
707
+ // `paths_full_root_opt_in: true` for users on small repos who genuinely
708
+ // need root-level watching.
709
+ max_watched_files: z2.number().int().positive().default(1e4),
710
+ paths_full_root_opt_in: z2.boolean().default(false)
711
+ }).passthrough().optional();
712
+ AdapterLocalPathSchema = z2.string().refine((s) => !/^([A-Za-z]:[\\/]|[\\/])/.test(s), {
713
+ message: "absolute paths are rejected; adapters.local entries must be relative to the massu.config.yaml directory"
714
+ }).refine((s) => !s.split(/[\\/]/).includes(".."), {
715
+ message: "parent-directory traversal (`..`) is rejected; adapters.local entries must stay inside the project tree"
716
+ }).transform((s) => s.split(/[\\/]/).filter((part) => part !== "" && part !== ".").join("/"));
717
+ AdaptersConfigSchema = z2.object({
718
+ enabled: z2.boolean().default(false),
719
+ local: z2.array(AdapterLocalPathSchema).default([])
720
+ }).passthrough().optional();
721
+ TelemetryConfigSchema = z2.object({
722
+ adapters: z2.boolean().default(false)
723
+ }).passthrough().optional();
724
+ LSPConfigSchema = z2.object({
725
+ enabled: z2.boolean().default(false),
726
+ servers: z2.array(z2.object({
727
+ language: z2.string(),
728
+ command: z2.string(),
729
+ // F-014 (closed 2026-05-06): explicit opt-in to spawn SUID/SGID
730
+ // binaries. Default false — argv[0] with the SUID bit is rejected
731
+ // unless this is true. Decision is auditable in the YAML.
732
+ allow_setuid: z2.boolean().default(false),
733
+ // F-015 (closed 2026-05-06): per-server RSS budget (MB). Watchdog
734
+ // SIGKILLs the server after sustained breach. Default 1024 MB.
735
+ // Set to 0 to disable the watchdog for this server.
736
+ max_rss_mb: z2.number().int().nonnegative().default(1024)
737
+ })).default([]),
738
+ autoDetect: z2.object({
739
+ viaPortScan: z2.boolean().default(false)
740
+ }).optional()
741
+ }).passthrough();
742
+ RawConfigSchema = z2.object({
743
+ schema_version: z2.union([z2.literal(1), z2.literal(2)]).default(1),
744
+ project: z2.object({
745
+ name: z2.string().default("my-project"),
746
+ root: z2.string().default("auto")
747
+ }).default({ name: "my-project", root: "auto" }),
748
+ framework: FrameworkConfigSchema.default({
749
+ type: "typescript",
750
+ router: "none",
751
+ orm: "none",
752
+ ui: "none"
753
+ }),
754
+ paths: PathsConfigSchema.default({ source: "src", aliases: { "@": "src" } }),
755
+ toolPrefix: z2.string().default("massu"),
756
+ dbAccessPattern: z2.string().optional(),
757
+ knownMismatches: z2.record(z2.string(), z2.record(z2.string(), z2.string())).optional(),
758
+ accessScopes: z2.array(z2.string()).optional(),
759
+ domains: z2.array(DomainConfigSchema).default([]),
760
+ rules: z2.array(PatternRuleConfigSchema).default([]),
761
+ // P-M-036 (plan-stage-d-medium-sweep): customer-authored CR-style
762
+ // governance rules. DISTINCT from `rules:` above (path-scoped lint hints
763
+ // used by pattern-scanner). At config-refresh time these entries are
764
+ // loaded into the `knowledge_rules` SQLite table with
765
+ // `source = 'customer-config'` so `massu_knowledge_rule` and the
766
+ // governance docs surface customer-defined rules alongside framework CRs.
767
+ governance_rules: z2.array(
768
+ z2.object({
769
+ id: z2.string().min(1, "governance_rules[].id is required"),
770
+ title: z2.string().min(1, "governance_rules[].title is required"),
771
+ description: z2.string().min(1, "governance_rules[].description is required"),
772
+ vr_type: z2.string().default("VR-CUSTOM"),
773
+ reference_path: z2.string().optional(),
774
+ severity: z2.enum(["critical", "high", "medium", "low", "info"]).default("medium")
775
+ }).passthrough()
776
+ ).default([]),
777
+ analytics: AnalyticsConfigSchema,
778
+ governance: GovernanceConfigSchema,
779
+ security: SecurityConfigSchema,
780
+ team: TeamConfigSchema,
781
+ regression: RegressionConfigSchema,
782
+ cloud: CloudConfigSchema,
783
+ // plan-living-memory-slice-1 P6-002: automatic-recall tunables.
784
+ memory: MemoryConfigSchema,
785
+ conventions: ConventionsConfigSchema,
786
+ autoLearning: AutoLearningConfigSchema,
787
+ python: PythonConfigSchema,
788
+ // P2-004 / P2-005 / P2-006 / P2-008: v2 extensions (all optional)
789
+ verification: VerificationConfigSchema,
790
+ canonical_paths: CanonicalPathsSchema,
791
+ verification_types: VerificationTypesSchema,
792
+ detection: DetectionConfigSchema,
793
+ // Plan #2: detector-owned per-language conventions (free-form passthrough)
794
+ detected: DetectedConfigSchema,
795
+ // Plan 3a: file-watcher daemon tunables
796
+ watch: WatchConfigSchema,
797
+ // Plan 3c: third-party adapter registry kill-switch + signing override + local-path opt-in.
798
+ adapters: AdaptersConfigSchema,
799
+ // Plan 3c: anonymous adapter-discovery telemetry opt-in (default off).
800
+ telemetry: TelemetryConfigSchema,
801
+ // Plan 3b Phase 4: optional LSP enrichment of AST adapter results.
802
+ lsp: LSPConfigSchema.optional()
803
+ }).passthrough();
804
+ _config = null;
805
+ _resolvedApiKeySource = "none";
806
+ _projectRoot = null;
807
+ }
808
+ });
809
+
810
+ // src/memory-vector.ts
811
+ function float32ToBlob(vec) {
812
+ return Buffer.from(new Uint8Array(vec.buffer, vec.byteOffset, vec.byteLength));
813
+ }
814
+ function blobToFloat32(buf) {
815
+ if (!buf || buf.byteLength % 4 !== 0) return null;
816
+ const bytes = new Uint8Array(buf.byteLength);
817
+ bytes.set(buf instanceof Buffer ? new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength) : buf);
818
+ return new Float32Array(bytes.buffer, 0, buf.byteLength / 4);
819
+ }
820
+ function l2normalize(vec) {
821
+ let sumSq = 0;
822
+ for (let i = 0; i < vec.length; i++) sumSq += vec[i] * vec[i];
823
+ const norm = Math.sqrt(sumSq);
824
+ if (norm === 0 || !Number.isFinite(norm)) return vec;
825
+ const out = new Float32Array(vec.length);
826
+ for (let i = 0; i < vec.length; i++) out[i] = vec[i] / norm;
827
+ return out;
828
+ }
829
+ function cosineSim(a, b) {
830
+ if (a.length !== b.length || a.length === 0) return 0;
831
+ let dot = 0;
832
+ let na = 0;
833
+ let nb = 0;
834
+ for (let i = 0; i < a.length; i++) {
835
+ dot += a[i] * b[i];
836
+ na += a[i] * a[i];
837
+ nb += b[i] * b[i];
838
+ }
839
+ if (na === 0 || nb === 0) return 0;
840
+ const denom = Math.sqrt(na) * Math.sqrt(nb);
841
+ if (denom === 0 || !Number.isFinite(denom)) return 0;
842
+ return dot / denom;
843
+ }
844
+ var init_memory_vector = __esm({
845
+ "src/memory-vector.ts"() {
846
+ "use strict";
847
+ }
848
+ });
849
+
850
+ // src/db-backup.ts
851
+ import Database from "better-sqlite3";
852
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync2 } from "fs";
853
+ import { resolve as resolve3, join, basename, dirname as dirname2 } from "path";
854
+ import { homedir as homedir3 } from "os";
855
+ function dbBackupsRoot(home = homedir3()) {
856
+ return resolve3(home, ".massu", "db-backups");
857
+ }
858
+ function projectBackupDir(projectRoot, home = homedir3()) {
859
+ const slug = basename(projectRoot).replace(/[^A-Za-z0-9._-]/g, "_") || "project";
860
+ return join(dbBackupsRoot(home), slug);
861
+ }
862
+ function backupStamp(nowMs) {
863
+ return new Date(nowMs).toISOString().replace(/[:.]/g, "-");
864
+ }
865
+ function listDbBackups(projectRoot, home = homedir3()) {
866
+ const dir = projectBackupDir(projectRoot, home);
867
+ if (!existsSync3(dir)) return [];
868
+ const out = [];
869
+ for (const f of readdirSync(dir)) {
870
+ if (!f.endsWith(".db")) continue;
871
+ const p = join(dir, f);
872
+ const st = statSync(p);
873
+ const db = f.replace(/-\d{4}-\d{2}-\d{2}T.*$/, "");
874
+ out.push({ db, path: p, bytes: st.size, mtimeMs: st.mtimeMs });
875
+ }
876
+ return out.sort((a, b) => b.mtimeMs - a.mtimeMs);
877
+ }
878
+ function hasFreshDbBackup(projectRoot, dbPath, nowMs = Date.now(), home = homedir3()) {
879
+ const name = basename(dbPath, ".db");
880
+ const backups = listDbBackups(projectRoot, home).filter((b) => b.db === name);
881
+ if (backups.length === 0) return false;
882
+ const newest = backups[0];
883
+ if (nowMs - newest.mtimeMs > FRESH_WINDOW_MS) return false;
884
+ if (existsSync3(dbPath) && statSync(dbPath).mtimeMs > newest.mtimeMs) return false;
885
+ return true;
886
+ }
887
+ function integrityOk(dbPath) {
888
+ let db = null;
889
+ try {
890
+ db = new Database(dbPath, { readonly: true });
891
+ const rows = db.pragma("integrity_check");
892
+ return rows.length === 1 && rows[0].integrity_check === "ok";
893
+ } catch {
894
+ return false;
895
+ } finally {
896
+ try {
897
+ db?.close();
898
+ } catch {
899
+ }
900
+ }
901
+ }
902
+ function backupDb(projectRoot, dbPath, nowMs = Date.now(), home = homedir3()) {
903
+ if (!existsSync3(dbPath)) {
904
+ throw new DbBackupError(`cannot back up a database that does not exist: ${dbPath}`);
905
+ }
906
+ const dir = projectBackupDir(projectRoot, home);
907
+ mkdirSync2(dir, { recursive: true });
908
+ const name = basename(dbPath, ".db");
909
+ const dest = join(dir, `${name}-${backupStamp(nowMs)}.db`);
910
+ let src = null;
911
+ try {
912
+ src = new Database(dbPath, { readonly: true });
913
+ src.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
914
+ } catch (err) {
915
+ throw new DbBackupError(
916
+ `VACUUM INTO failed for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
917
+ );
918
+ } finally {
919
+ try {
920
+ src?.close();
921
+ } catch {
922
+ }
923
+ }
924
+ if (!integrityOk(dest)) {
925
+ try {
926
+ unlinkSync(dest);
927
+ } catch {
928
+ }
929
+ throw new DbBackupError(
930
+ `the backup of ${dbPath} FAILED its own integrity check and was deleted. A corrupt backup is worse than none \u2014 you only discover it when you need it.`
931
+ );
932
+ }
933
+ pruneDbBackups(projectRoot, name, DEFAULT_RETENTION, home);
934
+ const st = statSync(dest);
935
+ return { db: name, path: dest, bytes: st.size, mtimeMs: st.mtimeMs };
936
+ }
937
+ function pruneDbBackups(projectRoot, dbName, keep = DEFAULT_RETENTION, home = homedir3()) {
938
+ const mine = listDbBackups(projectRoot, home).filter((b) => b.db === dbName);
939
+ let removed = 0;
940
+ for (const old of mine.slice(keep)) {
941
+ try {
942
+ unlinkSync(old.path);
943
+ removed++;
944
+ } catch {
945
+ }
946
+ }
947
+ return removed;
948
+ }
949
+ function backupBeforeSchemaChange(projectRoot, dbPath, onError, nowMs = Date.now(), home = homedir3()) {
950
+ try {
951
+ if (!existsSync3(dbPath)) return null;
952
+ if (hasFreshDbBackup(projectRoot, dbPath, nowMs, home)) return null;
953
+ return backupDb(projectRoot, dbPath, nowMs, home);
954
+ } catch (err) {
955
+ onError(err);
956
+ return null;
957
+ }
958
+ }
959
+ var DEFAULT_RETENTION, FRESH_WINDOW_MS, DbBackupError;
960
+ var init_db_backup = __esm({
961
+ "src/db-backup.ts"() {
962
+ "use strict";
963
+ DEFAULT_RETENTION = 5;
964
+ FRESH_WINDOW_MS = 24 * 60 * 60 * 1e3;
965
+ DbBackupError = class extends Error {
966
+ constructor(message) {
967
+ super(message);
968
+ this.name = "DbBackupError";
969
+ }
970
+ };
971
+ }
972
+ });
973
+
974
+ // src/hooks/lib/hook-failure-signal.ts
975
+ import { appendFileSync, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
976
+ import { join as join2, dirname as dirname3 } from "path";
977
+ function resolveFailureLogPath() {
978
+ const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
979
+ if (explicit) return explicit;
980
+ let dir = process.cwd();
981
+ for (let i = 0; i < 12; i++) {
982
+ if (existsSync4(join2(dir, ".massu")) || existsSync4(join2(dir, "massu.config.yaml"))) {
983
+ return join2(dir, ".massu", "hook-failures.jsonl");
984
+ }
985
+ const parent = dirname3(dir);
986
+ if (parent === dir) break;
987
+ dir = parent;
988
+ }
989
+ return join2(process.cwd(), ".massu", "hook-failures.jsonl");
990
+ }
991
+ function recordHookFailure(hook, error, context) {
992
+ const record = {
993
+ hook,
994
+ error: error instanceof Error ? error.message : String(error),
995
+ stack: error instanceof Error ? error.stack?.split("\n").slice(0, 6).join("\n") : void 0,
996
+ context,
997
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
998
+ };
999
+ let wroteSomething = false;
1000
+ try {
1001
+ const path = resolveFailureLogPath();
1002
+ mkdirSync3(dirname3(path), { recursive: true });
1003
+ appendFileSync(path, JSON.stringify(record) + "\n", "utf-8");
1004
+ wroteSomething = true;
1005
+ } catch {
1006
+ }
1007
+ try {
1008
+ process.stderr.write(
1009
+ `[massu] HOOK FAILURE in ${hook}: ${record.error}
1010
+ [massu] This is a bug in Massu, not in your code. See .massu/hook-failures.jsonl
1011
+ `
1012
+ );
1013
+ wroteSomething = true;
1014
+ } catch {
1015
+ }
1016
+ try {
1017
+ recordHookHealthRow(record);
1018
+ } catch {
1019
+ }
1020
+ return wroteSomething;
1021
+ }
1022
+ function recordHookHealthRow(record) {
1023
+ const { getMemoryDb: getMemoryDb2 } = (init_memory_db(), __toCommonJS(memory_db_exports));
1024
+ const db = getMemoryDb2();
1025
+ try {
1026
+ db.prepare(
1027
+ `INSERT INTO hook_health (hook, error, context_json, occurred_at)
1028
+ VALUES (?, ?, ?, ?)`
1029
+ ).run(
1030
+ record.hook,
1031
+ record.error,
1032
+ record.context ? JSON.stringify(record.context) : null,
1033
+ record.timestamp
1034
+ );
1035
+ } finally {
1036
+ db.close();
1037
+ }
1038
+ }
1039
+ var init_hook_failure_signal = __esm({
1040
+ "src/hooks/lib/hook-failure-signal.ts"() {
1041
+ "use strict";
1042
+ }
1043
+ });
1044
+
1045
+ // src/memory-embedder-tokenizer.ts
1046
+ import { readFileSync as readFileSync3 } from "fs";
1047
+ function loadVocab(vocabPath) {
1048
+ const lines = readFileSync3(vocabPath, "utf-8").split("\n");
1049
+ const vocab = /* @__PURE__ */ new Map();
1050
+ for (let i = 0; i < lines.length; i++) {
1051
+ const tok = lines[i].replace(/\r$/, "");
1052
+ if (tok.length === 0 && i === lines.length - 1) continue;
1053
+ vocab.set(tok, i);
1054
+ }
1055
+ return vocab;
1056
+ }
1057
+ function isWhitespace(ch) {
1058
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r") return true;
1059
+ const cp = ch.codePointAt(0);
1060
+ return cp === 160 || cp >= 8192 && cp <= 8202 || cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287 || cp === 12288 || cp === 65279;
1061
+ }
1062
+ function isControl(ch) {
1063
+ if (ch === " " || ch === "\n" || ch === "\r") return false;
1064
+ const cp = ch.codePointAt(0);
1065
+ return cp <= 31 || cp >= 127 && cp <= 159;
1066
+ }
1067
+ function isPunctuation(ch) {
1068
+ const cp = ch.codePointAt(0);
1069
+ if (cp >= 33 && cp <= 47 || cp >= 58 && cp <= 64 || cp >= 91 && cp <= 96 || cp >= 123 && cp <= 126)
1070
+ return true;
1071
+ return /\p{P}|\p{S}/u.test(ch);
1072
+ }
1073
+ function isCJK(cp) {
1074
+ return cp >= 19968 && cp <= 40959 || cp >= 13312 && cp <= 19903 || cp >= 131072 && cp <= 173791 || cp >= 173824 && cp <= 177983 || cp >= 177984 && cp <= 178207 || cp >= 178208 && cp <= 183983 || cp >= 63744 && cp <= 64255 || cp >= 194560 && cp <= 195103;
1075
+ }
1076
+ function stripAccents(text) {
1077
+ return text.normalize("NFD").replace(/\p{Mn}/gu, "");
1078
+ }
1079
+ function basicTokenize(text) {
1080
+ let cleaned = "";
1081
+ for (const ch of text) {
1082
+ const cp = ch.codePointAt(0);
1083
+ if (cp === 0 || cp === 65533 || isControl(ch)) continue;
1084
+ if (isWhitespace(ch)) {
1085
+ cleaned += " ";
1086
+ continue;
1087
+ }
1088
+ if (isCJK(cp)) {
1089
+ cleaned += " " + ch + " ";
1090
+ continue;
1091
+ }
1092
+ cleaned += ch;
1093
+ }
1094
+ const rawTokens = cleaned.split(/\s+/).filter(Boolean);
1095
+ const out = [];
1096
+ for (let tok of rawTokens) {
1097
+ tok = stripAccents(tok.toLowerCase());
1098
+ let cur = "";
1099
+ for (const ch of tok) {
1100
+ if (isPunctuation(ch)) {
1101
+ if (cur) {
1102
+ out.push(cur);
1103
+ cur = "";
1104
+ }
1105
+ out.push(ch);
1106
+ } else {
1107
+ cur += ch;
1108
+ }
1109
+ }
1110
+ if (cur) out.push(cur);
1111
+ }
1112
+ return out;
1113
+ }
1114
+ function wordpieceTokenize(token, vocab, maxChars = 100) {
1115
+ if (token.length > maxChars) return [UNK];
1116
+ const chars = Array.from(token);
1117
+ const subTokens = [];
1118
+ let start = 0;
1119
+ let bad = false;
1120
+ while (start < chars.length) {
1121
+ let end = chars.length;
1122
+ let curSub = null;
1123
+ while (start < end) {
1124
+ let substr = chars.slice(start, end).join("");
1125
+ if (start > 0) substr = "##" + substr;
1126
+ if (vocab.has(substr)) {
1127
+ curSub = substr;
1128
+ break;
1129
+ }
1130
+ end -= 1;
1131
+ }
1132
+ if (curSub === null) {
1133
+ bad = true;
1134
+ break;
1135
+ }
1136
+ subTokens.push(curSub);
1137
+ start = end;
1138
+ }
1139
+ return bad ? [UNK] : subTokens;
1140
+ }
1141
+ function encode(text, vocab, opts = {}) {
1142
+ const maxLen = opts.maxLen ?? 256;
1143
+ const basic = basicTokenize(text);
1144
+ const wpTokens = [];
1145
+ for (const t of basic) {
1146
+ for (const sub of wordpieceTokenize(t, vocab)) wpTokens.push(sub);
1147
+ }
1148
+ const truncated = wpTokens.slice(0, maxLen - 2);
1149
+ const tokens = [CLS, ...truncated, SEP];
1150
+ const unkId = vocab.get(UNK) ?? 100;
1151
+ const input_ids = tokens.map((t) => vocab.get(t) ?? unkId);
1152
+ const attention_mask = tokens.map(() => 1);
1153
+ const token_type_ids = tokens.map(() => 0);
1154
+ return { tokens, input_ids, attention_mask, token_type_ids };
1155
+ }
1156
+ var UNK, CLS, SEP;
1157
+ var init_memory_embedder_tokenizer = __esm({
1158
+ "src/memory-embedder-tokenizer.ts"() {
1159
+ "use strict";
1160
+ UNK = "[UNK]";
1161
+ CLS = "[CLS]";
1162
+ SEP = "[SEP]";
1163
+ }
1164
+ });
1165
+
1166
+ // src/memory-embedder.ts
1167
+ import { fileURLToPath } from "url";
1168
+ import { dirname as dirname4, join as join3 } from "path";
1169
+ import { existsSync as existsSync5 } from "fs";
1170
+ import { createRequire } from "module";
1171
+ function getActiveEmbedModel() {
1172
+ return _activeModel;
1173
+ }
1174
+ function loadEmbedSettings() {
1175
+ try {
1176
+ const r = getConfig().memory?.recall;
1177
+ return {
1178
+ enabled: r?.embedEnabled ?? true,
1179
+ endpoint: r?.embedEndpoint,
1180
+ model: r?.embedModel
1181
+ };
1182
+ } catch {
1183
+ return { enabled: true };
1184
+ }
1185
+ }
1186
+ function embeddingsDisabled() {
1187
+ return process.env.MASSU_DISABLE_EMBEDDINGS === "1";
1188
+ }
1189
+ function resolveModelDir() {
1190
+ try {
1191
+ let dir = dirname4(fileURLToPath(import.meta.url));
1192
+ let root = null;
1193
+ for (let i = 0; i < 8; i++) {
1194
+ if (existsSync5(join3(dir, "package.json"))) {
1195
+ root = dir;
1196
+ break;
1197
+ }
1198
+ const parent = dirname4(dir);
1199
+ if (parent === dir) break;
1200
+ dir = parent;
1201
+ }
1202
+ if (!root) return null;
1203
+ for (const rel of ["dist/embedder", "assets/embedder"]) {
1204
+ const candidate = join3(root, rel);
1205
+ if (existsSync5(join3(candidate, "model_quantized.onnx"))) return candidate;
1206
+ }
1207
+ return null;
1208
+ } catch {
1209
+ return null;
1210
+ }
1211
+ }
1212
+ function resolveWasmDir() {
1213
+ try {
1214
+ const req = createRequire(import.meta.url);
1215
+ const main2 = req.resolve(ORT_PKG);
1216
+ return dirname4(main2);
1217
+ } catch {
1218
+ return null;
1219
+ }
1220
+ }
1221
+ async function getSession() {
1222
+ if (_sessionPromise) return _sessionPromise;
1223
+ _sessionPromise = (async () => {
1224
+ try {
1225
+ const modelDir = resolveModelDir();
1226
+ const wasmDir = resolveWasmDir();
1227
+ if (!modelDir || !wasmDir) return null;
1228
+ const ort = await import(
1229
+ /* @vite-ignore */
1230
+ ORT_PKG
1231
+ );
1232
+ ort.env.wasm.numThreads = 1;
1233
+ ort.env.wasm.proxy = false;
1234
+ ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
1235
+ _ortTensor = ort.Tensor;
1236
+ const modelPath = join3(modelDir, "model_quantized.onnx");
1237
+ const vocabPath = join3(modelDir, "vocab.txt");
1238
+ _vocab = loadVocab(vocabPath);
1239
+ const session = await ort.InferenceSession.create(modelPath, {
1240
+ executionProviders: ["wasm"],
1241
+ graphOptimizationLevel: "all"
1242
+ });
1243
+ return session;
1244
+ } catch {
1245
+ return null;
1246
+ }
1247
+ })();
1248
+ return _sessionPromise;
1249
+ }
1250
+ function meanPool(hidden, mask, seqLen, hiddenSize) {
1251
+ const out = new Float32Array(hiddenSize);
1252
+ let maskSum = 0;
1253
+ for (let t = 0; t < seqLen; t++) {
1254
+ const m = mask[t];
1255
+ if (m === 0) continue;
1256
+ maskSum += m;
1257
+ const base = t * hiddenSize;
1258
+ for (let h = 0; h < hiddenSize; h++) out[h] += hidden[base + h] * m;
1259
+ }
1260
+ const denom = Math.max(maskSum, 1e-9);
1261
+ for (let h = 0; h < hiddenSize; h++) out[h] /= denom;
1262
+ return out;
1263
+ }
1264
+ function chunkForEmbedding(text) {
1265
+ const t = text.trim();
1266
+ if (!t) return [];
1267
+ if (t.length <= CHUNK_TARGET_CHARS) return [t];
1268
+ const chunks = [];
1269
+ let start = 0;
1270
+ while (start < t.length && chunks.length < MAX_CHUNKS_PER_RECORD) {
1271
+ let end = Math.min(start + CHUNK_TARGET_CHARS, t.length);
1272
+ if (end < t.length) {
1273
+ const floor = start + Math.floor(CHUNK_TARGET_CHARS / 2);
1274
+ const slice = t.slice(start, end);
1275
+ const para = slice.lastIndexOf("\n\n");
1276
+ const sent = Math.max(slice.lastIndexOf(". "), slice.lastIndexOf(".\n"));
1277
+ const space = slice.lastIndexOf(" ");
1278
+ for (const rel of [para, sent, space]) {
1279
+ const abs = start + rel;
1280
+ if (rel > 0 && abs > floor) {
1281
+ end = abs + 1;
1282
+ break;
1283
+ }
1284
+ }
1285
+ }
1286
+ const chunk = t.slice(start, end).trim();
1287
+ if (chunk) chunks.push(chunk);
1288
+ if (end >= t.length) break;
1289
+ start = Math.max(end - CHUNK_OVERLAP_CHARS, start + 1);
1290
+ }
1291
+ return chunks;
1292
+ }
1293
+ async function embedTier1(text) {
1294
+ try {
1295
+ const session = await getSession();
1296
+ if (!session || !_vocab || !_ortTensor) return null;
1297
+ const { input_ids, attention_mask, token_type_ids } = encode(text, _vocab);
1298
+ const seqLen = input_ids.length;
1299
+ const ids = BigInt64Array.from(input_ids.map((x) => BigInt(x)));
1300
+ const mask = BigInt64Array.from(attention_mask.map((x) => BigInt(x)));
1301
+ const types = BigInt64Array.from(token_type_ids.map((x) => BigInt(x)));
1302
+ const dims = [1, seqLen];
1303
+ const Tensor = _ortTensor;
1304
+ const feeds = {
1305
+ input_ids: new Tensor("int64", ids, dims),
1306
+ attention_mask: new Tensor("int64", mask, dims)
1307
+ };
1308
+ if (session.inputNames.includes("token_type_ids")) {
1309
+ feeds.token_type_ids = new Tensor("int64", types, dims);
1310
+ }
1311
+ const results = await session.run(feeds);
1312
+ const outName = session.outputNames.find((n) => results[n].dims.length === 3) ?? session.outputNames[0];
1313
+ const outTensor = results[outName];
1314
+ const hiddenSize = outTensor.dims[2];
1315
+ const pooled = meanPool(outTensor.data, attention_mask, seqLen, hiddenSize);
1316
+ const vec = l2normalize(pooled);
1317
+ if (vec.length !== EMBED_DIM) return null;
1318
+ _activeModel = { modelId: EMBED_MODEL_ID, dim: EMBED_DIM };
1319
+ return vec;
1320
+ } catch {
1321
+ return null;
1322
+ }
1323
+ }
1324
+ async function embedTier0(text, settings) {
1325
+ const endpoint = settings.endpoint;
1326
+ if (!endpoint) return null;
1327
+ const requestModel = settings.model || EMBED_MODEL_ID;
1328
+ const base = endpoint.replace(/\/+$/, "");
1329
+ const url = `${base}/v1/embeddings`;
1330
+ const controller = new AbortController();
1331
+ const timer = setTimeout(() => controller.abort(), TIER0_TIMEOUT_MS);
1332
+ try {
1333
+ const res = await fetch(url, {
1334
+ method: "POST",
1335
+ headers: { "content-type": "application/json" },
1336
+ body: JSON.stringify({ model: requestModel, input: text }),
1337
+ signal: controller.signal
1338
+ });
1339
+ if (!res.ok) return null;
1340
+ const body = await res.json();
1341
+ const raw = body?.data?.[0]?.embedding;
1342
+ if (!Array.isArray(raw) || raw.length === 0) return null;
1343
+ const vec = l2normalize(Float32Array.from(raw));
1344
+ _activeModel = { modelId: requestModel, dim: vec.length };
1345
+ return vec;
1346
+ } catch {
1347
+ return null;
1348
+ } finally {
1349
+ clearTimeout(timer);
1350
+ }
1351
+ }
1352
+ async function embed(text) {
1353
+ if (!text || !text.trim()) return null;
1354
+ if (embeddingsDisabled()) return null;
1355
+ try {
1356
+ const settings = loadEmbedSettings();
1357
+ if (!settings.enabled) return null;
1358
+ if (settings.endpoint) {
1359
+ const t0 = await embedTier0(text, settings);
1360
+ if (t0) return t0;
1361
+ }
1362
+ return await embedTier1(text);
1363
+ } catch {
1364
+ return null;
1365
+ }
1366
+ }
1367
+ async function embedBatch(texts) {
1368
+ if (!texts || texts.length === 0) return [];
1369
+ if (embeddingsDisabled()) return texts.map(() => null);
1370
+ const settings = loadEmbedSettings();
1371
+ if (!settings.enabled) return texts.map(() => null);
1372
+ const results = [];
1373
+ for (const t of texts) {
1374
+ if (!t || !t.trim()) {
1375
+ results.push(null);
1376
+ continue;
1377
+ }
1378
+ try {
1379
+ let vec = null;
1380
+ if (settings.endpoint) {
1381
+ vec = await embedTier0(t, settings);
1382
+ }
1383
+ if (!vec) vec = await embedTier1(t);
1384
+ results.push(vec);
1385
+ } catch {
1386
+ results.push(null);
1387
+ }
1388
+ }
1389
+ return results;
1390
+ }
1391
+ var EMBED_MODEL_ID, EMBED_DIM, ORT_PKG, _activeModel, _sessionPromise, _vocab, _ortTensor, CHUNK_TARGET_CHARS, CHUNK_OVERLAP_CHARS, MAX_CHUNKS_PER_RECORD, TIER0_TIMEOUT_MS;
1392
+ var init_memory_embedder = __esm({
1393
+ "src/memory-embedder.ts"() {
1394
+ "use strict";
1395
+ init_memory_vector();
1396
+ init_memory_embedder_tokenizer();
1397
+ init_config();
1398
+ EMBED_MODEL_ID = "all-MiniLM-L6-v2";
1399
+ EMBED_DIM = 384;
1400
+ ORT_PKG = "onnxruntime-web";
1401
+ _activeModel = null;
1402
+ _sessionPromise = null;
1403
+ _vocab = null;
1404
+ _ortTensor = null;
1405
+ CHUNK_TARGET_CHARS = 900;
1406
+ CHUNK_OVERLAP_CHARS = 120;
1407
+ MAX_CHUNKS_PER_RECORD = 24;
1408
+ TIER0_TIMEOUT_MS = 2e3;
1409
+ }
1410
+ });
1411
+
1412
+ // src/memory-embed-sweep.ts
1413
+ function readCursor(db, metaTable, key) {
1414
+ if (!KNOWN_META_TABLES.has(metaTable)) return 0;
1415
+ try {
1416
+ const row = db.prepare(`SELECT value FROM ${metaTable} WHERE key = ?`).get(key);
1417
+ const n = row ? Number(row.value) : 0;
1418
+ return Number.isFinite(n) && n >= 0 ? n : 0;
1419
+ } catch {
1420
+ return 0;
1421
+ }
1422
+ }
1423
+ function writeCursor(db, metaTable, key, value) {
1424
+ if (!KNOWN_META_TABLES.has(metaTable)) return;
1425
+ try {
1426
+ db.prepare(`INSERT OR REPLACE INTO ${metaTable} (key, value) VALUES (?, ?)`).run(
1427
+ key,
1428
+ String(value)
1429
+ );
1430
+ } catch {
1431
+ }
1432
+ }
1433
+ async function runEmbedSweep(db, cfg, opts = {}) {
1434
+ const batchSize = Math.max(1, opts.batchSize ?? 16);
1435
+ const limit = opts.limit ?? Infinity;
1436
+ const budgetMs = opts.budgetMs;
1437
+ const start = Date.now();
1438
+ const cursorKey = `embed_sweep_cursor_${cfg.sourceLabel}`;
1439
+ let embedded = 0;
1440
+ let scanned = 0;
1441
+ let active = getActiveEmbedModel();
1442
+ let cursor = readCursor(db, cfg.metaTable, cursorKey);
1443
+ try {
1444
+ while (true) {
1445
+ if (embedded >= limit) break;
1446
+ if (budgetMs !== void 0 && Date.now() - start >= budgetMs) break;
1447
+ const remaining = limit === Infinity ? batchSize : Math.min(batchSize, limit - embedded);
1448
+ const rows = cfg.selectMissing(db, cursor, active, Math.max(1, remaining));
1449
+ if (rows.length === 0) {
1450
+ writeCursor(db, cfg.metaTable, cursorKey, 0);
1451
+ break;
1452
+ }
1453
+ const units = [];
1454
+ for (const r of rows) {
1455
+ if (cfg.chunked) {
1456
+ const parts = chunkForEmbedding(r.text);
1457
+ if (parts.length === 0) continue;
1458
+ parts.forEach((text, chunkIx) => units.push({ id: r.id, chunkIx, text }));
1459
+ } else {
1460
+ units.push({ id: r.id, chunkIx: 0, text: r.text });
1461
+ }
1462
+ }
1463
+ const vecs = await embedBatch(units.map((u) => u.text));
1464
+ if (!active) active = getActiveEmbedModel();
1465
+ if (!active) {
1466
+ break;
1467
+ }
1468
+ const tx = db.transaction(() => {
1469
+ const del = cfg.chunked ? db.prepare(
1470
+ `DELETE FROM ${cfg.embeddingTable}
1471
+ WHERE ${cfg.idCol} = ? AND model_id = ? AND dim = ?`
1472
+ ) : null;
1473
+ const stmt = cfg.chunked ? db.prepare(
1474
+ `INSERT OR REPLACE INTO ${cfg.embeddingTable}
1475
+ (${cfg.idCol}, chunk_ix, model_id, dim, vec, created_at)
1476
+ VALUES (?, ?, ?, ?, ?, datetime('now'))`
1477
+ ) : db.prepare(
1478
+ `INSERT OR REPLACE INTO ${cfg.embeddingTable}
1479
+ (${cfg.idCol}, model_id, dim, vec, created_at)
1480
+ VALUES (?, ?, ?, ?, datetime('now'))`
1481
+ );
1482
+ const cleared = /* @__PURE__ */ new Set();
1483
+ const embeddedIds = /* @__PURE__ */ new Set();
1484
+ for (let i = 0; i < units.length; i++) {
1485
+ const v = vecs[i];
1486
+ if (!v) continue;
1487
+ const u = units[i];
1488
+ if (del && !cleared.has(u.id)) {
1489
+ del.run(u.id, active.modelId, active.dim);
1490
+ cleared.add(u.id);
1491
+ }
1492
+ if (cfg.chunked) {
1493
+ stmt.run(u.id, u.chunkIx, active.modelId, active.dim, float32ToBlob(v));
1494
+ } else {
1495
+ stmt.run(u.id, active.modelId, active.dim, float32ToBlob(v));
1496
+ }
1497
+ embeddedIds.add(u.id);
1498
+ }
1499
+ embedded += embeddedIds.size;
1500
+ });
1501
+ tx();
1502
+ scanned += rows.length;
1503
+ cursor = rows[rows.length - 1].id;
1504
+ writeCursor(db, cfg.metaTable, cursorKey, cursor);
1505
+ }
1506
+ } catch {
1507
+ }
1508
+ return { embedded, scanned };
1509
+ }
1510
+ var KNOWN_META_TABLES;
1511
+ var init_memory_embed_sweep = __esm({
1512
+ "src/memory-embed-sweep.ts"() {
1513
+ "use strict";
1514
+ init_memory_embedder();
1515
+ init_memory_vector();
1516
+ KNOWN_META_TABLES = /* @__PURE__ */ new Set(["memory_meta", "knowledge_meta"]);
1517
+ }
1518
+ });
1519
+
1520
+ // src/memory-db.ts
1521
+ var memory_db_exports = {};
1522
+ __export(memory_db_exports, {
1523
+ CONSOLIDATION_LESSON_EVIDENCE: () => CONSOLIDATION_LESSON_EVIDENCE,
1524
+ MEMORY_FILE_TITLE_LIKE: () => MEMORY_FILE_TITLE_LIKE,
1525
+ MEMORY_FILE_TITLE_PREFIX: () => MEMORY_FILE_TITLE_PREFIX,
1526
+ MEMORY_SCHEMA_VERSION: () => MEMORY_SCHEMA_VERSION,
1527
+ TOOL_COST_EVENTS_RETENTION_DAYS: () => TOOL_COST_EVENTS_RETENTION_DAYS,
1528
+ USAGE_COUNTER_ARMED_KEY: () => USAGE_COUNTER_ARMED_KEY,
1529
+ addConversationTurn: () => addConversationTurn,
1530
+ addFailureClass: () => addFailureClass,
1531
+ addObservation: () => addObservation,
1532
+ addSummary: () => addSummary,
1533
+ addToolCallDetail: () => addToolCallDetail,
1534
+ addUserPrompt: () => addUserPrompt,
1535
+ appendIncidentToFailureClass: () => appendIncidentToFailureClass,
1536
+ armUsageCounter: () => armUsageCounter,
1537
+ assignImportance: () => assignImportance,
1538
+ autoDetectTaskId: () => autoDetectTaskId,
1539
+ createSession: () => createSession,
1540
+ deduplicateFailedAttempt: () => deduplicateFailedAttempt,
1541
+ dequeuePendingSync: () => dequeuePendingSync,
1542
+ drainRulePromotionEvents: () => drainRulePromotionEvents,
1543
+ drainTeamPromotions: () => drainTeamPromotions,
1544
+ drainTeamRevocations: () => drainTeamRevocations,
1545
+ embedMissingObservations: () => embedMissingObservations,
1546
+ endSession: () => endSession,
1547
+ enqueueRulePromotionEvent: () => enqueueRulePromotionEvent,
1548
+ enqueueSyncPayload: () => enqueueSyncPayload,
1549
+ enqueueTeamPromotion: () => enqueueTeamPromotion,
1550
+ enqueueTeamRevocation: () => enqueueTeamRevocation,
1551
+ expireOldLowValueObservations: () => expireOldLowValueObservations,
1552
+ getConversationTurns: () => getConversationTurns,
1553
+ getCrossTaskProgress: () => getCrossTaskProgress,
1554
+ getDecisionsAbout: () => getDecisionsAbout,
1555
+ getFailedAttempts: () => getFailedAttempts,
1556
+ getFailureClasses: () => getFailureClasses,
1557
+ getLastProcessedLine: () => getLastProcessedLine,
1558
+ getMemoryDb: () => getMemoryDb,
1559
+ getMemoryMeta: () => getMemoryMeta,
1560
+ getObservabilityDbSize: () => getObservabilityDbSize,
1561
+ getRecentObservations: () => getRecentObservations,
1562
+ getRecurrenceCountForPromptHash: () => getRecurrenceCountForPromptHash,
1563
+ getSessionStats: () => getSessionStats,
1564
+ getSessionSummaries: () => getSessionSummaries,
1565
+ getSessionTimeline: () => getSessionTimeline,
1566
+ getSessionsByTask: () => getSessionsByTask,
1567
+ getToolPatterns: () => getToolPatterns,
1568
+ incrementRetryCount: () => incrementRetryCount,
1569
+ initMemorySchema: () => initMemorySchema,
1570
+ linkSessionToTask: () => linkSessionToTask,
1571
+ markRecordSuperseded: () => markRecordSuperseded,
1572
+ memoryTableHasTemporal: () => memoryTableHasTemporal,
1573
+ migrateAuditLogCheckExtension: () => migrateAuditLogCheckExtension,
1574
+ migrateMemoryFilesFor4B: () => migrateMemoryFilesFor4B,
1575
+ migrateObservationEmbeddingChunks: () => migrateObservationEmbeddingChunks,
1576
+ pruneOldConversationTurns: () => pruneOldConversationTurns,
1577
+ pruneOldObservations: () => pruneOldObservations,
1578
+ pruneToolCostEvents: () => pruneToolCostEvents,
1579
+ recordRecallHits: () => recordRecallHits,
1580
+ recordTelemetry: () => recordTelemetry,
1581
+ removePendingSync: () => removePendingSync,
1582
+ sanitizeFts5Query: () => sanitizeFts5Query,
1583
+ sanitizeFts5QueryOr: () => sanitizeFts5QueryOr,
1584
+ scoreFailureClasses: () => scoreFailureClasses,
1585
+ searchConversationTurns: () => searchConversationTurns,
1586
+ searchObservations: () => searchObservations,
1587
+ setLastProcessedLine: () => setLastProcessedLine,
1588
+ setMemoryMeta: () => setMemoryMeta,
1589
+ upsertObservationEmbedding: () => upsertObservationEmbedding,
1590
+ usageWarmupElapsed: () => usageWarmupElapsed
1591
+ });
1592
+ import Database2 from "better-sqlite3";
1593
+ import { dirname as dirname5, basename as basename2 } from "path";
1594
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
1595
+ function sanitizeFts5Query(raw) {
1596
+ const trimmed = raw.trim();
1597
+ if (!trimmed) return '""';
1598
+ const tokens = trimmed.replace(/"/g, "").split(/\s+/).filter(Boolean);
1599
+ return tokens.map((t) => `"${t}"`).join(" ");
1600
+ }
1601
+ function sanitizeFts5QueryOr(raw) {
1602
+ const trimmed = raw.trim();
1603
+ if (!trimmed) return '""';
1604
+ const tokens = trimmed.replace(/"/g, "").split(/\s+/).filter((t) => t.replace(/[^a-zA-Z0-9]/g, "").length >= 3);
1605
+ if (tokens.length === 0) return '""';
1606
+ return tokens.map((t) => `"${t}"`).join(" OR ");
1607
+ }
1608
+ function getMemoryDb() {
1609
+ const dbPath = getResolvedPaths().memoryDbPath;
1610
+ const dir = dirname5(dbPath);
1611
+ if (!existsSync6(dir)) {
1612
+ mkdirSync4(dir, { recursive: true });
1613
+ }
1614
+ const preExisting = existsSync6(dbPath);
1615
+ const db = new Database2(dbPath);
1616
+ db.pragma("journal_mode = WAL");
1617
+ db.pragma("foreign_keys = ON");
1618
+ const onDisk = db.pragma("user_version", { simple: true });
1619
+ if (preExisting && onDisk !== MEMORY_SCHEMA_VERSION) {
1620
+ backupBeforeSchemaChange(
1621
+ getProjectRoot(),
1622
+ dbPath,
1623
+ (err) => recordHookFailure("memory-db:pre-ddl-backup", err, { dbPath, onDisk })
1624
+ );
1625
+ }
1626
+ initMemorySchema(db);
1627
+ if (onDisk !== MEMORY_SCHEMA_VERSION) {
1628
+ db.pragma(`user_version = ${MEMORY_SCHEMA_VERSION}`);
1629
+ }
1630
+ return db;
1631
+ }
1632
+ function pruneToolCostEvents(db) {
1633
+ const result = db.prepare(
1634
+ `DELETE FROM tool_cost_events WHERE created_at < datetime('now', '-' || ? || ' days')`
1635
+ ).run(TOOL_COST_EVENTS_RETENTION_DAYS);
1636
+ return result.changes;
1637
+ }
1638
+ function migrateObservationEmbeddingChunks(db) {
1639
+ const cols = db.pragma("table_info(observation_embeddings)");
1640
+ if (cols.length === 0) return;
1641
+ if (cols.some((c) => c.name === "chunk_ix")) return;
1642
+ db.pragma("foreign_keys = OFF");
1643
+ try {
1644
+ db.exec("BEGIN TRANSACTION");
1645
+ db.exec(`
1646
+ CREATE TABLE observation_embeddings_new (
1647
+ observation_id INTEGER NOT NULL REFERENCES observations(id) ON DELETE CASCADE,
1648
+ chunk_ix INTEGER NOT NULL DEFAULT 0,
1649
+ model_id TEXT NOT NULL,
1650
+ dim INTEGER NOT NULL,
1651
+ vec BLOB NOT NULL,
1652
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1653
+ PRIMARY KEY (observation_id, model_id, dim, chunk_ix)
1654
+ );
1655
+ INSERT INTO observation_embeddings_new
1656
+ (observation_id, chunk_ix, model_id, dim, vec, created_at)
1657
+ SELECT observation_id, 0, model_id, dim, vec, created_at FROM observation_embeddings;
1658
+ DROP TABLE observation_embeddings;
1659
+ ALTER TABLE observation_embeddings_new RENAME TO observation_embeddings;
1660
+ CREATE INDEX IF NOT EXISTS idx_obs_emb_model ON observation_embeddings(model_id);
1661
+ `);
1662
+ db.exec("COMMIT");
1663
+ } catch (err) {
1664
+ db.exec("ROLLBACK");
1665
+ throw err;
1666
+ } finally {
1667
+ db.pragma("foreign_keys = ON");
1668
+ }
1669
+ }
1670
+ function migrateMemoryFilesFor4B(db) {
1671
+ const ADDITIONS = [
1672
+ // The renderer's authorship credential (OD-1). NULL on every pre-existing row,
1673
+ // which is the safe direction: no MAC ⇒ unverifiable ⇒ the file is HUMAN.
1674
+ { table: "memory_files", name: "massu_render_mac", decl: "TEXT" },
1675
+ // F-15 stickiness: once a file is human, only `massu memory adopt` reverses it.
1676
+ { table: "memory_files", name: "adopted_human_at_epoch", decl: "INTEGER" },
1677
+ // OD-2: a CACHE of `.massu-tombstones.jsonl`. The ledger is the source of truth.
1678
+ { table: "memory_files", name: "tombstoned_at_epoch", decl: "INTEGER" },
1679
+ { table: "memory_files", name: "origin", decl: "TEXT NOT NULL DEFAULT 'local'" },
1680
+ { table: "memory_files", name: "render_suppressed", decl: "INTEGER NOT NULL DEFAULT 0" },
1681
+ // N-03 — the SOURCE row. This is the one F-08 actually requires.
1682
+ { table: "observations", name: "origin", decl: "TEXT NOT NULL DEFAULT 'local'" }
1683
+ ];
1684
+ for (const add of ADDITIONS) {
1685
+ const cols = db.prepare(`PRAGMA table_info(${add.table})`).all();
1686
+ if (cols.length === 0) continue;
1687
+ if (cols.some((c) => c.name === add.name)) continue;
1688
+ db.exec(`ALTER TABLE ${add.table} ADD COLUMN ${add.name} ${add.decl}`);
1689
+ }
1690
+ }
1691
+ function migrateAuditLogCheckExtension(db) {
1692
+ const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='audit_log'").get();
1693
+ if (!row) return;
1694
+ const expected = [
1695
+ "code_change",
1696
+ "rule_enforced",
1697
+ "approval",
1698
+ "review",
1699
+ "commit",
1700
+ "compaction",
1701
+ "rule_candidate_emitted",
1702
+ "rule_promoted",
1703
+ "rule_dismissed",
1704
+ // A-19/N-02 — without these the memory-file events throw a CHECK violation.
1705
+ // In the ingest path the throw is swallowed by a bare catch, so observability
1706
+ // silently produces NOTHING; in the renderer's transaction it ROLLS BACK a
1707
+ // legitimate render. This migration is what lets an EXISTING db accept them.
1708
+ "memory_file_ingested",
1709
+ "memory_file_expired",
1710
+ "memory_file_adopted_human",
1711
+ "memory_file_rendered",
1712
+ "memory_file_render_refused",
1713
+ "memory_file_tombstoned"
1714
+ ];
1715
+ const checkClauseMatch = row.sql.match(/event_type\s+TEXT\s+NOT\s+NULL\s+CHECK\s*\(\s*event_type\s+IN\s*\(([\s\S]*?)\)\s*\)/i);
1716
+ if (checkClauseMatch) {
1717
+ const values = (checkClauseMatch[1].match(/'([^']+)'/g) ?? []).map((s) => s.slice(1, -1));
1718
+ if (expected.every((v) => values.includes(v))) return;
1719
+ }
1720
+ db.pragma("foreign_keys = OFF");
1721
+ try {
1722
+ db.exec("BEGIN TRANSACTION");
1723
+ db.exec(`
1724
+ CREATE TABLE audit_log_new (
1725
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1726
+ session_id TEXT NOT NULL,
1727
+ timestamp TEXT DEFAULT (datetime('now')),
1728
+ event_type TEXT NOT NULL CHECK(event_type IN (
1729
+ 'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
1730
+ 'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
1731
+ 'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
1732
+ 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
1733
+ )),
1734
+ actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
1735
+ model_id TEXT,
1736
+ file_path TEXT,
1737
+ change_type TEXT CHECK(change_type IN ('create', 'edit', 'delete')),
1738
+ rules_in_effect TEXT,
1739
+ approval_status TEXT CHECK(approval_status IN ('auto_approved', 'human_approved', 'pending', 'denied')),
1740
+ evidence TEXT,
1741
+ metadata TEXT,
1742
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
1743
+ );
1744
+ INSERT INTO audit_log_new SELECT * FROM audit_log;
1745
+ DROP TABLE audit_log;
1746
+ ALTER TABLE audit_log_new RENAME TO audit_log;
1747
+ CREATE INDEX IF NOT EXISTS idx_al_session ON audit_log(session_id);
1748
+ CREATE INDEX IF NOT EXISTS idx_al_file ON audit_log(file_path);
1749
+ CREATE INDEX IF NOT EXISTS idx_al_event ON audit_log(event_type);
1750
+ CREATE INDEX IF NOT EXISTS idx_al_timestamp ON audit_log(timestamp DESC);
1751
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_audit_rule_promoted
1752
+ ON audit_log (event_type, json_extract(metadata, '$.prompt_hash'))
1753
+ WHERE event_type = 'rule_promoted';
1754
+ `);
1755
+ db.exec("COMMIT");
1756
+ } catch (err) {
1757
+ db.exec("ROLLBACK");
1758
+ throw err;
1759
+ } finally {
1760
+ db.pragma("foreign_keys = ON");
1761
+ }
1762
+ }
1763
+ function initMemorySchema(db) {
1764
+ db.exec(`
1765
+ -- Sessions table (linked to Claude Code session IDs)
1766
+ CREATE TABLE IF NOT EXISTS sessions (
1767
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1768
+ session_id TEXT UNIQUE NOT NULL,
1769
+ project TEXT NOT NULL DEFAULT 'my-project',
1770
+ git_branch TEXT,
1771
+ started_at TEXT NOT NULL,
1772
+ started_at_epoch INTEGER NOT NULL,
1773
+ ended_at TEXT,
1774
+ ended_at_epoch INTEGER,
1775
+ status TEXT CHECK(status IN ('active', 'completed', 'abandoned')) NOT NULL DEFAULT 'active',
1776
+ plan_file TEXT,
1777
+ plan_phase TEXT,
1778
+ task_id TEXT
1779
+ );
1780
+
1781
+ CREATE INDEX IF NOT EXISTS idx_sessions_session_id ON sessions(session_id);
1782
+ CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at_epoch DESC);
1783
+ CREATE INDEX IF NOT EXISTS idx_sessions_task_id ON sessions(task_id);
1784
+
1785
+ -- Observations table (structured knowledge from tool usage)
1786
+ CREATE TABLE IF NOT EXISTS observations (
1787
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1788
+ session_id TEXT NOT NULL,
1789
+ type TEXT NOT NULL CHECK(type IN (
1790
+ 'decision', 'bugfix', 'feature', 'refactor', 'discovery',
1791
+ 'cr_violation', 'vr_check', 'pattern_compliance', 'failed_attempt',
1792
+ 'file_change', 'incident_near_miss'
1793
+ )),
1794
+ title TEXT NOT NULL,
1795
+ detail TEXT,
1796
+ files_involved TEXT DEFAULT '[]',
1797
+ plan_item TEXT,
1798
+ cr_rule TEXT,
1799
+ vr_type TEXT,
1800
+ evidence TEXT,
1801
+ importance INTEGER NOT NULL DEFAULT 3 CHECK(importance BETWEEN 1 AND 5),
1802
+ recurrence_count INTEGER NOT NULL DEFAULT 1,
1803
+ original_tokens INTEGER DEFAULT 0,
1804
+ -- B-10/F-08/N-03: the SOURCE-row provenance flag. The renderer refuses any
1805
+ -- row whose origin is not 'local' BEFORE it computes a path, mints a
1806
+ -- credential, or takes a snapshot. A Slice-5 synced memory arrives here with
1807
+ -- origin='team' and must never reach disk without CR-55's gates.
1808
+ origin TEXT NOT NULL DEFAULT 'local',
1809
+ created_at TEXT NOT NULL,
1810
+ created_at_epoch INTEGER NOT NULL,
1811
+ FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
1812
+ );
1813
+
1814
+ CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
1815
+ CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
1816
+ CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch DESC);
1817
+ CREATE INDEX IF NOT EXISTS idx_observations_plan_item ON observations(plan_item);
1818
+ CREATE INDEX IF NOT EXISTS idx_observations_cr_rule ON observations(cr_rule);
1819
+ CREATE INDEX IF NOT EXISTS idx_observations_importance ON observations(importance DESC);
1820
+ `);
1821
+ try {
1822
+ db.exec(`
1823
+ CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
1824
+ title, detail, evidence,
1825
+ content='observations',
1826
+ content_rowid='id'
1827
+ );
1828
+ `);
1829
+ } catch (_e) {
1830
+ }
1831
+ db.exec(`
1832
+ CREATE TRIGGER IF NOT EXISTS observations_ai AFTER INSERT ON observations BEGIN
1833
+ INSERT INTO observations_fts(rowid, title, detail, evidence)
1834
+ VALUES (new.id, new.title, new.detail, new.evidence);
1835
+ END;
1836
+
1837
+ CREATE TRIGGER IF NOT EXISTS observations_ad AFTER DELETE ON observations BEGIN
1838
+ INSERT INTO observations_fts(observations_fts, rowid, title, detail, evidence)
1839
+ VALUES ('delete', old.id, old.title, old.detail, old.evidence);
1840
+ END;
1841
+
1842
+ CREATE TRIGGER IF NOT EXISTS observations_au AFTER UPDATE ON observations BEGIN
1843
+ INSERT INTO observations_fts(observations_fts, rowid, title, detail, evidence)
1844
+ VALUES ('delete', old.id, old.title, old.detail, old.evidence);
1845
+ INSERT INTO observations_fts(rowid, title, detail, evidence)
1846
+ VALUES (new.id, new.title, new.detail, new.evidence);
1847
+ END;
1848
+ `);
1849
+ db.exec(`
1850
+ CREATE TABLE IF NOT EXISTS session_summaries (
1851
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1852
+ session_id TEXT NOT NULL,
1853
+ request TEXT,
1854
+ investigated TEXT,
1855
+ decisions TEXT,
1856
+ completed TEXT,
1857
+ failed_attempts TEXT,
1858
+ next_steps TEXT,
1859
+ files_created TEXT DEFAULT '[]',
1860
+ files_modified TEXT DEFAULT '[]',
1861
+ verification_results TEXT DEFAULT '{}',
1862
+ plan_progress TEXT DEFAULT '{}',
1863
+ created_at TEXT NOT NULL,
1864
+ created_at_epoch INTEGER NOT NULL,
1865
+ FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
1866
+ );
1867
+
1868
+ CREATE INDEX IF NOT EXISTS idx_summaries_session ON session_summaries(session_id);
1869
+ `);
1870
+ db.exec(`
1871
+ CREATE TABLE IF NOT EXISTS user_prompts (
1872
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1873
+ session_id TEXT NOT NULL,
1874
+ prompt_text TEXT NOT NULL,
1875
+ prompt_number INTEGER NOT NULL DEFAULT 1,
1876
+ created_at TEXT NOT NULL,
1877
+ created_at_epoch INTEGER NOT NULL,
1878
+ FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
1879
+ );
1880
+ `);
1881
+ try {
1882
+ db.exec(`
1883
+ CREATE VIRTUAL TABLE IF NOT EXISTS user_prompts_fts USING fts5(
1884
+ prompt_text,
1885
+ content='user_prompts',
1886
+ content_rowid='id'
1887
+ );
1888
+ `);
1889
+ } catch (_e) {
1890
+ }
1891
+ db.exec(`
1892
+ CREATE TRIGGER IF NOT EXISTS prompts_ai AFTER INSERT ON user_prompts BEGIN
1893
+ INSERT INTO user_prompts_fts(rowid, prompt_text) VALUES (new.id, new.prompt_text);
1894
+ END;
1895
+
1896
+ CREATE TRIGGER IF NOT EXISTS prompts_ad AFTER DELETE ON user_prompts BEGIN
1897
+ INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
1898
+ VALUES ('delete', old.id, old.prompt_text);
1899
+ END;
1900
+ `);
1901
+ db.exec(`
1902
+ CREATE TABLE IF NOT EXISTS memory_meta (
1903
+ key TEXT PRIMARY KEY,
1904
+ value TEXT NOT NULL
1905
+ );
1906
+ `);
1907
+ db.exec(`
1908
+ CREATE TABLE IF NOT EXISTS memory_usage (
1909
+ source TEXT NOT NULL,
1910
+ record_id INTEGER NOT NULL,
1911
+ hit_count INTEGER NOT NULL DEFAULT 0,
1912
+ hits_windowed REAL NOT NULL DEFAULT 0,
1913
+ last_hit_epoch INTEGER,
1914
+ last_reweight_epoch INTEGER,
1915
+ PRIMARY KEY (source, record_id)
1916
+ );
1917
+
1918
+ CREATE TABLE IF NOT EXISTS memory_usage_sessions (
1919
+ source TEXT NOT NULL,
1920
+ record_id INTEGER NOT NULL,
1921
+ session_id TEXT NOT NULL,
1922
+ PRIMARY KEY (source, record_id, session_id)
1923
+ );
1924
+
1925
+ CREATE INDEX IF NOT EXISTS idx_memory_usage_hits
1926
+ ON memory_usage(source, hits_windowed DESC);
1927
+ `);
1928
+ db.exec(`
1929
+ CREATE TABLE IF NOT EXISTS conversation_turns (
1930
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1931
+ session_id TEXT NOT NULL,
1932
+ turn_number INTEGER NOT NULL,
1933
+ user_prompt TEXT NOT NULL,
1934
+ assistant_response TEXT,
1935
+ tool_calls_json TEXT,
1936
+ tool_call_count INTEGER DEFAULT 0,
1937
+ model_used TEXT,
1938
+ duration_ms INTEGER,
1939
+ prompt_tokens INTEGER,
1940
+ response_tokens INTEGER,
1941
+ created_at TEXT DEFAULT (datetime('now')),
1942
+ created_at_epoch INTEGER DEFAULT (unixepoch()),
1943
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
1944
+ );
1945
+
1946
+ CREATE INDEX IF NOT EXISTS idx_ct_session ON conversation_turns(session_id);
1947
+ CREATE INDEX IF NOT EXISTS idx_ct_created ON conversation_turns(created_at DESC);
1948
+ CREATE INDEX IF NOT EXISTS idx_ct_turn ON conversation_turns(session_id, turn_number);
1949
+ `);
1950
+ db.exec(`
1951
+ CREATE TABLE IF NOT EXISTS tool_call_details (
1952
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1953
+ session_id TEXT NOT NULL,
1954
+ turn_number INTEGER NOT NULL,
1955
+ tool_name TEXT NOT NULL,
1956
+ tool_input_summary TEXT,
1957
+ tool_input_size INTEGER,
1958
+ tool_output_size INTEGER,
1959
+ tool_success INTEGER DEFAULT 1,
1960
+ duration_ms INTEGER,
1961
+ files_involved TEXT,
1962
+ created_at TEXT DEFAULT (datetime('now')),
1963
+ created_at_epoch INTEGER DEFAULT (unixepoch()),
1964
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
1965
+ );
1966
+
1967
+ CREATE INDEX IF NOT EXISTS idx_tcd_session ON tool_call_details(session_id);
1968
+ CREATE INDEX IF NOT EXISTS idx_tcd_tool ON tool_call_details(tool_name);
1969
+ CREATE INDEX IF NOT EXISTS idx_tcd_created ON tool_call_details(created_at DESC);
1970
+ `);
1971
+ try {
1972
+ db.exec(`
1973
+ CREATE VIRTUAL TABLE IF NOT EXISTS conversation_turns_fts USING fts5(
1974
+ user_prompt,
1975
+ assistant_response,
1976
+ content=conversation_turns,
1977
+ content_rowid=id
1978
+ );
1979
+ `);
1980
+ } catch (_e) {
1981
+ }
1982
+ db.exec(`
1983
+ CREATE TRIGGER IF NOT EXISTS ct_fts_insert AFTER INSERT ON conversation_turns BEGIN
1984
+ INSERT INTO conversation_turns_fts(rowid, user_prompt, assistant_response)
1985
+ VALUES (new.id, new.user_prompt, new.assistant_response);
1986
+ END;
1987
+
1988
+ CREATE TRIGGER IF NOT EXISTS ct_fts_delete AFTER DELETE ON conversation_turns BEGIN
1989
+ INSERT INTO conversation_turns_fts(conversation_turns_fts, rowid, user_prompt, assistant_response)
1990
+ VALUES ('delete', old.id, old.user_prompt, old.assistant_response);
1991
+ END;
1992
+
1993
+ CREATE TRIGGER IF NOT EXISTS ct_fts_update AFTER UPDATE ON conversation_turns BEGIN
1994
+ INSERT INTO conversation_turns_fts(conversation_turns_fts, rowid, user_prompt, assistant_response)
1995
+ VALUES ('delete', old.id, old.user_prompt, old.assistant_response);
1996
+ INSERT INTO conversation_turns_fts(rowid, user_prompt, assistant_response)
1997
+ VALUES (new.id, new.user_prompt, new.assistant_response);
1998
+ END;
1999
+ `);
2000
+ db.exec(`
2001
+ CREATE TABLE IF NOT EXISTS session_quality_scores (
2002
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2003
+ session_id TEXT NOT NULL UNIQUE,
2004
+ project TEXT NOT NULL DEFAULT 'my-project',
2005
+ score INTEGER NOT NULL DEFAULT 100,
2006
+ security_score INTEGER NOT NULL DEFAULT 100,
2007
+ architecture_score INTEGER NOT NULL DEFAULT 100,
2008
+ coupling_score INTEGER NOT NULL DEFAULT 100,
2009
+ test_score INTEGER NOT NULL DEFAULT 100,
2010
+ rule_compliance_score INTEGER NOT NULL DEFAULT 100,
2011
+ observations_total INTEGER NOT NULL DEFAULT 0,
2012
+ bugs_found INTEGER NOT NULL DEFAULT 0,
2013
+ bugs_fixed INTEGER NOT NULL DEFAULT 0,
2014
+ vr_checks_passed INTEGER NOT NULL DEFAULT 0,
2015
+ vr_checks_failed INTEGER NOT NULL DEFAULT 0,
2016
+ incidents_triggered INTEGER NOT NULL DEFAULT 0,
2017
+ created_at TEXT DEFAULT (datetime('now')),
2018
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
2019
+ );
2020
+ CREATE INDEX IF NOT EXISTS idx_sqs_session ON session_quality_scores(session_id);
2021
+ CREATE INDEX IF NOT EXISTS idx_sqs_project ON session_quality_scores(project);
2022
+ `);
2023
+ db.exec(`
2024
+ CREATE TABLE IF NOT EXISTS session_costs (
2025
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2026
+ session_id TEXT NOT NULL UNIQUE,
2027
+ project TEXT NOT NULL DEFAULT 'my-project',
2028
+ input_tokens INTEGER NOT NULL DEFAULT 0,
2029
+ output_tokens INTEGER NOT NULL DEFAULT 0,
2030
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
2031
+ cache_write_tokens INTEGER NOT NULL DEFAULT 0,
2032
+ total_tokens INTEGER NOT NULL DEFAULT 0,
2033
+ estimated_cost_usd REAL NOT NULL DEFAULT 0.0,
2034
+ model TEXT,
2035
+ duration_minutes REAL NOT NULL DEFAULT 0.0,
2036
+ tool_calls INTEGER NOT NULL DEFAULT 0,
2037
+ created_at TEXT DEFAULT (datetime('now')),
2038
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
2039
+ );
2040
+ CREATE INDEX IF NOT EXISTS idx_sc_session ON session_costs(session_id);
2041
+ `);
2042
+ db.exec(`
2043
+ CREATE TABLE IF NOT EXISTS feature_costs (
2044
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2045
+ feature_key TEXT NOT NULL,
2046
+ session_id TEXT NOT NULL,
2047
+ tokens_used INTEGER NOT NULL DEFAULT 0,
2048
+ estimated_cost_usd REAL NOT NULL DEFAULT 0.0,
2049
+ commit_hash TEXT,
2050
+ created_at TEXT DEFAULT (datetime('now')),
2051
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
2052
+ );
2053
+ CREATE INDEX IF NOT EXISTS idx_fc_feature ON feature_costs(feature_key);
2054
+ CREATE INDEX IF NOT EXISTS idx_fc_session ON feature_costs(session_id);
2055
+ `);
2056
+ db.exec(`
2057
+ CREATE TABLE IF NOT EXISTS prompt_outcomes (
2058
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2059
+ session_id TEXT NOT NULL,
2060
+ prompt_hash TEXT NOT NULL,
2061
+ prompt_text TEXT NOT NULL,
2062
+ prompt_category TEXT NOT NULL DEFAULT 'feature',
2063
+ word_count INTEGER NOT NULL DEFAULT 0,
2064
+ outcome TEXT NOT NULL DEFAULT 'success' CHECK(outcome IN ('success', 'partial', 'failure', 'abandoned')),
2065
+ corrections_needed INTEGER NOT NULL DEFAULT 0,
2066
+ follow_up_prompts INTEGER NOT NULL DEFAULT 0,
2067
+ created_at TEXT DEFAULT (datetime('now')),
2068
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
2069
+ );
2070
+ CREATE INDEX IF NOT EXISTS idx_po_session ON prompt_outcomes(session_id);
2071
+ CREATE INDEX IF NOT EXISTS idx_po_category ON prompt_outcomes(prompt_category);
2072
+ `);
2073
+ db.exec(`
2074
+ CREATE TABLE IF NOT EXISTS audit_log (
2075
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2076
+ session_id TEXT NOT NULL,
2077
+ timestamp TEXT DEFAULT (datetime('now')),
2078
+ event_type TEXT NOT NULL CHECK(event_type IN (
2079
+ 'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
2080
+ 'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
2081
+ 'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
2082
+ 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
2083
+ )),
2084
+ actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
2085
+ model_id TEXT,
2086
+ file_path TEXT,
2087
+ change_type TEXT CHECK(change_type IN ('create', 'edit', 'delete')),
2088
+ rules_in_effect TEXT,
2089
+ approval_status TEXT CHECK(approval_status IN ('auto_approved', 'human_approved', 'pending', 'denied')),
2090
+ evidence TEXT,
2091
+ metadata TEXT,
2092
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
2093
+ );
2094
+ CREATE INDEX IF NOT EXISTS idx_al_session ON audit_log(session_id);
2095
+ CREATE INDEX IF NOT EXISTS idx_al_file ON audit_log(file_path);
2096
+ CREATE INDEX IF NOT EXISTS idx_al_event ON audit_log(event_type);
2097
+ CREATE INDEX IF NOT EXISTS idx_al_timestamp ON audit_log(timestamp DESC);
2098
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_audit_rule_promoted
2099
+ ON audit_log (event_type, json_extract(metadata, '$.prompt_hash'))
2100
+ WHERE event_type = 'rule_promoted';
2101
+ `);
2102
+ db.exec(`
2103
+ CREATE TABLE IF NOT EXISTS hook_health (
2104
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2105
+ hook TEXT NOT NULL,
2106
+ error TEXT NOT NULL,
2107
+ context_json TEXT,
2108
+ occurred_at TEXT NOT NULL DEFAULT (datetime('now'))
2109
+ );
2110
+ CREATE INDEX IF NOT EXISTS idx_hook_health_hook ON hook_health(hook);
2111
+ CREATE INDEX IF NOT EXISTS idx_hook_health_time ON hook_health(occurred_at DESC);
2112
+ `);
2113
+ db.exec(`
2114
+ CREATE TABLE IF NOT EXISTS prompt_outcomes_signal_blacklist (
2115
+ signal TEXT PRIMARY KEY,
2116
+ dismissal_count INTEGER NOT NULL DEFAULT 0,
2117
+ first_dismissed_at TEXT DEFAULT (datetime('now')),
2118
+ last_dismissed_at TEXT DEFAULT (datetime('now'))
2119
+ );
2120
+ CREATE INDEX IF NOT EXISTS idx_psb_count
2121
+ ON prompt_outcomes_signal_blacklist(dismissal_count DESC);
2122
+ `);
2123
+ migrateAuditLogCheckExtension(db);
2124
+ db.exec(`
2125
+ CREATE TABLE IF NOT EXISTS memory_files (
2126
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2127
+ rel_path TEXT NOT NULL UNIQUE COLLATE NOCASE,
2128
+ name TEXT,
2129
+ raw TEXT NOT NULL,
2130
+ frontmatter_json TEXT,
2131
+ body TEXT NOT NULL,
2132
+ content_hash TEXT NOT NULL,
2133
+ ingest_schema_version INTEGER NOT NULL DEFAULT 1,
2134
+ massu_authored INTEGER NOT NULL DEFAULT 0,
2135
+ massu_render_mac TEXT,
2136
+ adopted_human_at_epoch INTEGER,
2137
+ tombstoned_at_epoch INTEGER,
2138
+ origin TEXT NOT NULL DEFAULT 'local',
2139
+ render_suppressed INTEGER NOT NULL DEFAULT 0,
2140
+ observation_id INTEGER,
2141
+ synced_at_epoch INTEGER,
2142
+ expired_at_epoch INTEGER
2143
+ );
2144
+ CREATE INDEX IF NOT EXISTS idx_mf_hash ON memory_files(content_hash);
2145
+ CREATE INDEX IF NOT EXISTS idx_mf_expired ON memory_files(expired_at_epoch);
2146
+ CREATE INDEX IF NOT EXISTS idx_mf_obs ON memory_files(observation_id);
2147
+ `);
2148
+ db.exec(`
2149
+ CREATE TABLE IF NOT EXISTS validation_results (
2150
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2151
+ session_id TEXT NOT NULL,
2152
+ file_path TEXT NOT NULL,
2153
+ validation_type TEXT NOT NULL,
2154
+ passed INTEGER NOT NULL DEFAULT 1,
2155
+ details TEXT,
2156
+ rules_violated TEXT,
2157
+ created_at TEXT DEFAULT (datetime('now')),
2158
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
2159
+ );
2160
+ CREATE INDEX IF NOT EXISTS idx_vr_session ON validation_results(session_id);
2161
+ CREATE INDEX IF NOT EXISTS idx_vr_file ON validation_results(file_path);
2162
+ `);
2163
+ db.exec(`
2164
+ CREATE TABLE IF NOT EXISTS architecture_decisions (
2165
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2166
+ session_id TEXT NOT NULL,
2167
+ title TEXT NOT NULL,
2168
+ context TEXT,
2169
+ decision TEXT NOT NULL,
2170
+ status TEXT NOT NULL DEFAULT 'accepted' CHECK(status IN ('accepted', 'superseded', 'deprecated')),
2171
+ alternatives TEXT,
2172
+ consequences TEXT,
2173
+ affected_files TEXT,
2174
+ commit_hash TEXT,
2175
+ created_at TEXT DEFAULT (datetime('now')),
2176
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
2177
+ );
2178
+ CREATE INDEX IF NOT EXISTS idx_ad_session ON architecture_decisions(session_id);
2179
+ CREATE INDEX IF NOT EXISTS idx_ad_status ON architecture_decisions(status);
2180
+ `);
2181
+ db.exec(`
2182
+ CREATE TABLE IF NOT EXISTS security_scores (
2183
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2184
+ session_id TEXT NOT NULL,
2185
+ file_path TEXT NOT NULL,
2186
+ risk_score INTEGER NOT NULL DEFAULT 0,
2187
+ findings TEXT,
2188
+ created_at TEXT DEFAULT (datetime('now')),
2189
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
2190
+ );
2191
+ CREATE INDEX IF NOT EXISTS idx_ss_session ON security_scores(session_id);
2192
+ CREATE INDEX IF NOT EXISTS idx_ss_file ON security_scores(file_path);
2193
+ `);
2194
+ db.exec(`
2195
+ CREATE TABLE IF NOT EXISTS dependency_assessments (
2196
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2197
+ package_name TEXT NOT NULL,
2198
+ version TEXT,
2199
+ risk_score INTEGER NOT NULL DEFAULT 0,
2200
+ vulnerabilities INTEGER NOT NULL DEFAULT 0,
2201
+ last_publish_days INTEGER,
2202
+ weekly_downloads INTEGER,
2203
+ license TEXT,
2204
+ bundle_size_kb INTEGER,
2205
+ previous_removals INTEGER NOT NULL DEFAULT 0,
2206
+ assessed_at TEXT DEFAULT (datetime('now'))
2207
+ );
2208
+ CREATE INDEX IF NOT EXISTS idx_da_package ON dependency_assessments(package_name);
2209
+ `);
2210
+ db.exec(`
2211
+ CREATE TABLE IF NOT EXISTS developer_expertise (
2212
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2213
+ developer_id TEXT NOT NULL,
2214
+ module TEXT NOT NULL,
2215
+ session_count INTEGER NOT NULL DEFAULT 0,
2216
+ observation_count INTEGER NOT NULL DEFAULT 0,
2217
+ expertise_score INTEGER NOT NULL DEFAULT 0,
2218
+ last_active TEXT DEFAULT (datetime('now')),
2219
+ UNIQUE(developer_id, module)
2220
+ );
2221
+ CREATE INDEX IF NOT EXISTS idx_de_developer ON developer_expertise(developer_id);
2222
+ CREATE INDEX IF NOT EXISTS idx_de_module ON developer_expertise(module);
2223
+ `);
2224
+ db.exec(`
2225
+ CREATE TABLE IF NOT EXISTS shared_observations (
2226
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2227
+ original_id INTEGER,
2228
+ developer_id TEXT NOT NULL,
2229
+ project TEXT NOT NULL,
2230
+ observation_type TEXT NOT NULL,
2231
+ summary TEXT NOT NULL,
2232
+ file_path TEXT,
2233
+ module TEXT,
2234
+ severity INTEGER NOT NULL DEFAULT 3,
2235
+ is_shared INTEGER NOT NULL DEFAULT 0,
2236
+ shared_at TEXT,
2237
+ created_at TEXT DEFAULT (datetime('now'))
2238
+ );
2239
+ CREATE INDEX IF NOT EXISTS idx_so_developer ON shared_observations(developer_id);
2240
+ CREATE INDEX IF NOT EXISTS idx_so_file ON shared_observations(file_path);
2241
+ CREATE INDEX IF NOT EXISTS idx_so_module ON shared_observations(module);
2242
+ `);
2243
+ db.exec(`
2244
+ CREATE TABLE IF NOT EXISTS team_promotion_outbound (
2245
+ prompt_hash TEXT PRIMARY KEY,
2246
+ destination TEXT NOT NULL,
2247
+ draft_text TEXT NOT NULL,
2248
+ score REAL,
2249
+ signals_json TEXT NOT NULL DEFAULT '[]',
2250
+ content_hash TEXT NOT NULL,
2251
+ -- PA3-004 (Phase 3 Stream A): hardened-destination publish carries the
2252
+ -- publisher's review attestation so the server CHECK (hardened rows need a
2253
+ -- review_attestation) is satisfiable. hardened=0 for the Phase-2 rows.
2254
+ hardened INTEGER NOT NULL DEFAULT 0,
2255
+ review_attestation_json TEXT,
2256
+ created_at TEXT DEFAULT (datetime('now'))
2257
+ );
2258
+ CREATE TABLE IF NOT EXISTS team_revocation_outbound (
2259
+ prompt_hash TEXT PRIMARY KEY,
2260
+ created_at TEXT DEFAULT (datetime('now'))
2261
+ );
2262
+ `);
2263
+ db.exec(`
2264
+ CREATE TABLE IF NOT EXISTS rule_promotion_events_outbound (
2265
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2266
+ prompt_hash TEXT NOT NULL,
2267
+ event_type TEXT NOT NULL,
2268
+ metadata_json TEXT NOT NULL DEFAULT '{}',
2269
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
2270
+ );
2271
+ CREATE INDEX IF NOT EXISTS idx_rpe_outbound_created ON rule_promotion_events_outbound(created_at);
2272
+ `);
2273
+ db.exec(`
2274
+ CREATE TABLE IF NOT EXISTS analytics_events (
2275
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2276
+ event_type TEXT NOT NULL,
2277
+ event_data TEXT NOT NULL DEFAULT '{}',
2278
+ created_at TEXT DEFAULT (datetime('now'))
2279
+ );
2280
+ CREATE INDEX IF NOT EXISTS idx_analytics_events_type ON analytics_events(event_type);
2281
+ `);
2282
+ db.exec(`
2283
+ CREATE TABLE IF NOT EXISTS knowledge_conflicts (
2284
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2285
+ file_path TEXT NOT NULL,
2286
+ developer_a TEXT NOT NULL,
2287
+ developer_b TEXT NOT NULL,
2288
+ conflict_type TEXT NOT NULL DEFAULT 'concurrent_edit',
2289
+ resolved INTEGER NOT NULL DEFAULT 0,
2290
+ detected_at TEXT DEFAULT (datetime('now'))
2291
+ );
2292
+ CREATE INDEX IF NOT EXISTS idx_kc_file ON knowledge_conflicts(file_path);
2293
+ `);
2294
+ db.exec(`
2295
+ CREATE TABLE IF NOT EXISTS feature_health (
2296
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2297
+ feature_key TEXT NOT NULL UNIQUE,
2298
+ health_score INTEGER NOT NULL DEFAULT 100,
2299
+ tests_passing INTEGER NOT NULL DEFAULT 0,
2300
+ tests_failing INTEGER NOT NULL DEFAULT 0,
2301
+ test_coverage_pct REAL,
2302
+ modifications_since_test INTEGER NOT NULL DEFAULT 0,
2303
+ last_modified TEXT,
2304
+ last_tested TEXT,
2305
+ created_at TEXT DEFAULT (datetime('now'))
2306
+ );
2307
+ CREATE INDEX IF NOT EXISTS idx_fh_feature ON feature_health(feature_key);
2308
+ CREATE INDEX IF NOT EXISTS idx_fh_health ON feature_health(health_score);
2309
+ `);
2310
+ db.exec(`
2311
+ CREATE TABLE IF NOT EXISTS tool_cost_events (
2312
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2313
+ session_id TEXT NOT NULL,
2314
+ tool_name TEXT NOT NULL,
2315
+ estimated_input_tokens INTEGER DEFAULT 0,
2316
+ estimated_output_tokens INTEGER DEFAULT 0,
2317
+ model TEXT DEFAULT '',
2318
+ created_at TEXT DEFAULT (datetime('now'))
2319
+ );
2320
+ CREATE INDEX IF NOT EXISTS idx_tce_session ON tool_cost_events(session_id);
2321
+ CREATE INDEX IF NOT EXISTS idx_tce_tool ON tool_cost_events(tool_name);
2322
+ CREATE INDEX IF NOT EXISTS idx_tce_created ON tool_cost_events(created_at DESC);
2323
+ `);
2324
+ db.exec(`
2325
+ CREATE TABLE IF NOT EXISTS quality_events (
2326
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2327
+ session_id TEXT NOT NULL,
2328
+ event_type TEXT NOT NULL,
2329
+ tool_name TEXT NOT NULL,
2330
+ details TEXT DEFAULT '',
2331
+ created_at TEXT DEFAULT (datetime('now'))
2332
+ );
2333
+ CREATE INDEX IF NOT EXISTS idx_qe_session ON quality_events(session_id);
2334
+ CREATE INDEX IF NOT EXISTS idx_qe_event_type ON quality_events(event_type);
2335
+ CREATE INDEX IF NOT EXISTS idx_qe_created ON quality_events(created_at DESC);
2336
+ `);
2337
+ db.exec(`
2338
+ CREATE TABLE IF NOT EXISTS pending_sync (
2339
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2340
+ payload TEXT NOT NULL,
2341
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2342
+ retry_count INTEGER NOT NULL DEFAULT 0,
2343
+ last_error TEXT
2344
+ );
2345
+ CREATE INDEX IF NOT EXISTS idx_pending_sync_created ON pending_sync(created_at ASC);
2346
+ `);
2347
+ db.exec(`
2348
+ CREATE TABLE IF NOT EXISTS license_cache (
2349
+ api_key_hash TEXT PRIMARY KEY,
2350
+ tier TEXT NOT NULL,
2351
+ valid_until TEXT NOT NULL,
2352
+ last_validated TEXT NOT NULL,
2353
+ features TEXT DEFAULT '[]'
2354
+ );
2355
+ `);
2356
+ const licenseCacheCols = db.prepare(`PRAGMA table_info(license_cache)`).all();
2357
+ if (!licenseCacheCols.some((c) => c.name === "signed_payload_json")) {
2358
+ db.exec(
2359
+ `ALTER TABLE license_cache ADD COLUMN signed_payload_json TEXT NOT NULL DEFAULT ''`
2360
+ );
2361
+ }
2362
+ const outboundCols = db.prepare(`PRAGMA table_info(team_promotion_outbound)`).all();
2363
+ if (!outboundCols.some((c) => c.name === "hardened")) {
2364
+ db.exec(`ALTER TABLE team_promotion_outbound ADD COLUMN hardened INTEGER NOT NULL DEFAULT 0`);
2365
+ }
2366
+ if (!outboundCols.some((c) => c.name === "review_attestation_json")) {
2367
+ db.exec(`ALTER TABLE team_promotion_outbound ADD COLUMN review_attestation_json TEXT`);
2368
+ }
2369
+ const sessionCols = db.prepare(`PRAGMA table_info(sessions)`).all();
2370
+ if (!sessionCols.some((c) => c.name === "consolidated_at")) {
2371
+ db.exec(`ALTER TABLE sessions ADD COLUMN consolidated_at TEXT`);
2372
+ }
2373
+ if (!sessionCols.some((c) => c.name === "consolidated_at_epoch")) {
2374
+ db.exec(`ALTER TABLE sessions ADD COLUMN consolidated_at_epoch INTEGER`);
2375
+ }
2376
+ if (!sessionCols.some((c) => c.name === "consolidated_status")) {
2377
+ db.exec(`ALTER TABLE sessions ADD COLUMN consolidated_status TEXT`);
2378
+ }
2379
+ const BITEMPORAL_COLUMNS = [
2380
+ { name: "valid_from", type: "TEXT" },
2381
+ { name: "valid_to", type: "TEXT" },
2382
+ { name: "ingested_at", type: "TEXT" },
2383
+ { name: "expired_at", type: "TEXT" },
2384
+ { name: "valid_from_epoch", type: "INTEGER" },
2385
+ { name: "valid_to_epoch", type: "INTEGER" },
2386
+ { name: "ingested_at_epoch", type: "INTEGER" },
2387
+ { name: "expired_at_epoch", type: "INTEGER" },
2388
+ { name: "superseded_by", type: "INTEGER" }
2389
+ ];
2390
+ for (const table of ["observations", "architecture_decisions"]) {
2391
+ const cols = db.prepare(`PRAGMA table_info(${table})`).all();
2392
+ for (const col of BITEMPORAL_COLUMNS) {
2393
+ if (!cols.some((c) => c.name === col.name)) {
2394
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${col.name} ${col.type}`);
2395
+ }
2396
+ }
2397
+ }
2398
+ db.exec(`
2399
+ UPDATE observations
2400
+ SET valid_from = created_at,
2401
+ ingested_at = created_at,
2402
+ valid_from_epoch = created_at_epoch,
2403
+ ingested_at_epoch = created_at_epoch
2404
+ WHERE valid_from_epoch IS NULL;
2405
+ `);
2406
+ db.exec(`
2407
+ UPDATE architecture_decisions
2408
+ SET valid_from = created_at,
2409
+ ingested_at = created_at,
2410
+ valid_from_epoch = CAST(strftime('%s', created_at) AS INTEGER),
2411
+ ingested_at_epoch = CAST(strftime('%s', created_at) AS INTEGER)
2412
+ WHERE valid_from_epoch IS NULL AND created_at IS NOT NULL;
2413
+ `);
2414
+ db.exec(`
2415
+ CREATE INDEX IF NOT EXISTS idx_observations_expired ON observations(expired_at_epoch);
2416
+ CREATE INDEX IF NOT EXISTS idx_ad_expired ON architecture_decisions(expired_at_epoch);
2417
+ `);
2418
+ db.exec(`
2419
+ CREATE TABLE IF NOT EXISTS failure_classes (
2420
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2421
+ name TEXT NOT NULL UNIQUE,
2422
+ description TEXT NOT NULL,
2423
+ diff_patterns TEXT NOT NULL DEFAULT '[]',
2424
+ file_patterns TEXT NOT NULL DEFAULT '[]',
2425
+ prompt_keywords TEXT NOT NULL DEFAULT '[]',
2426
+ incidents TEXT NOT NULL DEFAULT '[]',
2427
+ rules TEXT NOT NULL DEFAULT '[]',
2428
+ scanner_checks TEXT NOT NULL DEFAULT '[]',
2429
+ known_message TEXT NOT NULL DEFAULT '',
2430
+ needs_review INTEGER NOT NULL DEFAULT 0,
2431
+ created_at TEXT DEFAULT (datetime('now')),
2432
+ updated_at TEXT DEFAULT (datetime('now'))
2433
+ );
2434
+ CREATE INDEX IF NOT EXISTS idx_fc_name ON failure_classes(name);
2435
+ CREATE INDEX IF NOT EXISTS idx_fc_needs_review ON failure_classes(needs_review);
2436
+ `);
2437
+ db.exec(`
2438
+ CREATE TABLE IF NOT EXISTS observation_embeddings (
2439
+ observation_id INTEGER NOT NULL REFERENCES observations(id) ON DELETE CASCADE,
2440
+ chunk_ix INTEGER NOT NULL DEFAULT 0,
2441
+ model_id TEXT NOT NULL,
2442
+ dim INTEGER NOT NULL,
2443
+ vec BLOB NOT NULL,
2444
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2445
+ PRIMARY KEY (observation_id, model_id, dim, chunk_ix)
2446
+ );
2447
+ CREATE INDEX IF NOT EXISTS idx_obs_emb_model ON observation_embeddings(model_id);
2448
+ `);
2449
+ migrateObservationEmbeddingChunks(db);
2450
+ migrateMemoryFilesFor4B(db);
2451
+ }
2452
+ function enqueueSyncPayload(db, payload) {
2453
+ db.prepare("INSERT INTO pending_sync (payload) VALUES (?)").run(payload);
2454
+ }
2455
+ function getMemoryMeta(db, key) {
2456
+ const row = db.prepare("SELECT value FROM memory_meta WHERE key = ?").get(key);
2457
+ return row ? row.value : null;
2458
+ }
2459
+ function setMemoryMeta(db, key, value) {
2460
+ db.prepare("INSERT OR REPLACE INTO memory_meta (key, value) VALUES (?, ?)").run(key, value);
2461
+ }
2462
+ function upsertObservationEmbedding(db, observationId, vec, modelId, dim) {
2463
+ db.prepare(
2464
+ `INSERT OR REPLACE INTO observation_embeddings (observation_id, model_id, dim, vec, created_at)
2465
+ VALUES (?, ?, ?, ?, datetime('now'))`
2466
+ ).run(observationId, modelId, dim, float32ToBlob(vec));
2467
+ }
2468
+ function observationEmbedText(title, detail) {
2469
+ const t = (title ?? "").trim();
2470
+ const d = (detail ?? "").trim();
2471
+ return d ? `${t}. ${d}` : t;
2472
+ }
2473
+ async function embedMissingObservations(db, opts = {}) {
2474
+ return runEmbedSweep(
2475
+ db,
2476
+ {
2477
+ embeddingTable: "observation_embeddings",
2478
+ idCol: "observation_id",
2479
+ metaTable: "memory_meta",
2480
+ sourceLabel: "observation",
2481
+ // A-04: memory bodies are now stored WHOLE (up to ~14K chars). One vector per
2482
+ // memory would cover only its first ~1,000 chars.
2483
+ chunked: true,
2484
+ selectMissing: (d, cursor, model, batchSize) => {
2485
+ const rows = model ? d.prepare(
2486
+ `SELECT o.id AS id, o.title AS title, o.detail AS detail
2487
+ FROM observations o
2488
+ WHERE o.id > ?
2489
+ AND NOT EXISTS (
2490
+ SELECT 1 FROM observation_embeddings e
2491
+ WHERE e.observation_id = o.id AND e.model_id = ? AND e.dim = ?
2492
+ )
2493
+ ORDER BY o.id LIMIT ?`
2494
+ ).all(cursor, model.modelId, model.dim, batchSize) : d.prepare(
2495
+ `SELECT o.id AS id, o.title AS title, o.detail AS detail
2496
+ FROM observations o
2497
+ WHERE o.id > ?
2498
+ AND NOT EXISTS (
2499
+ SELECT 1 FROM observation_embeddings e WHERE e.observation_id = o.id
2500
+ )
2501
+ ORDER BY o.id LIMIT ?`
2502
+ ).all(cursor, batchSize);
2503
+ return rows.map((r) => ({ id: r.id, text: observationEmbedText(r.title, r.detail) }));
2504
+ }
2505
+ },
2506
+ opts
2507
+ );
2508
+ }
2509
+ function enqueueTeamPromotion(db, promo) {
2510
+ db.prepare(`
2511
+ INSERT OR REPLACE INTO team_promotion_outbound
2512
+ (prompt_hash, destination, draft_text, score, signals_json, content_hash, hardened, review_attestation_json, created_at)
2513
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
2514
+ `).run(
2515
+ promo.prompt_hash,
2516
+ promo.destination,
2517
+ promo.draft_text.slice(0, MAX_DRAFT_TEXT_LEN),
2518
+ promo.score ?? null,
2519
+ JSON.stringify(promo.signals ?? []),
2520
+ promo.content_hash,
2521
+ promo.hardened ? 1 : 0,
2522
+ promo.review_attestation !== void 0 ? JSON.stringify(promo.review_attestation) : null
2523
+ );
2524
+ }
2525
+ function drainTeamPromotions(db) {
2526
+ const rows = db.prepare(`
2527
+ SELECT prompt_hash, destination, draft_text, score, signals_json, content_hash, hardened, review_attestation_json
2528
+ FROM team_promotion_outbound ORDER BY created_at ASC LIMIT 1000
2529
+ `).all();
2530
+ if (rows.length === 0) return [];
2531
+ db.prepare("DELETE FROM team_promotion_outbound").run();
2532
+ return rows.map((r) => ({
2533
+ prompt_hash: r.prompt_hash,
2534
+ destination: r.destination,
2535
+ draft_text: r.draft_text,
2536
+ score: r.score ?? void 0,
2537
+ signals: safeJsonArray(r.signals_json),
2538
+ content_hash: r.content_hash,
2539
+ hardened: r.hardened === 1,
2540
+ review_attestation: r.review_attestation_json ? safeJsonParse(r.review_attestation_json) : void 0
2541
+ }));
2542
+ }
2543
+ function enqueueTeamRevocation(db, promptHash) {
2544
+ db.prepare(`
2545
+ INSERT OR REPLACE INTO team_revocation_outbound (prompt_hash, created_at)
2546
+ VALUES (?, datetime('now'))
2547
+ `).run(promptHash);
2548
+ }
2549
+ function drainTeamRevocations(db) {
2550
+ const rows = db.prepare(
2551
+ `SELECT prompt_hash FROM team_revocation_outbound ORDER BY created_at ASC LIMIT 1000`
2552
+ ).all();
2553
+ if (rows.length === 0) return [];
2554
+ db.prepare("DELETE FROM team_revocation_outbound").run();
2555
+ return rows.map((r) => r.prompt_hash);
2556
+ }
2557
+ function getRecurrenceCountForPromptHash(db, promptHash) {
2558
+ try {
2559
+ const row = db.prepare(`
2560
+ SELECT json_extract(metadata, '$.recurrence_count') AS rc
2561
+ FROM audit_log
2562
+ WHERE event_type = 'rule_promoted'
2563
+ AND json_extract(metadata, '$.prompt_hash') = ?
2564
+ LIMIT 1
2565
+ `).get(promptHash);
2566
+ if (!row || row.rc === null || row.rc === void 0) return null;
2567
+ return Number(row.rc);
2568
+ } catch {
2569
+ return null;
2570
+ }
2571
+ }
2572
+ function enqueueRulePromotionEvent(db, ev) {
2573
+ try {
2574
+ db.prepare(`
2575
+ INSERT INTO rule_promotion_events_outbound
2576
+ (prompt_hash, event_type, metadata_json, created_at)
2577
+ VALUES (?, ?, ?, ?)
2578
+ `).run(
2579
+ ev.prompt_hash,
2580
+ ev.event_type,
2581
+ JSON.stringify(ev.metadata ?? {}),
2582
+ ev.created_at
2583
+ );
2584
+ db.prepare(`
2585
+ DELETE FROM rule_promotion_events_outbound
2586
+ WHERE id NOT IN (
2587
+ SELECT id FROM rule_promotion_events_outbound ORDER BY id DESC LIMIT ?
2588
+ )
2589
+ `).run(FUNNEL_EVENT_OUTBOX_CAP);
2590
+ } catch {
2591
+ }
2592
+ }
2593
+ function drainRulePromotionEvents(db) {
2594
+ const rows = db.prepare(`
2595
+ SELECT id, prompt_hash, event_type, metadata_json, created_at
2596
+ FROM rule_promotion_events_outbound ORDER BY id ASC LIMIT ?
2597
+ `).all(FUNNEL_EVENT_DRAIN_LIMIT);
2598
+ if (rows.length === 0) return [];
2599
+ const maxId = rows[rows.length - 1].id;
2600
+ db.prepare("DELETE FROM rule_promotion_events_outbound WHERE id <= ?").run(maxId);
2601
+ return rows.map((r) => ({
2602
+ prompt_hash: r.prompt_hash,
2603
+ event_type: r.event_type,
2604
+ created_at: r.created_at,
2605
+ metadata: safeJsonParse(r.metadata_json) ?? {}
2606
+ }));
2607
+ }
2608
+ function recordTelemetry(db, eventType, data) {
2609
+ try {
2610
+ db.prepare(`
2611
+ INSERT INTO analytics_events (event_type, event_data, created_at)
2612
+ VALUES (?, ?, datetime('now'))
2613
+ `).run(eventType, JSON.stringify(data));
2614
+ } catch {
2615
+ }
2616
+ }
2617
+ function safeJsonArray(json) {
2618
+ try {
2619
+ const v = JSON.parse(json);
2620
+ return Array.isArray(v) ? v : [];
2621
+ } catch {
2622
+ return [];
2623
+ }
2624
+ }
2625
+ function safeJsonParse(json) {
2626
+ try {
2627
+ return JSON.parse(json);
2628
+ } catch {
2629
+ return void 0;
2630
+ }
2631
+ }
2632
+ function dequeuePendingSync(db, limit = 10) {
2633
+ const stale = db.prepare(
2634
+ "SELECT id, retry_count, last_error FROM pending_sync WHERE retry_count >= 10 LIMIT 10000"
2635
+ ).all();
2636
+ if (stale.length > 0) {
2637
+ const ids = stale.map((s) => s.id);
2638
+ db.prepare(`DELETE FROM pending_sync WHERE id IN (${ids.map(() => "?").join(",")})`).run(...ids);
2639
+ const lastErrors = [...new Set(stale.map((s) => s.last_error).filter(Boolean))];
2640
+ process.stderr.write(
2641
+ `[massu] WARNING: ${stale.length} cloud-sync queue item(s) discarded after 10+ retries. Likely cause: invalid API key or unreachable endpoint. Recent errors: ${lastErrors.slice(0, 3).join("; ") || "(none recorded)"}
2642
+ `
2643
+ );
2644
+ try {
2645
+ db.prepare(`
2646
+ INSERT INTO analytics_events (event_type, event_data, created_at)
2647
+ VALUES (?, ?, datetime('now'))
2648
+ `).run(
2649
+ "cloud_sync_giveup",
2650
+ JSON.stringify({
2651
+ discarded_count: stale.length,
2652
+ recent_errors: lastErrors.slice(0, 3)
2653
+ })
2654
+ );
2655
+ } catch {
2656
+ }
2657
+ }
2658
+ return db.prepare(
2659
+ "SELECT id, payload, retry_count FROM pending_sync ORDER BY created_at ASC LIMIT ?"
2660
+ ).all(limit);
2661
+ }
2662
+ function removePendingSync(db, id) {
2663
+ db.prepare("DELETE FROM pending_sync WHERE id = ?").run(id);
2664
+ }
2665
+ function incrementRetryCount(db, id, error) {
2666
+ db.prepare(
2667
+ "UPDATE pending_sync SET retry_count = retry_count + 1, last_error = ? WHERE id = ?"
2668
+ ).run(error, id);
2669
+ }
2670
+ function assignImportance(type, vrResult) {
2671
+ switch (type) {
2672
+ case "decision":
2673
+ case "failed_attempt":
2674
+ return 5;
2675
+ case "cr_violation":
2676
+ case "incident_near_miss":
2677
+ return 4;
2678
+ case "vr_check":
2679
+ return vrResult === "PASS" ? 2 : 4;
2680
+ case "pattern_compliance":
2681
+ return vrResult === "PASS" ? 2 : 4;
2682
+ case "feature":
2683
+ case "bugfix":
2684
+ return 3;
2685
+ case "refactor":
2686
+ return 2;
2687
+ case "file_change":
2688
+ case "discovery":
2689
+ return 1;
2690
+ default:
2691
+ return 3;
2692
+ }
2693
+ }
2694
+ function autoDetectTaskId(planFile) {
2695
+ if (!planFile) return null;
2696
+ const base = basename2(planFile);
2697
+ return base.replace(/\.md$/, "");
2698
+ }
2699
+ function createSession(db, sessionId, opts) {
2700
+ const now = /* @__PURE__ */ new Date();
2701
+ const taskId = autoDetectTaskId(opts?.planFile);
2702
+ db.prepare(`
2703
+ INSERT OR IGNORE INTO sessions (session_id, git_branch, plan_file, task_id, started_at, started_at_epoch)
2704
+ VALUES (?, ?, ?, ?, ?, ?)
2705
+ `).run(sessionId, opts?.branch ?? null, opts?.planFile ?? null, taskId, now.toISOString(), Math.floor(now.getTime() / 1e3));
2706
+ }
2707
+ function endSession(db, sessionId, status = "completed") {
2708
+ const now = /* @__PURE__ */ new Date();
2709
+ db.prepare(`
2710
+ UPDATE sessions SET status = ?, ended_at = ?, ended_at_epoch = ? WHERE session_id = ?
2711
+ `).run(status, now.toISOString(), Math.floor(now.getTime() / 1e3), sessionId);
2712
+ }
2713
+ function memoryTableHasTemporal(db, table) {
2714
+ let perDb = _temporalColCache.get(db);
2715
+ if (!perDb) {
2716
+ perDb = /* @__PURE__ */ new Map();
2717
+ _temporalColCache.set(db, perDb);
2718
+ }
2719
+ let has = perDb.get(table);
2720
+ if (has === void 0) {
2721
+ try {
2722
+ const cols = db.prepare(`PRAGMA table_info(${table})`).all();
2723
+ has = cols.some((c) => c.name === "valid_from_epoch");
2724
+ } catch {
2725
+ has = false;
2726
+ }
2727
+ perDb.set(table, has);
2728
+ }
2729
+ return has;
2730
+ }
2731
+ function addObservation(db, sessionId, type, title, detail, opts) {
2732
+ const now = /* @__PURE__ */ new Date();
2733
+ const importance = opts?.importance ?? assignImportance(type, opts?.evidence?.includes("PASS") ? "PASS" : void 0);
2734
+ const iso = now.toISOString();
2735
+ const epochSec = Math.floor(now.getTime() / 1e3);
2736
+ let result;
2737
+ if (memoryTableHasTemporal(db, "observations")) {
2738
+ result = db.prepare(`
2739
+ INSERT INTO observations (session_id, type, title, detail, files_involved, plan_item, cr_rule, vr_type, evidence, importance, original_tokens, created_at, created_at_epoch, valid_from, ingested_at, valid_from_epoch, ingested_at_epoch)
2740
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2741
+ `).run(
2742
+ sessionId,
2743
+ type,
2744
+ title,
2745
+ detail,
2746
+ JSON.stringify(opts?.filesInvolved ?? []),
2747
+ opts?.planItem ?? null,
2748
+ opts?.crRule ?? null,
2749
+ opts?.vrType ?? null,
2750
+ opts?.evidence ?? null,
2751
+ importance,
2752
+ opts?.originalTokens ?? 0,
2753
+ iso,
2754
+ epochSec,
2755
+ iso,
2756
+ iso,
2757
+ epochSec,
2758
+ epochSec
2759
+ );
2760
+ } else {
2761
+ result = db.prepare(`
2762
+ INSERT INTO observations (session_id, type, title, detail, files_involved, plan_item, cr_rule, vr_type, evidence, importance, original_tokens, created_at, created_at_epoch)
2763
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2764
+ `).run(
2765
+ sessionId,
2766
+ type,
2767
+ title,
2768
+ detail,
2769
+ JSON.stringify(opts?.filesInvolved ?? []),
2770
+ opts?.planItem ?? null,
2771
+ opts?.crRule ?? null,
2772
+ opts?.vrType ?? null,
2773
+ opts?.evidence ?? null,
2774
+ importance,
2775
+ opts?.originalTokens ?? 0,
2776
+ iso,
2777
+ epochSec
2778
+ );
2779
+ }
2780
+ return Number(result.lastInsertRowid);
2781
+ }
2782
+ function markRecordSuperseded(db, table, recordId, successorId, nowEpochSec = Math.floor(Date.now() / 1e3)) {
2783
+ const iso = new Date(nowEpochSec * 1e3).toISOString();
2784
+ const statusClause = table === "architecture_decisions" ? `, status = 'superseded'` : "";
2785
+ const res = db.prepare(
2786
+ `UPDATE ${table}
2787
+ SET valid_to = ?, expired_at = ?, valid_to_epoch = ?, expired_at_epoch = ?, superseded_by = ?${statusClause}
2788
+ WHERE id = ? AND expired_at IS NULL AND id != ?`
2789
+ ).run(iso, iso, nowEpochSec, nowEpochSec, successorId, recordId, successorId);
2790
+ return res.changes > 0;
2791
+ }
2792
+ function addSummary(db, sessionId, summary) {
2793
+ const now = /* @__PURE__ */ new Date();
2794
+ db.prepare(`
2795
+ INSERT INTO session_summaries (session_id, request, investigated, decisions, completed, failed_attempts, next_steps, files_created, files_modified, verification_results, plan_progress, created_at, created_at_epoch)
2796
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2797
+ `).run(
2798
+ sessionId,
2799
+ summary.request ?? null,
2800
+ summary.investigated ?? null,
2801
+ summary.decisions ?? null,
2802
+ summary.completed ?? null,
2803
+ summary.failedAttempts ?? null,
2804
+ summary.nextSteps ?? null,
2805
+ JSON.stringify(summary.filesCreated ?? []),
2806
+ JSON.stringify(summary.filesModified ?? []),
2807
+ JSON.stringify(summary.verificationResults ?? {}),
2808
+ JSON.stringify(summary.planProgress ?? {}),
2809
+ now.toISOString(),
2810
+ Math.floor(now.getTime() / 1e3)
2811
+ );
2812
+ }
2813
+ function addUserPrompt(db, sessionId, text, promptNumber) {
2814
+ const now = /* @__PURE__ */ new Date();
2815
+ db.prepare(`
2816
+ INSERT INTO user_prompts (session_id, prompt_text, prompt_number, created_at, created_at_epoch)
2817
+ VALUES (?, ?, ?, ?, ?)
2818
+ `).run(sessionId, text, promptNumber, now.toISOString(), Math.floor(now.getTime() / 1e3));
2819
+ }
2820
+ function searchObservations(db, query, opts) {
2821
+ const limit = opts?.limit ?? 20;
2822
+ let sql = `
2823
+ SELECT o.id, o.type, o.title, o.created_at, o.session_id, o.importance,
2824
+ rank
2825
+ FROM observations_fts
2826
+ JOIN observations o ON observations_fts.rowid = o.id
2827
+ WHERE observations_fts MATCH ?
2828
+ `;
2829
+ const params = [sanitizeFts5Query(query)];
2830
+ if (opts?.type) {
2831
+ sql += " AND o.type = ?";
2832
+ params.push(opts.type);
2833
+ }
2834
+ if (opts?.crRule) {
2835
+ sql += " AND o.cr_rule = ?";
2836
+ params.push(opts.crRule);
2837
+ }
2838
+ if (opts?.dateFrom) {
2839
+ sql += " AND o.created_at >= ?";
2840
+ params.push(opts.dateFrom);
2841
+ }
2842
+ sql += " ORDER BY rank LIMIT ?";
2843
+ params.push(limit);
2844
+ return db.prepare(sql).all(...params);
2845
+ }
2846
+ function getRecentObservations(db, limit = 20, sessionId) {
2847
+ if (sessionId) {
2848
+ return db.prepare(`
2849
+ SELECT id, type, title, detail, importance, created_at, session_id
2850
+ FROM observations WHERE session_id = ?
2851
+ ORDER BY created_at_epoch DESC LIMIT ?
2852
+ `).all(sessionId, limit);
2853
+ }
2854
+ return db.prepare(`
2855
+ SELECT id, type, title, detail, importance, created_at, session_id
2856
+ FROM observations
2857
+ ORDER BY created_at_epoch DESC LIMIT ?
2858
+ `).all(limit);
2859
+ }
2860
+ function getSessionSummaries(db, limit = 10) {
2861
+ return db.prepare(`
2862
+ SELECT session_id, request, completed, failed_attempts, plan_progress, created_at
2863
+ FROM session_summaries
2864
+ ORDER BY created_at_epoch DESC LIMIT ?
2865
+ `).all(limit);
2866
+ }
2867
+ function getSessionTimeline(db, sessionId) {
2868
+ const session = db.prepare("SELECT * FROM sessions WHERE session_id = ?").get(sessionId);
2869
+ const observations = db.prepare("SELECT * FROM observations WHERE session_id = ? ORDER BY created_at_epoch ASC").all(sessionId);
2870
+ const summary = db.prepare("SELECT * FROM session_summaries WHERE session_id = ? ORDER BY created_at_epoch DESC LIMIT 1").get(sessionId);
2871
+ const prompts = db.prepare("SELECT * FROM user_prompts WHERE session_id = ? ORDER BY prompt_number ASC").all(sessionId);
2872
+ return {
2873
+ session: session ?? null,
2874
+ observations,
2875
+ summary: summary ?? null,
2876
+ prompts
2877
+ };
2878
+ }
2879
+ function getFailedAttempts(db, query, limit = 20) {
2880
+ if (query) {
2881
+ return db.prepare(`
2882
+ SELECT o.id, o.title, o.detail, o.session_id, o.recurrence_count, o.created_at
2883
+ FROM observations_fts
2884
+ JOIN observations o ON observations_fts.rowid = o.id
2885
+ WHERE observations_fts MATCH ? AND o.type = 'failed_attempt'
2886
+ ORDER BY o.recurrence_count DESC, rank LIMIT ?
2887
+ `).all(sanitizeFts5Query(query), limit);
2888
+ }
2889
+ return db.prepare(`
2890
+ SELECT id, title, detail, session_id, recurrence_count, created_at
2891
+ FROM observations WHERE type = 'failed_attempt'
2892
+ ORDER BY recurrence_count DESC, created_at_epoch DESC LIMIT ?
2893
+ `).all(limit);
2894
+ }
2895
+ function getDecisionsAbout(db, query, limit = 20) {
2896
+ return db.prepare(`
2897
+ SELECT o.id, o.title, o.detail, o.session_id, o.created_at
2898
+ FROM observations_fts
2899
+ JOIN observations o ON observations_fts.rowid = o.id
2900
+ WHERE observations_fts MATCH ? AND o.type = 'decision'
2901
+ ORDER BY rank LIMIT ?
2902
+ `).all(sanitizeFts5Query(query), limit);
2903
+ }
2904
+ function armUsageCounter(db, nowEpochSec) {
2905
+ const existing = getMemoryMeta(db, USAGE_COUNTER_ARMED_KEY);
2906
+ if (existing) {
2907
+ const n = Number(existing);
2908
+ if (Number.isFinite(n) && n > 0) return n;
2909
+ }
2910
+ const now = nowEpochSec ?? Math.floor(Date.now() / 1e3);
2911
+ setMemoryMeta(db, USAGE_COUNTER_ARMED_KEY, String(now));
2912
+ return now;
2913
+ }
2914
+ function usageWarmupElapsed(db, warmupDays, nowEpochSec) {
2915
+ const armed = getMemoryMeta(db, USAGE_COUNTER_ARMED_KEY);
2916
+ if (!armed) return false;
2917
+ const armedEpoch = Number(armed);
2918
+ if (!Number.isFinite(armedEpoch) || armedEpoch <= 0) return false;
2919
+ const now = nowEpochSec ?? Math.floor(Date.now() / 1e3);
2920
+ return now - armedEpoch >= warmupDays * 86400;
2921
+ }
2922
+ function expireOldLowValueObservations(db, opts) {
2923
+ const now = opts.nowEpochSec ?? Math.floor(Date.now() / 1e3);
2924
+ if (!usageWarmupElapsed(db, opts.usageWarmupDays, now)) return 0;
2925
+ const cutoffEpoch = now - opts.retentionDays * 86400;
2926
+ const typePlaceholders = opts.protectedTypes.length ? opts.protectedTypes.map(() => "?").join(",") : "''";
2927
+ const iso = new Date(now * 1e3).toISOString();
2928
+ const result = db.prepare(
2929
+ `UPDATE observations
2930
+ SET expired_at = ?,
2931
+ expired_at_epoch = ?,
2932
+ valid_to = ?,
2933
+ valid_to_epoch = ?
2934
+ WHERE expired_at IS NULL
2935
+ AND created_at_epoch < ?
2936
+ AND importance <= ?
2937
+ AND type NOT IN (${typePlaceholders})
2938
+ AND COALESCE(evidence, '') != ?
2939
+ -- A file-backed row mirrors a file the human still keeps on disk.
2940
+ -- Value-decay may never retire it; only the file's removal can.
2941
+ AND title NOT LIKE ?
2942
+ AND NOT EXISTS (
2943
+ SELECT 1 FROM memory_usage u
2944
+ WHERE u.source = 'observation'
2945
+ AND u.record_id = observations.id
2946
+ AND u.hit_count > 0
2947
+ )
2948
+ AND NOT EXISTS (
2949
+ -- The grace period: a row demoted within the last cadence
2950
+ -- window is NOT yet expirable, so being pushed to the floor can
2951
+ -- never be immediately fatal.
2952
+ SELECT 1 FROM memory_usage u
2953
+ WHERE u.source = 'observation'
2954
+ AND u.record_id = observations.id
2955
+ AND u.last_reweight_epoch IS NOT NULL
2956
+ AND u.last_reweight_epoch > ?
2957
+ )`
2958
+ ).run(
2959
+ iso,
2960
+ now,
2961
+ iso,
2962
+ now,
2963
+ cutoffEpoch,
2964
+ opts.importanceFloor,
2965
+ ...opts.protectedTypes,
2966
+ CONSOLIDATION_LESSON_EVIDENCE,
2967
+ MEMORY_FILE_TITLE_LIKE,
2968
+ now - (opts.reweightIntervalDays ?? 1) * 86400
2969
+ );
2970
+ return result.changes;
2971
+ }
2972
+ function pruneOldObservations(db, opts) {
2973
+ return expireOldLowValueObservations(db, opts);
2974
+ }
2975
+ function recordRecallHits(db, sessionId, hits, nowEpochSec) {
2976
+ if (!hits.length) return 0;
2977
+ const now = nowEpochSec ?? Math.floor(Date.now() / 1e3);
2978
+ let recorded = 0;
2979
+ const claim = db.prepare(
2980
+ `INSERT OR IGNORE INTO memory_usage_sessions (source, record_id, session_id)
2981
+ VALUES (?, ?, ?)`
2982
+ );
2983
+ const bump = db.prepare(
2984
+ `INSERT INTO memory_usage (source, record_id, hit_count, hits_windowed, last_hit_epoch)
2985
+ VALUES (?, ?, 1, 1, ?)
2986
+ ON CONFLICT(source, record_id) DO UPDATE SET
2987
+ hit_count = hit_count + 1,
2988
+ hits_windowed = hits_windowed + 1,
2989
+ last_hit_epoch = excluded.last_hit_epoch`
2990
+ );
2991
+ const tx = db.transaction(() => {
2992
+ for (const h of hits) {
2993
+ if (claim.run(h.source, h.id, sessionId).changes === 0) continue;
2994
+ bump.run(h.source, h.id, now);
2995
+ recorded++;
2996
+ }
2997
+ });
2998
+ tx();
2999
+ return recorded;
3000
+ }
3001
+ function deduplicateFailedAttempt(db, sessionId, title, detail, opts) {
3002
+ const existing = db.prepare(`
3003
+ SELECT id, recurrence_count FROM observations
3004
+ WHERE type = 'failed_attempt' AND title = ?
3005
+ ORDER BY created_at_epoch DESC LIMIT 1
3006
+ `).get(title);
3007
+ if (existing) {
3008
+ db.prepare("UPDATE observations SET recurrence_count = recurrence_count + 1, detail = COALESCE(?, detail) WHERE id = ?").run(detail, existing.id);
3009
+ return existing.id;
3010
+ }
3011
+ return addObservation(db, sessionId, "failed_attempt", title, detail, {
3012
+ ...opts,
3013
+ importance: 5
3014
+ });
3015
+ }
3016
+ function getSessionsByTask(db, taskId) {
3017
+ return db.prepare(`
3018
+ SELECT session_id, status, started_at, ended_at, plan_phase
3019
+ FROM sessions WHERE task_id = ?
3020
+ ORDER BY started_at_epoch DESC
3021
+ LIMIT 10000
3022
+ `).all(taskId);
3023
+ }
3024
+ function getCrossTaskProgress(db, taskId) {
3025
+ const sessions = db.prepare(`
3026
+ SELECT session_id FROM sessions WHERE task_id = ? LIMIT 10000
3027
+ `).all(taskId);
3028
+ const merged = {};
3029
+ for (const session of sessions) {
3030
+ const summaries = db.prepare(`
3031
+ SELECT plan_progress FROM session_summaries WHERE session_id = ? LIMIT 10000
3032
+ `).all(session.session_id);
3033
+ for (const summary of summaries) {
3034
+ try {
3035
+ const progress = JSON.parse(summary.plan_progress);
3036
+ for (const [key, value] of Object.entries(progress)) {
3037
+ if (!merged[key] || value === "complete" || value === "in_progress" && merged[key] === "pending") {
3038
+ merged[key] = value;
3039
+ }
3040
+ }
3041
+ } catch (_e) {
3042
+ }
3043
+ }
3044
+ }
3045
+ return merged;
3046
+ }
3047
+ function linkSessionToTask(db, sessionId, taskId) {
3048
+ db.prepare("UPDATE sessions SET task_id = ? WHERE session_id = ?").run(taskId, sessionId);
3049
+ }
3050
+ function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantResponse, toolCallsJson, toolCallCount, promptTokens, responseTokens) {
3051
+ const result = db.prepare(`
3052
+ INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens)
3053
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3054
+ `).run(
3055
+ sessionId,
3056
+ turnNumber,
3057
+ userPrompt,
3058
+ assistantResponse ? assistantResponse.slice(0, 1e4) : null,
3059
+ toolCallsJson,
3060
+ toolCallCount,
3061
+ promptTokens,
3062
+ responseTokens
3063
+ );
3064
+ return Number(result.lastInsertRowid);
3065
+ }
3066
+ function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, inputSize, outputSize, success, filesInvolved) {
3067
+ db.prepare(`
3068
+ INSERT INTO tool_call_details (session_id, turn_number, tool_name, tool_input_summary, tool_input_size, tool_output_size, tool_success, files_involved)
3069
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3070
+ `).run(
3071
+ sessionId,
3072
+ turnNumber,
3073
+ toolName,
3074
+ inputSummary ? inputSummary.slice(0, 500) : null,
3075
+ inputSize,
3076
+ outputSize,
3077
+ success ? 1 : 0,
3078
+ filesInvolved ? JSON.stringify(filesInvolved) : null
3079
+ );
3080
+ }
3081
+ function getLastProcessedLine(db, sessionId) {
3082
+ const row = db.prepare("SELECT value FROM memory_meta WHERE key = ?").get(`last_processed_line:${sessionId}`);
3083
+ return row ? parseInt(row.value, 10) : 0;
3084
+ }
3085
+ function setLastProcessedLine(db, sessionId, lineNumber) {
3086
+ db.prepare("INSERT OR REPLACE INTO memory_meta (key, value) VALUES (?, ?)").run(`last_processed_line:${sessionId}`, String(lineNumber));
3087
+ }
3088
+ function pruneOldConversationTurns(db, retentionDays = 90) {
3089
+ const cutoffEpoch = Math.floor(Date.now() / 1e3) - retentionDays * 86400;
3090
+ const turnsResult = db.prepare("DELETE FROM conversation_turns WHERE created_at_epoch < ?").run(cutoffEpoch);
3091
+ const detailsResult = db.prepare("DELETE FROM tool_call_details WHERE created_at_epoch < ?").run(cutoffEpoch);
3092
+ return { turnsDeleted: turnsResult.changes, detailsDeleted: detailsResult.changes };
3093
+ }
3094
+ function getConversationTurns(db, sessionId, opts) {
3095
+ let sql = "SELECT id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens, created_at FROM conversation_turns WHERE session_id = ?";
3096
+ const params = [sessionId];
3097
+ if (opts?.turnFrom !== void 0) {
3098
+ sql += " AND turn_number >= ?";
3099
+ params.push(opts.turnFrom);
3100
+ }
3101
+ if (opts?.turnTo !== void 0) {
3102
+ sql += " AND turn_number <= ?";
3103
+ params.push(opts.turnTo);
3104
+ }
3105
+ sql += " ORDER BY turn_number ASC";
3106
+ return db.prepare(sql).all(...params);
3107
+ }
3108
+ function searchConversationTurns(db, query, opts) {
3109
+ const limit = opts?.limit ?? 20;
3110
+ let sql = `
3111
+ SELECT ct.id, ct.session_id, ct.turn_number, ct.user_prompt, ct.tool_call_count, ct.response_tokens, ct.created_at, rank
3112
+ FROM conversation_turns_fts
3113
+ JOIN conversation_turns ct ON conversation_turns_fts.rowid = ct.id
3114
+ WHERE conversation_turns_fts MATCH ?
3115
+ `;
3116
+ const params = [sanitizeFts5Query(query)];
3117
+ if (opts?.sessionId) {
3118
+ sql += " AND ct.session_id = ?";
3119
+ params.push(opts.sessionId);
3120
+ }
3121
+ if (opts?.dateFrom) {
3122
+ sql += " AND ct.created_at >= ?";
3123
+ params.push(opts.dateFrom);
3124
+ }
3125
+ if (opts?.dateTo) {
3126
+ sql += " AND ct.created_at <= ?";
3127
+ params.push(opts.dateTo);
3128
+ }
3129
+ if (opts?.minToolCalls !== void 0) {
3130
+ sql += " AND ct.tool_call_count >= ?";
3131
+ params.push(opts.minToolCalls);
3132
+ }
3133
+ sql += " ORDER BY rank LIMIT ?";
3134
+ params.push(limit);
3135
+ return db.prepare(sql).all(...params);
3136
+ }
3137
+ function getToolPatterns(db, opts) {
3138
+ const groupBy = opts?.groupBy ?? "tool";
3139
+ const params = [];
3140
+ let whereClause = "";
3141
+ const conditions = [];
3142
+ if (opts?.sessionId) {
3143
+ conditions.push("session_id = ?");
3144
+ params.push(opts.sessionId);
3145
+ }
3146
+ if (opts?.toolName) {
3147
+ conditions.push("tool_name = ?");
3148
+ params.push(opts.toolName);
3149
+ }
3150
+ if (opts?.dateFrom) {
3151
+ conditions.push("created_at >= ?");
3152
+ params.push(opts.dateFrom);
3153
+ }
3154
+ if (conditions.length > 0) {
3155
+ whereClause = "WHERE " + conditions.join(" AND ");
3156
+ }
3157
+ let sql;
3158
+ switch (groupBy) {
3159
+ case "session":
3160
+ sql = `SELECT session_id, COUNT(*) as call_count, COUNT(DISTINCT tool_name) as unique_tools,
3161
+ SUM(CASE WHEN tool_success = 1 THEN 1 ELSE 0 END) as successes,
3162
+ SUM(CASE WHEN tool_success = 0 THEN 1 ELSE 0 END) as failures,
3163
+ AVG(tool_output_size) as avg_output_size
3164
+ FROM tool_call_details ${whereClause}
3165
+ GROUP BY session_id ORDER BY call_count DESC`;
3166
+ break;
3167
+ case "day":
3168
+ sql = `SELECT date(created_at) as day, COUNT(*) as call_count, COUNT(DISTINCT tool_name) as unique_tools,
3169
+ SUM(CASE WHEN tool_success = 1 THEN 1 ELSE 0 END) as successes
3170
+ FROM tool_call_details ${whereClause}
3171
+ GROUP BY date(created_at) ORDER BY day DESC`;
3172
+ break;
3173
+ default:
3174
+ sql = `SELECT tool_name, COUNT(*) as call_count,
3175
+ SUM(CASE WHEN tool_success = 1 THEN 1 ELSE 0 END) as successes,
3176
+ SUM(CASE WHEN tool_success = 0 THEN 1 ELSE 0 END) as failures,
3177
+ AVG(tool_output_size) as avg_output_size,
3178
+ AVG(tool_input_size) as avg_input_size
3179
+ FROM tool_call_details ${whereClause}
3180
+ GROUP BY tool_name ORDER BY call_count DESC`;
3181
+ break;
3182
+ }
3183
+ return db.prepare(sql).all(...params);
3184
+ }
3185
+ function getSessionStats(db, opts) {
3186
+ if (opts?.sessionId) {
3187
+ const turns = db.prepare("SELECT COUNT(*) as turn_count, SUM(tool_call_count) as total_tool_calls, SUM(prompt_tokens) as total_prompt_tokens, SUM(response_tokens) as total_response_tokens FROM conversation_turns WHERE session_id = ?").get(opts.sessionId);
3188
+ const toolBreakdown = db.prepare("SELECT tool_name, COUNT(*) as count FROM tool_call_details WHERE session_id = ? GROUP BY tool_name ORDER BY count DESC").all(opts.sessionId);
3189
+ const session = db.prepare("SELECT * FROM sessions WHERE session_id = ?").get(opts.sessionId);
3190
+ return [{
3191
+ session_id: opts.sessionId,
3192
+ status: session?.status ?? "unknown",
3193
+ started_at: session?.started_at ?? null,
3194
+ ended_at: session?.ended_at ?? null,
3195
+ ...turns,
3196
+ tool_breakdown: toolBreakdown
3197
+ }];
3198
+ }
3199
+ const limit = opts?.limit ?? 10;
3200
+ return db.prepare(`
3201
+ SELECT s.session_id, s.status, s.started_at, s.ended_at,
3202
+ COUNT(ct.id) as turn_count,
3203
+ COALESCE(SUM(ct.tool_call_count), 0) as total_tool_calls,
3204
+ COALESCE(SUM(ct.prompt_tokens), 0) as total_prompt_tokens,
3205
+ COALESCE(SUM(ct.response_tokens), 0) as total_response_tokens
3206
+ FROM sessions s
3207
+ LEFT JOIN conversation_turns ct ON s.session_id = ct.session_id
3208
+ GROUP BY s.session_id
3209
+ ORDER BY s.started_at_epoch DESC
3210
+ LIMIT ?
3211
+ `).all(limit);
3212
+ }
3213
+ function getObservabilityDbSize(db) {
3214
+ const turnsCount = db.prepare("SELECT COUNT(*) as c FROM conversation_turns").get().c;
3215
+ const detailsCount = db.prepare("SELECT COUNT(*) as c FROM tool_call_details").get().c;
3216
+ const obsCount = db.prepare("SELECT COUNT(*) as c FROM observations").get().c;
3217
+ const pageCount = db.pragma("page_count")[0]?.page_count ?? 0;
3218
+ const pageSize = db.pragma("page_size")[0]?.page_size ?? 4096;
3219
+ return {
3220
+ conversation_turns_count: turnsCount,
3221
+ tool_call_details_count: detailsCount,
3222
+ observations_count: obsCount,
3223
+ db_page_count: pageCount,
3224
+ db_page_size: pageSize,
3225
+ estimated_size_mb: Math.round(pageCount * pageSize / (1024 * 1024) * 100) / 100
3226
+ };
3227
+ }
3228
+ function addFailureClass(db, opts) {
3229
+ const result = db.prepare(`
3230
+ INSERT OR IGNORE INTO failure_classes (name, description, diff_patterns, file_patterns, prompt_keywords, incidents, rules, scanner_checks, known_message, needs_review)
3231
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3232
+ `).run(
3233
+ opts.name,
3234
+ opts.description,
3235
+ JSON.stringify(opts.diffPatterns ?? []),
3236
+ JSON.stringify(opts.filePatterns ?? []),
3237
+ JSON.stringify(opts.promptKeywords ?? []),
3238
+ JSON.stringify(opts.incidents ?? []),
3239
+ JSON.stringify(opts.rules ?? []),
3240
+ JSON.stringify(opts.scannerChecks ?? []),
3241
+ opts.knownMessage ?? "",
3242
+ opts.needsReview ? 1 : 0
3243
+ );
3244
+ return Number(result.lastInsertRowid);
3245
+ }
3246
+ function getFailureClasses(db) {
3247
+ const rows = db.prepare("SELECT * FROM failure_classes ORDER BY name LIMIT 10000").all();
3248
+ return rows.map((row) => ({
3249
+ id: row.id,
3250
+ name: row.name,
3251
+ description: row.description,
3252
+ diff_patterns: JSON.parse(row.diff_patterns || "[]"),
3253
+ file_patterns: JSON.parse(row.file_patterns || "[]"),
3254
+ prompt_keywords: JSON.parse(row.prompt_keywords || "[]"),
3255
+ incidents: JSON.parse(row.incidents || "[]"),
3256
+ rules: JSON.parse(row.rules || "[]"),
3257
+ scanner_checks: JSON.parse(row.scanner_checks || "[]"),
3258
+ known_message: row.known_message,
3259
+ needs_review: !!row.needs_review
3260
+ }));
3261
+ }
3262
+ function appendIncidentToFailureClass(db, className, incidentId) {
3263
+ const row = db.prepare("SELECT incidents FROM failure_classes WHERE name = ?").get(className);
3264
+ if (!row) return;
3265
+ const incidents = JSON.parse(row.incidents || "[]");
3266
+ if (!incidents.includes(incidentId)) {
3267
+ incidents.push(incidentId);
3268
+ db.prepare("UPDATE failure_classes SET incidents = ?, updated_at = datetime('now') WHERE name = ?").run(JSON.stringify(incidents), className);
3269
+ }
3270
+ }
3271
+ function scoreFailureClasses(db, matchText, filePath, promptContext, weights) {
3272
+ const classes = getFailureClasses(db);
3273
+ if (classes.length === 0) return null;
3274
+ const diffWeight = weights?.diffPatternWeight ?? 3;
3275
+ const fileWeight = weights?.filePatternWeight ?? 2;
3276
+ const promptWeight = weights?.promptKeywordWeight ?? 2;
3277
+ let bestMatch = null;
3278
+ for (const fc of classes) {
3279
+ let score = 0;
3280
+ for (const pattern of fc.diff_patterns) {
3281
+ if (!pattern) continue;
3282
+ try {
3283
+ if (new RegExp(pattern, "i").test(matchText)) {
3284
+ score += diffWeight;
3285
+ }
3286
+ } catch {
3287
+ if (matchText.toLowerCase().includes(pattern.toLowerCase())) {
3288
+ score += diffWeight;
3289
+ }
3290
+ }
3291
+ }
3292
+ for (const pattern of fc.file_patterns) {
3293
+ if (!pattern) continue;
3294
+ try {
3295
+ if (new RegExp(pattern).test(filePath)) {
3296
+ score += fileWeight;
3297
+ }
3298
+ } catch {
3299
+ if (filePath.includes(pattern)) {
3300
+ score += fileWeight;
3301
+ }
3302
+ }
3303
+ }
3304
+ if (promptContext) {
3305
+ for (const keyword of fc.prompt_keywords) {
3306
+ if (!keyword) continue;
3307
+ try {
3308
+ if (new RegExp(keyword, "i").test(promptContext)) {
3309
+ score += promptWeight;
3310
+ }
3311
+ } catch {
3312
+ if (promptContext.toLowerCase().includes(keyword.toLowerCase())) {
3313
+ score += promptWeight;
3314
+ }
3315
+ }
3316
+ }
3317
+ }
3318
+ if (!bestMatch || score > bestMatch.score) {
3319
+ bestMatch = {
3320
+ name: fc.name,
3321
+ score,
3322
+ incidentCount: fc.incidents.length,
3323
+ rules: fc.rules,
3324
+ knownMessage: fc.known_message
3325
+ };
3326
+ }
3327
+ }
3328
+ return bestMatch;
3329
+ }
3330
+ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN, FUNNEL_EVENT_DRAIN_LIMIT, FUNNEL_EVENT_OUTBOX_CAP, _temporalColCache, CONSOLIDATION_LESSON_EVIDENCE, MEMORY_FILE_TITLE_PREFIX, MEMORY_FILE_TITLE_LIKE, USAGE_COUNTER_ARMED_KEY;
3331
+ var init_memory_db = __esm({
3332
+ "src/memory-db.ts"() {
3333
+ "use strict";
3334
+ init_config();
3335
+ init_memory_vector();
3336
+ init_db_backup();
3337
+ init_hook_failure_signal();
3338
+ init_memory_embed_sweep();
3339
+ MEMORY_SCHEMA_VERSION = 1;
3340
+ TOOL_COST_EVENTS_RETENTION_DAYS = 90;
3341
+ MAX_DRAFT_TEXT_LEN = 16384;
3342
+ FUNNEL_EVENT_DRAIN_LIMIT = 5e3;
3343
+ FUNNEL_EVENT_OUTBOX_CAP = 2e4;
3344
+ _temporalColCache = /* @__PURE__ */ new WeakMap();
3345
+ CONSOLIDATION_LESSON_EVIDENCE = "consolidation:session-summary";
3346
+ MEMORY_FILE_TITLE_PREFIX = "[memory-file] ";
3347
+ MEMORY_FILE_TITLE_LIKE = `${MEMORY_FILE_TITLE_PREFIX}%`;
3348
+ USAGE_COUNTER_ARMED_KEY = "usage_counter_armed_epoch";
3349
+ }
3350
+ });
3351
+
3352
+ // src/hooks/memory-recall.ts
3353
+ init_memory_db();
3354
+ init_config();
3355
+
3356
+ // src/memory-hybrid-search.ts
3357
+ init_memory_db();
3358
+ init_memory_vector();
3359
+ function temporalPredicate(asOf, includeSuperseded, prefix) {
3360
+ const c = (name) => `${prefix}${name}`;
3361
+ if (asOf != null && Number.isFinite(asOf)) {
3362
+ const t = Math.floor(Number(asOf) / 1e3);
3363
+ return `${c("ingested_at_epoch")} <= ${t} AND (${c("expired_at_epoch")} IS NULL OR ${c("expired_at_epoch")} > ${t}) AND ${c("valid_from_epoch")} <= ${t} AND (${c("valid_to_epoch")} IS NULL OR ${c("valid_to_epoch")} > ${t})`;
3364
+ }
3365
+ if (!includeSuperseded) {
3366
+ return `${c("expired_at_epoch")} IS NULL`;
3367
+ }
3368
+ return "";
3369
+ }
3370
+ var RRF_K = 10;
3371
+ var RECENCY_HALF_LIFE_DAYS = 180;
3372
+ var DEFAULT_LIMIT = 8;
3373
+ var DEFAULT_POOL = 30;
3374
+ var ALL_SOURCES = [
3375
+ "observation",
3376
+ "architecture_decision",
3377
+ "knowledge_chunk",
3378
+ "failure_class"
3379
+ ];
3380
+ function snippetOf(text, max = 160) {
3381
+ if (!text) return "";
3382
+ const clean = text.replace(/\s+/g, " ").trim();
3383
+ return clean.length > max ? clean.slice(0, max - 1) + "\u2026" : clean;
3384
+ }
3385
+ function ageDaysFrom(epochMs, nowMs) {
3386
+ const days = (nowMs - epochMs) / 864e5;
3387
+ return days < 0 ? 0 : days;
3388
+ }
3389
+ function recencyWeight(ageDays) {
3390
+ const freshness = Math.pow(0.5, ageDays / RECENCY_HALF_LIFE_DAYS);
3391
+ return 0.7 + 0.3 * freshness;
3392
+ }
3393
+ function importanceWeightOf(importance) {
3394
+ const clamped = Math.max(1, Math.min(5, importance));
3395
+ return 0.7 + 0.3 * (clamped / 5);
3396
+ }
3397
+ function collectObservations(memDb, queryText, pool, nowMs, loadVec, modelId, dim, asOf, includeSuperseded) {
3398
+ const map = /* @__PURE__ */ new Map();
3399
+ const bm25Temporal = temporalPredicate(asOf, includeSuperseded, "o.");
3400
+ const plainTemporal = temporalPredicate(asOf, includeSuperseded, "");
3401
+ try {
3402
+ const rows = memDb.prepare(
3403
+ `SELECT o.id, o.title, o.detail, o.importance, o.created_at_epoch
3404
+ FROM observations_fts
3405
+ JOIN observations o ON observations_fts.rowid = o.id
3406
+ WHERE observations_fts MATCH ?${bm25Temporal ? ` AND ${bm25Temporal}` : ""}
3407
+ ORDER BY rank LIMIT ?`
3408
+ ).all(sanitizeFts5QueryOr(queryText), pool);
3409
+ rows.forEach((r, i) => {
3410
+ map.set(r.id, {
3411
+ id: r.id,
3412
+ source: "observation",
3413
+ title: r.title,
3414
+ snippet: snippetOf(r.detail ?? r.title),
3415
+ importance: r.importance,
3416
+ ageDays: ageDaysFrom(r.created_at_epoch * 1e3, nowMs),
3417
+ bm25Order: i,
3418
+ vecs: []
3419
+ });
3420
+ });
3421
+ } catch {
3422
+ }
3423
+ const recent = memDb.prepare(
3424
+ `SELECT id, title, detail, importance, created_at_epoch
3425
+ FROM observations${plainTemporal ? ` WHERE ${plainTemporal}` : ""} ORDER BY created_at_epoch DESC LIMIT ?`
3426
+ ).all(pool);
3427
+ for (const r of recent) {
3428
+ if (!map.has(r.id)) {
3429
+ map.set(r.id, {
3430
+ id: r.id,
3431
+ source: "observation",
3432
+ title: r.title,
3433
+ snippet: snippetOf(r.detail ?? r.title),
3434
+ importance: r.importance,
3435
+ ageDays: ageDaysFrom(r.created_at_epoch * 1e3, nowMs),
3436
+ bm25Order: null,
3437
+ vecs: []
3438
+ });
3439
+ }
3440
+ }
3441
+ if (loadVec && map.size > 0) {
3442
+ loadEmbeddings(
3443
+ memDb,
3444
+ "observation_embeddings",
3445
+ "observation_id",
3446
+ [...map.keys()],
3447
+ modelId,
3448
+ dim,
3449
+ (id, vec) => {
3450
+ const c = map.get(id);
3451
+ if (c) c.vecs.push(vec);
3452
+ }
3453
+ );
3454
+ }
3455
+ return [...map.values()];
3456
+ }
3457
+ function collectLikeSource(db, source, cfg, queryText, pool, nowMs) {
3458
+ const map = /* @__PURE__ */ new Map();
3459
+ const tokens = queryText.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3).slice(0, 8);
3460
+ const ageExpr = (created) => {
3461
+ const ms = cfg.createdIsEpoch ? Number(created) * 1e3 : Date.parse(String(created));
3462
+ return Number.isFinite(ms) ? ageDaysFrom(ms, nowMs) : 0;
3463
+ };
3464
+ if (tokens.length > 0) {
3465
+ const likeClause = cfg.searchCols.map((col) => tokens.map(() => `${col} LIKE ?`).join(" OR ")).join(" OR ");
3466
+ const params = [];
3467
+ for (const _col of cfg.searchCols) for (const t of tokens) params.push(`%${t}%`);
3468
+ try {
3469
+ const rows = db.prepare(
3470
+ `SELECT ${cfg.idCol} AS id, ${cfg.titleExpr} AS title,
3471
+ ${cfg.snippetCol} AS snippet, ${cfg.createdCol} AS created
3472
+ FROM ${cfg.table} WHERE (${likeClause})${cfg.temporalClause ? ` AND ${cfg.temporalClause}` : ""}
3473
+ ORDER BY ${cfg.createdCol} DESC LIMIT ?`
3474
+ ).all(...params, pool);
3475
+ rows.forEach((r, i) => {
3476
+ map.set(r.id, {
3477
+ id: r.id,
3478
+ source,
3479
+ title: r.title,
3480
+ snippet: snippetOf(r.snippet ?? r.title),
3481
+ importance: 3,
3482
+ ageDays: ageExpr(r.created),
3483
+ bm25Order: i,
3484
+ vecs: []
3485
+ });
3486
+ });
3487
+ } catch {
3488
+ }
3489
+ }
3490
+ const recent = db.prepare(
3491
+ `SELECT ${cfg.idCol} AS id, ${cfg.titleExpr} AS title,
3492
+ ${cfg.snippetCol} AS snippet, ${cfg.createdCol} AS created
3493
+ FROM ${cfg.table}${cfg.temporalClause ? ` WHERE ${cfg.temporalClause}` : ""} ORDER BY ${cfg.createdCol} DESC LIMIT ?`
3494
+ ).all(pool);
3495
+ for (const r of recent) {
3496
+ if (!map.has(r.id)) {
3497
+ map.set(r.id, {
3498
+ id: r.id,
3499
+ source,
3500
+ title: r.title,
3501
+ snippet: snippetOf(r.snippet ?? r.title),
3502
+ importance: 3,
3503
+ ageDays: ageExpr(r.created),
3504
+ bm25Order: null,
3505
+ vecs: []
3506
+ });
3507
+ }
3508
+ }
3509
+ return [...map.values()];
3510
+ }
3511
+ function collectKnowledgeChunks(knowledgeDb, queryText, pool, nowMs, loadVec, modelId, dim) {
3512
+ const map = /* @__PURE__ */ new Map();
3513
+ try {
3514
+ const rows = knowledgeDb.prepare(
3515
+ `SELECT kc.id AS id, kc.heading AS heading, kc.content AS content,
3516
+ kd.indexed_at_epoch AS epoch
3517
+ FROM knowledge_fts
3518
+ JOIN knowledge_chunks kc ON knowledge_fts.rowid = kc.id
3519
+ JOIN knowledge_documents kd ON kd.id = kc.document_id
3520
+ WHERE knowledge_fts MATCH ?
3521
+ ORDER BY rank LIMIT ?`
3522
+ ).all(sanitizeFts5QueryOr(queryText), pool);
3523
+ rows.forEach((r, i) => {
3524
+ map.set(r.id, {
3525
+ id: r.id,
3526
+ source: "knowledge_chunk",
3527
+ title: r.heading || snippetOf(r.content, 60),
3528
+ snippet: snippetOf(r.content),
3529
+ importance: 3,
3530
+ ageDays: ageDaysFrom((r.epoch ?? 0) * 1e3, nowMs),
3531
+ bm25Order: i,
3532
+ vecs: []
3533
+ });
3534
+ });
3535
+ } catch {
3536
+ }
3537
+ if (loadVec && map.size > 0) {
3538
+ loadEmbeddings(
3539
+ knowledgeDb,
3540
+ "knowledge_chunk_embeddings",
3541
+ "chunk_id",
3542
+ [...map.keys()],
3543
+ modelId,
3544
+ dim,
3545
+ (id, vec) => {
3546
+ const c = map.get(id);
3547
+ if (c) c.vecs.push(vec);
3548
+ }
3549
+ );
3550
+ }
3551
+ return [...map.values()];
3552
+ }
3553
+ function loadEmbeddings(db, table, idCol, ids, modelId, dim, assign) {
3554
+ if (!modelId || !dim || ids.length === 0) return;
3555
+ try {
3556
+ const placeholders = ids.map(() => "?").join(",");
3557
+ const rows = db.prepare(
3558
+ `SELECT ${idCol} AS id, vec FROM ${table}
3559
+ WHERE ${idCol} IN (${placeholders}) AND model_id = ? AND dim = ?
3560
+ LIMIT ?`
3561
+ ).all(...ids, modelId, dim, ids.length);
3562
+ for (const r of rows) {
3563
+ const v = blobToFloat32(r.vec);
3564
+ if (v && v.length === dim) assign(r.id, v);
3565
+ }
3566
+ } catch {
3567
+ }
3568
+ }
3569
+ function hybridSearch(memDb, knowledgeDb, opts) {
3570
+ const sources = opts.sources ?? ALL_SOURCES;
3571
+ const limit = opts.limit ?? DEFAULT_LIMIT;
3572
+ const minScore = opts.minScore ?? 0;
3573
+ const pool = opts.candidatePool ?? DEFAULT_POOL;
3574
+ const nowMs = opts.now ?? Date.now();
3575
+ const queryVec = opts.queryVec ?? null;
3576
+ const modelId = queryVec ? opts.modelId ?? null : null;
3577
+ const dim = queryVec ? opts.dim ?? null : null;
3578
+ const loadVec = !!(queryVec && modelId && dim);
3579
+ const includeSuperseded = opts.includeSuperseded ?? false;
3580
+ let candidates = [];
3581
+ if (sources.includes("observation")) {
3582
+ candidates = candidates.concat(
3583
+ collectObservations(
3584
+ memDb,
3585
+ opts.queryText,
3586
+ pool,
3587
+ nowMs,
3588
+ loadVec,
3589
+ modelId,
3590
+ dim,
3591
+ opts.asOf,
3592
+ includeSuperseded
3593
+ )
3594
+ );
3595
+ }
3596
+ if (sources.includes("architecture_decision")) {
3597
+ candidates = candidates.concat(
3598
+ collectLikeSource(
3599
+ memDb,
3600
+ "architecture_decision",
3601
+ {
3602
+ table: "architecture_decisions",
3603
+ idCol: "id",
3604
+ titleExpr: "title",
3605
+ snippetCol: "decision",
3606
+ searchCols: ["title", "decision", "context"],
3607
+ createdCol: "created_at",
3608
+ createdIsEpoch: false,
3609
+ temporalClause: temporalPredicate(opts.asOf, includeSuperseded, "")
3610
+ },
3611
+ opts.queryText,
3612
+ pool,
3613
+ nowMs
3614
+ )
3615
+ );
3616
+ }
3617
+ if (sources.includes("failure_class")) {
3618
+ candidates = candidates.concat(
3619
+ collectLikeSource(
3620
+ memDb,
3621
+ "failure_class",
3622
+ {
3623
+ table: "failure_classes",
3624
+ idCol: "id",
3625
+ titleExpr: "name",
3626
+ snippetCol: "description",
3627
+ searchCols: ["name", "description", "known_message"],
3628
+ createdCol: "created_at",
3629
+ createdIsEpoch: false
3630
+ },
3631
+ opts.queryText,
3632
+ pool,
3633
+ nowMs
3634
+ )
3635
+ );
3636
+ }
3637
+ if (sources.includes("knowledge_chunk") && knowledgeDb) {
3638
+ candidates = candidates.concat(
3639
+ collectKnowledgeChunks(knowledgeDb, opts.queryText, pool, nowMs, loadVec, modelId, dim)
3640
+ );
3641
+ }
3642
+ if (candidates.length === 0) return [];
3643
+ const worstBm25 = pool + 1;
3644
+ let cosineOrder = null;
3645
+ if (loadVec && queryVec) {
3646
+ const scored = candidates.map((c) => ({
3647
+ key: `${c.source}:${c.id}`,
3648
+ // MAX-POOL: score the record by its BEST-matching passage.
3649
+ sim: c.vecs.length ? Math.max(...c.vecs.map((v) => cosineSim(queryVec, v))) : -Infinity
3650
+ })).filter((s) => s.sim > -Infinity).sort((a, b) => b.sim - a.sim);
3651
+ cosineOrder = /* @__PURE__ */ new Map();
3652
+ scored.forEach((s, i) => cosineOrder.set(s.key, i));
3653
+ }
3654
+ const worstCosine = candidates.length + 1;
3655
+ const ranked = candidates.map((c) => {
3656
+ const key = `${c.source}:${c.id}`;
3657
+ const bm25Rank = c.bm25Order ?? worstBm25;
3658
+ let rrf = 1 / (RRF_K + bm25Rank);
3659
+ if (cosineOrder) {
3660
+ const cRank = cosineOrder.has(key) ? cosineOrder.get(key) : worstCosine;
3661
+ rrf += 1 / (RRF_K + cRank);
3662
+ }
3663
+ const score = rrf * importanceWeightOf(c.importance) * recencyWeight(c.ageDays);
3664
+ return {
3665
+ id: c.id,
3666
+ source: c.source,
3667
+ title: c.title,
3668
+ snippet: c.snippet,
3669
+ score,
3670
+ importance: c.importance,
3671
+ ageDays: c.ageDays
3672
+ };
3673
+ });
3674
+ const top = ranked.filter((r) => r.score >= minScore).sort((a, b) => b.score - a.score || a.ageDays - b.ageDays).slice(0, limit);
3675
+ if (includeSuperseded && opts.asOf == null) {
3676
+ for (const r of top) {
3677
+ const table = r.source === "observation" ? "observations" : r.source === "architecture_decision" ? "architecture_decisions" : null;
3678
+ if (!table) continue;
3679
+ try {
3680
+ const row = memDb.prepare(`SELECT valid_to, superseded_by, expired_at_epoch FROM ${table} WHERE id = ?`).get(r.id);
3681
+ if (row && row.expired_at_epoch != null) {
3682
+ const when = row.valid_to ? row.valid_to.slice(0, 10) : "an earlier date";
3683
+ const by = row.superseded_by != null ? ` by #${row.superseded_by}` : "";
3684
+ r.snippet = `(superseded on ${when}${by}) ${r.snippet}`;
3685
+ }
3686
+ } catch {
3687
+ }
3688
+ }
3689
+ }
3690
+ return top;
3691
+ }
3692
+
3693
+ // src/memory-recall-format.ts
3694
+ var DEFAULT_MAX_TOKENS = 1200;
3695
+ var HEADER = "=== \u{1F9E0} Relevant memory (auto-recalled) ===\n";
3696
+ var FOOTER = "(call massu_memory_detail / massu_knowledge_pattern for full text)\n=== END \u{1F9E0} ===\n";
3697
+ function estimateTokens(text) {
3698
+ return Math.ceil(text.length / 4);
3699
+ }
3700
+ var SOURCE_ICON = {
3701
+ observation: "\u{1F4A1}",
3702
+ architecture_decision: "\u{1F4D0}",
3703
+ knowledge_chunk: "\u{1F4DA}",
3704
+ failure_class: "\u26A0\uFE0F"
3705
+ };
3706
+ var SOURCE_LABEL = {
3707
+ observation: "observation",
3708
+ architecture_decision: "decision",
3709
+ knowledge_chunk: "knowledge",
3710
+ failure_class: "failure"
3711
+ };
3712
+ function ageLabel(ageDays) {
3713
+ if (ageDays < 1) return "today";
3714
+ const d = Math.round(ageDays);
3715
+ if (d < 30) return `${d}d ago`;
3716
+ const m = Math.round(d / 30);
3717
+ if (m < 12) return `${m}mo ago`;
3718
+ return `${Math.round(m / 12)}y ago`;
3719
+ }
3720
+ function lineFor(r) {
3721
+ const icon = SOURCE_ICON[r.source] ?? "\u2022";
3722
+ const label = SOURCE_LABEL[r.source] ?? r.source;
3723
+ const title = r.title.replace(/\s+/g, " ").trim();
3724
+ const why = r.snippet && r.snippet !== title ? ` \u2014 ${r.snippet}` : "";
3725
+ return `${icon} ${label} (${ageLabel(r.ageDays)}): ${title}${why}
3726
+ `;
3727
+ }
3728
+ function formatRecallBlock(results, opts) {
3729
+ const shown = selectRecallItems(results, opts);
3730
+ if (shown.length === 0) return "";
3731
+ return HEADER + shown.map(lineFor).join("") + FOOTER;
3732
+ }
3733
+ function selectRecallItems(results, opts) {
3734
+ if (!results || results.length === 0) return [];
3735
+ const maxTokens = opts?.maxTokens ?? DEFAULT_MAX_TOKENS;
3736
+ let used = estimateTokens(HEADER) + estimateTokens(FOOTER);
3737
+ const shown = [];
3738
+ for (const r of results) {
3739
+ const cost = estimateTokens(lineFor(r));
3740
+ if (used + cost > maxTokens) break;
3741
+ shown.push(r);
3742
+ used += cost;
3743
+ }
3744
+ return shown;
3745
+ }
3746
+
3747
+ // src/hooks/memory-recall.ts
3748
+ init_memory_embedder();
3749
+
3750
+ // src/hooks/lib/write-hook-message.ts
3751
+ function writeHookMessage(message) {
3752
+ process.stdout.write(JSON.stringify({ message }) + "\n");
3753
+ }
3754
+
3755
+ // src/hooks/memory-recall.ts
3756
+ init_hook_failure_signal();
3757
+ import { existsSync as existsSync7 } from "fs";
3758
+ var DEFAULTS = {
3759
+ enabled: true,
3760
+ maxTokens: 1200,
3761
+ sources: ["observation", "architecture_decision", "knowledge_chunk", "failure_class"],
3762
+ timeoutMs: 8e3,
3763
+ limit: 8,
3764
+ minScore: 0,
3765
+ embedEnabled: true
3766
+ };
3767
+ function loadRecallConfig() {
3768
+ try {
3769
+ const r = getConfig().memory?.recall;
3770
+ if (!r) return DEFAULTS;
3771
+ return {
3772
+ enabled: r.enabled ?? DEFAULTS.enabled,
3773
+ maxTokens: r.maxTokens ?? DEFAULTS.maxTokens,
3774
+ sources: r.sources ?? DEFAULTS.sources,
3775
+ timeoutMs: r.timeoutMs ?? DEFAULTS.timeoutMs,
3776
+ limit: r.limit ?? DEFAULTS.limit,
3777
+ minScore: r.minScore ?? DEFAULTS.minScore,
3778
+ embedEnabled: r.embedEnabled ?? DEFAULTS.embedEnabled,
3779
+ embedEndpoint: r.embedEndpoint,
3780
+ embedModel: r.embedModel
3781
+ };
3782
+ } catch {
3783
+ return DEFAULTS;
3784
+ }
3785
+ }
3786
+ function withTimeout(p, ms) {
3787
+ return Promise.race([
3788
+ p,
3789
+ new Promise((res) => setTimeout(() => res(null), ms))
3790
+ ]);
3791
+ }
3792
+ async function main() {
3793
+ try {
3794
+ const input = await readStdin();
3795
+ if (!input.trim()) return void process.exit(0);
3796
+ let hookInput;
3797
+ try {
3798
+ hookInput = JSON.parse(input);
3799
+ } catch {
3800
+ return void process.exit(0);
3801
+ }
3802
+ const prompt = (hookInput.prompt ?? "").trim();
3803
+ if (!prompt) return void process.exit(0);
3804
+ const cfg = loadRecallConfig();
3805
+ if (!cfg.enabled) return void process.exit(0);
3806
+ const block = await withTimeout(
3807
+ computeRecall(prompt, cfg, hookInput.session_id),
3808
+ cfg.timeoutMs
3809
+ );
3810
+ if (block && block.trim()) {
3811
+ writeHookMessage(block);
3812
+ }
3813
+ } catch {
3814
+ } finally {
3815
+ process.exit(0);
3816
+ }
3817
+ }
3818
+ async function computeRecall(prompt, cfg, sessionId) {
3819
+ let memDb = null;
3820
+ let knowledgeDb = null;
3821
+ try {
3822
+ const queryVec = await withTimeout(embed(prompt), Math.min(cfg.timeoutMs - 500, 6e3)).catch(
3823
+ () => null
3824
+ );
3825
+ memDb = getMemoryDb();
3826
+ const knowledgeDbPath = getResolvedPaths().knowledgeDbPath;
3827
+ if (cfg.sources.includes("knowledge_chunk") && existsSync7(knowledgeDbPath)) {
3828
+ try {
3829
+ const DatabaseCtor = (await import("better-sqlite3")).default;
3830
+ knowledgeDb = new DatabaseCtor(knowledgeDbPath, { readonly: true });
3831
+ } catch {
3832
+ knowledgeDb = null;
3833
+ }
3834
+ }
3835
+ const active = queryVec ? getActiveEmbedModel() : null;
3836
+ let annotateSuperseded = false;
3837
+ try {
3838
+ annotateSuperseded = getConfig().memory?.contradiction?.annotateSuperseded === true;
3839
+ } catch {
3840
+ annotateSuperseded = false;
3841
+ }
3842
+ const results = hybridSearch(memDb, knowledgeDb, {
3843
+ queryText: prompt,
3844
+ queryVec: queryVec ?? null,
3845
+ modelId: active?.modelId ?? null,
3846
+ dim: active?.dim ?? null,
3847
+ sources: cfg.sources,
3848
+ limit: cfg.limit,
3849
+ minScore: cfg.minScore,
3850
+ includeSuperseded: annotateSuperseded
3851
+ });
3852
+ const shown = selectRecallItems(results, { maxTokens: cfg.maxTokens });
3853
+ if (sessionId) {
3854
+ try {
3855
+ recordRecallHits(
3856
+ memDb,
3857
+ sessionId,
3858
+ shown.map((r) => ({ source: r.source, id: r.id }))
3859
+ );
3860
+ } catch {
3861
+ }
3862
+ }
3863
+ return formatRecallBlock(shown, { maxTokens: cfg.maxTokens });
3864
+ } catch (err) {
3865
+ recordHookFailure("memory-recall", err);
3866
+ return "";
3867
+ } finally {
3868
+ try {
3869
+ knowledgeDb?.close();
3870
+ } catch {
3871
+ }
3872
+ try {
3873
+ memDb?.close();
3874
+ } catch {
3875
+ }
3876
+ }
3877
+ }
3878
+ function readStdin() {
3879
+ return new Promise((resolve5) => {
3880
+ let data = "";
3881
+ process.stdin.setEncoding("utf-8");
3882
+ process.stdin.on("data", (chunk) => {
3883
+ data += chunk;
3884
+ });
3885
+ process.stdin.on("end", () => resolve5(data));
3886
+ setTimeout(() => resolve5(data), 3e3);
3887
+ });
3888
+ }
3889
+ main();