@massu/core 1.15.2 → 1.15.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -2
- package/dist/cli.js +6166 -1718
- package/dist/embedder/MODEL-LICENSE +221 -0
- package/dist/embedder/model_quantized.onnx +0 -0
- package/dist/embedder/vocab.txt +30522 -0
- package/dist/hooks/auto-learning-pipeline.js +3145 -411
- package/dist/hooks/classify-failure.js +2399 -420
- package/dist/hooks/cost-tracker.js +2558 -410
- package/dist/hooks/fix-detector.js +3151 -417
- package/dist/hooks/incident-pipeline.js +2372 -420
- package/dist/hooks/intent-suggester.js +3318 -4
- package/dist/hooks/memory-recall.js +3889 -0
- package/dist/hooks/post-edit-context.js +3101 -401
- package/dist/hooks/post-tool-use.js +2802 -515
- package/dist/hooks/pre-compact.js +2401 -428
- package/dist/hooks/pre-delete-check.js +3106 -407
- package/dist/hooks/pre-tool-use-gate.js +3484 -702
- package/dist/hooks/quality-event.js +2558 -410
- package/dist/hooks/rule-enforcement-pipeline.js +3140 -406
- package/dist/hooks/security-gate.js +3377 -47
- package/dist/hooks/session-end.js +3402 -431
- package/dist/hooks/session-start.js +12572 -8985
- package/dist/hooks/user-prompt.js +2407 -444
- package/docs/AUTHORING-ADAPTERS.md +1 -1
- package/docs/SECURITY.md +2 -2
- package/package.json +7 -5
|
@@ -1,22 +1,40 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import{createRequire as __cr}from"module";const require=__cr(import.meta.url);
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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);
|
|
15
28
|
|
|
16
29
|
// src/lib/memory-path.ts
|
|
17
30
|
function encodeMemoryDirName(projectRoot) {
|
|
18
31
|
return projectRoot.replace(/\//g, "-");
|
|
19
32
|
}
|
|
33
|
+
var init_memory_path = __esm({
|
|
34
|
+
"src/lib/memory-path.ts"() {
|
|
35
|
+
"use strict";
|
|
36
|
+
}
|
|
37
|
+
});
|
|
20
38
|
|
|
21
39
|
// src/credentials.ts
|
|
22
40
|
import { homedir } from "os";
|
|
@@ -29,9 +47,6 @@ import {
|
|
|
29
47
|
rmSync,
|
|
30
48
|
chmodSync
|
|
31
49
|
} from "fs";
|
|
32
|
-
var MASSU_ENV_API_KEY = "MASSU_API_KEY";
|
|
33
|
-
var MASSU_ENV_CLOUD_ENDPOINT = "MASSU_CLOUD_ENDPOINT";
|
|
34
|
-
var DEFAULT_CLOUD_ENDPOINT = "https://api.massu.ai/v1";
|
|
35
50
|
function credentialsDir(home = homedir()) {
|
|
36
51
|
return resolve(home, ".massu");
|
|
37
52
|
}
|
|
@@ -79,391 +94,160 @@ function resolveEndpoint(opts = {}) {
|
|
|
79
94
|
if (envEp.length > 0) return envEp;
|
|
80
95
|
return DEFAULT_CLOUD_ENDPOINT;
|
|
81
96
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
});
|
|
91
|
-
var PatternRuleConfigSchema = z.object({
|
|
92
|
-
pattern: z.string().default("**"),
|
|
93
|
-
rules: z.array(z.string()).default([]),
|
|
94
|
-
language: z.string().optional()
|
|
95
|
-
});
|
|
96
|
-
var CostModelSchema = z.object({
|
|
97
|
-
input_per_million: z.number(),
|
|
98
|
-
output_per_million: z.number(),
|
|
99
|
-
cache_read_per_million: z.number().optional(),
|
|
100
|
-
cache_write_per_million: z.number().optional()
|
|
101
|
-
});
|
|
102
|
-
var AnalyticsConfigSchema = z.object({
|
|
103
|
-
quality: z.object({
|
|
104
|
-
weights: z.record(z.string(), z.number()).default({
|
|
105
|
-
bug_found: -5,
|
|
106
|
-
vr_failure: -10,
|
|
107
|
-
incident: -20,
|
|
108
|
-
cr_violation: -3,
|
|
109
|
-
vr_pass: 2,
|
|
110
|
-
clean_commit: 5,
|
|
111
|
-
successful_verification: 3
|
|
112
|
-
}),
|
|
113
|
-
categories: z.array(z.string()).default(["security", "architecture", "coupling", "tests", "rule_compliance"])
|
|
114
|
-
}).optional(),
|
|
115
|
-
cost: z.object({
|
|
116
|
-
models: z.record(z.string(), CostModelSchema).default({}),
|
|
117
|
-
currency: z.string().default("USD")
|
|
118
|
-
}).optional(),
|
|
119
|
-
prompts: z.object({
|
|
120
|
-
success_indicators: z.array(z.string()).default(["committed", "approved", "looks good", "perfect", "great", "thanks"]),
|
|
121
|
-
failure_indicators: z.array(z.string()).default(["revert", "wrong", "that's not", "undo", "incorrect"]),
|
|
122
|
-
max_turns_for_success: z.number().default(2)
|
|
123
|
-
}).optional()
|
|
124
|
-
}).optional();
|
|
125
|
-
var CustomPatternSchema = z.object({
|
|
126
|
-
pattern: z.string(),
|
|
127
|
-
severity: z.string(),
|
|
128
|
-
message: z.string()
|
|
129
|
-
});
|
|
130
|
-
var GovernanceConfigSchema = z.object({
|
|
131
|
-
audit: z.object({
|
|
132
|
-
formats: z.array(z.string()).default(["summary", "detailed", "soc2"]),
|
|
133
|
-
retention_days: z.number().default(365),
|
|
134
|
-
auto_log: z.record(z.string(), z.boolean()).default({
|
|
135
|
-
code_changes: true,
|
|
136
|
-
rule_enforcement: true,
|
|
137
|
-
approvals: true,
|
|
138
|
-
commits: true
|
|
139
|
-
})
|
|
140
|
-
}).optional(),
|
|
141
|
-
validation: z.object({
|
|
142
|
-
realtime: z.boolean().default(true),
|
|
143
|
-
checks: z.record(z.string(), z.boolean()).default({
|
|
144
|
-
rule_compliance: true,
|
|
145
|
-
import_existence: true,
|
|
146
|
-
naming_conventions: true
|
|
147
|
-
}),
|
|
148
|
-
custom_patterns: z.array(CustomPatternSchema).default([])
|
|
149
|
-
}).optional(),
|
|
150
|
-
adr: z.object({
|
|
151
|
-
detection_phrases: z.array(z.string()).default(["chose", "decided", "switching to", "moving from", "going with"]),
|
|
152
|
-
template: z.string().default("default"),
|
|
153
|
-
storage: z.string().default("database"),
|
|
154
|
-
output_dir: z.string().default("docs/adr")
|
|
155
|
-
}).optional()
|
|
156
|
-
}).optional();
|
|
157
|
-
var SecurityPatternSchema = z.object({
|
|
158
|
-
pattern: z.string(),
|
|
159
|
-
severity: z.string(),
|
|
160
|
-
category: z.string(),
|
|
161
|
-
description: z.string()
|
|
162
|
-
});
|
|
163
|
-
var SecurityConfigSchema = z.object({
|
|
164
|
-
patterns: z.array(SecurityPatternSchema).default([]),
|
|
165
|
-
auto_score_on_edit: z.boolean().default(true),
|
|
166
|
-
score_threshold_alert: z.number().default(50),
|
|
167
|
-
severity_weights: z.record(z.string(), z.number()).optional(),
|
|
168
|
-
restrictive_licenses: z.array(z.string()).optional(),
|
|
169
|
-
dep_alternatives: z.record(z.string(), z.array(z.string())).optional(),
|
|
170
|
-
dependencies: z.object({
|
|
171
|
-
package_manager: z.string().default("npm"),
|
|
172
|
-
blocked_packages: z.array(z.string()).default([]),
|
|
173
|
-
preferred_packages: z.record(z.string(), z.string()).default({}),
|
|
174
|
-
max_bundle_size_kb: z.number().default(500)
|
|
175
|
-
}).optional()
|
|
176
|
-
}).optional();
|
|
177
|
-
var TeamConfigSchema = z.object({
|
|
178
|
-
enabled: z.boolean().default(false),
|
|
179
|
-
sync_backend: z.string().default("local"),
|
|
180
|
-
developer_id: z.string().default("auto"),
|
|
181
|
-
share_by_default: z.boolean().default(false),
|
|
182
|
-
expertise_weights: z.object({
|
|
183
|
-
session: z.number().default(20),
|
|
184
|
-
observation: z.number().default(10)
|
|
185
|
-
}).optional(),
|
|
186
|
-
privacy: z.object({
|
|
187
|
-
share_file_paths: z.boolean().default(true),
|
|
188
|
-
share_code_snippets: z.boolean().default(false),
|
|
189
|
-
share_observations: z.boolean().default(true)
|
|
190
|
-
}).optional()
|
|
191
|
-
}).optional();
|
|
192
|
-
var RegressionConfigSchema = z.object({
|
|
193
|
-
test_patterns: z.array(z.string()).default([
|
|
194
|
-
"{dir}/__tests__/{name}.test.{ext}",
|
|
195
|
-
"{dir}/{name}.spec.{ext}",
|
|
196
|
-
"tests/{path}.test.{ext}"
|
|
197
|
-
]),
|
|
198
|
-
test_runner: z.string().default("npm test"),
|
|
199
|
-
health_thresholds: z.object({
|
|
200
|
-
healthy: z.number().default(80),
|
|
201
|
-
warning: z.number().default(50)
|
|
202
|
-
}).optional()
|
|
203
|
-
}).optional();
|
|
204
|
-
var AutoLearningConfigSchema = z.object({
|
|
205
|
-
enabled: z.boolean().default(true),
|
|
206
|
-
incidentDir: z.string().default("docs/incidents"),
|
|
207
|
-
memoryDir: z.string().default("memory"),
|
|
208
|
-
memoryIndexFile: z.string().default("MEMORY.md"),
|
|
209
|
-
enforcementHooksDir: z.string().default("scripts/hooks"),
|
|
210
|
-
fixDetection: z.object({
|
|
211
|
-
enabled: z.boolean().default(true),
|
|
212
|
-
lookbackDays: z.number().default(7),
|
|
213
|
-
signals: z.array(z.string()).default([
|
|
214
|
-
"removed_broken_code",
|
|
215
|
-
"added_error_handling",
|
|
216
|
-
"method_name_correction",
|
|
217
|
-
"auth_fix",
|
|
218
|
-
"nil_handling_fix",
|
|
219
|
-
"concurrency_fix",
|
|
220
|
-
"async_pattern_fix",
|
|
221
|
-
"added_missing_import"
|
|
222
|
-
])
|
|
223
|
-
}).default({}),
|
|
224
|
-
failureClassification: z.object({
|
|
225
|
-
enabled: z.boolean().default(true),
|
|
226
|
-
thresholds: z.object({
|
|
227
|
-
known: z.number().default(5),
|
|
228
|
-
similar: z.number().default(3)
|
|
229
|
-
}).default({}),
|
|
230
|
-
scoring: z.object({
|
|
231
|
-
diffPatternWeight: z.number().default(3),
|
|
232
|
-
filePatternWeight: z.number().default(2),
|
|
233
|
-
promptKeywordWeight: z.number().default(2)
|
|
234
|
-
}).default({})
|
|
235
|
-
}).default({}),
|
|
236
|
-
pipeline: z.object({
|
|
237
|
-
requireIncidentReport: z.boolean().default(true),
|
|
238
|
-
requirePreventionRule: z.boolean().default(true),
|
|
239
|
-
requireEnforcement: z.boolean().default(true)
|
|
240
|
-
}).default({}),
|
|
241
|
-
// plan-v0.2-interactive-rule-approval P-D-008 / P-D-009: project-configured
|
|
242
|
-
// custom destinations for the rule-candidate funnel. The classifier matches
|
|
243
|
-
// a candidate to one of these entries when none of the framework
|
|
244
|
-
// destinations (pattern-scanner / claude-md-cr / corrections-md) apply.
|
|
245
|
-
customDestinations: z.array(z.object({
|
|
246
|
-
name: z.string(),
|
|
247
|
-
path: z.string(),
|
|
248
|
-
triggerKeywords: z.array(z.string()).default([]),
|
|
249
|
-
template: z.string()
|
|
250
|
-
})).default([])
|
|
251
|
-
}).optional();
|
|
252
|
-
var CloudConfigSchema = z.object({
|
|
253
|
-
enabled: z.boolean().default(false),
|
|
254
|
-
apiKey: z.string().optional(),
|
|
255
|
-
endpoint: z.string().optional(),
|
|
256
|
-
sync: z.object({
|
|
257
|
-
memory: z.boolean().default(true),
|
|
258
|
-
analytics: z.boolean().default(true),
|
|
259
|
-
audit: z.boolean().default(true)
|
|
260
|
-
}).default({ memory: true, analytics: true, audit: true })
|
|
261
|
-
}).optional();
|
|
262
|
-
var ConventionsConfigSchema = z.object({
|
|
263
|
-
claudeDirName: z.string().default(".claude").refine(
|
|
264
|
-
(s) => !s.includes("..") && !s.startsWith("/"),
|
|
265
|
-
{ message: 'claudeDirName must not contain ".." or start with "/"' }
|
|
266
|
-
),
|
|
267
|
-
sessionStatePath: z.string().default(".claude/session-state/CURRENT.md").refine(
|
|
268
|
-
(s) => !s.includes("..") && !s.startsWith("/"),
|
|
269
|
-
{ message: 'sessionStatePath must not contain ".." or start with "/"' }
|
|
270
|
-
),
|
|
271
|
-
sessionArchivePath: z.string().default(".claude/session-state/archive").refine(
|
|
272
|
-
(s) => !s.includes("..") && !s.startsWith("/"),
|
|
273
|
-
{ message: 'sessionArchivePath must not contain ".." or start with "/"' }
|
|
274
|
-
),
|
|
275
|
-
knowledgeCategories: z.array(z.string()).default([
|
|
276
|
-
"patterns",
|
|
277
|
-
"commands",
|
|
278
|
-
"incidents",
|
|
279
|
-
"reference",
|
|
280
|
-
"protocols",
|
|
281
|
-
"checklists",
|
|
282
|
-
"playbooks",
|
|
283
|
-
"critical",
|
|
284
|
-
"scripts",
|
|
285
|
-
"status",
|
|
286
|
-
"templates",
|
|
287
|
-
"loop-state",
|
|
288
|
-
"session-state",
|
|
289
|
-
"agents"
|
|
290
|
-
]),
|
|
291
|
-
knowledgeSourceFiles: z.array(z.string()).default(["CLAUDE.md", "MEMORY.md", "corrections.md"]),
|
|
292
|
-
excludePatterns: z.array(z.string()).default(["/ARCHIVE/", "/SESSION-HISTORY/"])
|
|
293
|
-
}).optional();
|
|
294
|
-
var PythonDomainConfigSchema = z.object({
|
|
295
|
-
name: z.string(),
|
|
296
|
-
packages: z.array(z.string()),
|
|
297
|
-
allowed_imports_from: z.array(z.string()).default([])
|
|
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
|
+
}
|
|
298
105
|
});
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
+
}
|
|
316
243
|
});
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
ui: z.string().optional()
|
|
325
|
-
}).passthrough();
|
|
326
|
-
var FrameworkConfigSchema = z.object({
|
|
327
|
-
type: z.string().default("typescript"),
|
|
328
|
-
primary: z.string().optional(),
|
|
329
|
-
router: z.string().default("none"),
|
|
330
|
-
orm: z.string().default("none"),
|
|
331
|
-
ui: z.string().default("none"),
|
|
332
|
-
languages: z.record(z.string(), LanguageFrameworkEntrySchema).optional()
|
|
333
|
-
}).passthrough();
|
|
334
|
-
var DetectedConfigSchema = z.object({}).passthrough().optional();
|
|
335
|
-
var VerificationEntrySchema = z.object({
|
|
336
|
-
type: z.string().optional(),
|
|
337
|
-
test: z.string().optional(),
|
|
338
|
-
syntax: z.string().optional(),
|
|
339
|
-
lint: z.string().optional(),
|
|
340
|
-
build: z.string().optional()
|
|
341
|
-
}).passthrough();
|
|
342
|
-
var VerificationConfigSchema = z.record(z.string(), VerificationEntrySchema).optional();
|
|
343
|
-
var CanonicalPathsSchema = z.record(z.string(), z.string()).optional();
|
|
344
|
-
var VerificationTypesSchema = z.record(z.string(), z.string()).optional();
|
|
345
|
-
var DetectionRuleEntrySchema = z.object({
|
|
346
|
-
signals: z.array(z.string()).default([]),
|
|
347
|
-
priority: z.number().optional()
|
|
348
|
-
}).passthrough();
|
|
349
|
-
var DetectionConfigSchema = z.object({
|
|
350
|
-
rules: z.record(
|
|
351
|
-
z.string(),
|
|
352
|
-
// language
|
|
353
|
-
z.record(z.string(), DetectionRuleEntrySchema)
|
|
354
|
-
// framework -> rule entry
|
|
355
|
-
).optional(),
|
|
356
|
-
signal_weights: z.record(z.string(), z.number()).optional(),
|
|
357
|
-
disable_builtin: z.boolean().optional()
|
|
358
|
-
}).passthrough().optional();
|
|
359
|
-
var WatchConfigSchema = z.object({
|
|
360
|
-
debounce_ms: z.number().int().positive().default(3e3),
|
|
361
|
-
storm_threshold: z.number().int().positive().default(50),
|
|
362
|
-
deep_storm_threshold: z.number().int().positive().default(500),
|
|
363
|
-
hard_timeout_ms: z.number().int().positive().default(3e5),
|
|
364
|
-
scope: z.enum(["paths", "full"]).default("paths"),
|
|
365
|
-
// Plan 3a hotfix 2026-05-02: refuse to start if the watch surface
|
|
366
|
-
// exceeds this many files. Prevents the misconfig pattern where
|
|
367
|
-
// `paths.source_dirs` includes `.` or otherwise expands to a 60K+
|
|
368
|
-
// file tree, producing 30-100% steady CPU. Override via
|
|
369
|
-
// `paths_full_root_opt_in: true` for users on small repos who genuinely
|
|
370
|
-
// need root-level watching.
|
|
371
|
-
max_watched_files: z.number().int().positive().default(1e4),
|
|
372
|
-
paths_full_root_opt_in: z.boolean().default(false)
|
|
373
|
-
}).passthrough().optional();
|
|
374
|
-
var AdapterLocalPathSchema = z.string().refine((s) => !/^([A-Za-z]:[\\/]|[\\/])/.test(s), {
|
|
375
|
-
message: "absolute paths are rejected; adapters.local entries must be relative to the massu.config.yaml directory"
|
|
376
|
-
}).refine((s) => !s.split(/[\\/]/).includes(".."), {
|
|
377
|
-
message: "parent-directory traversal (`..`) is rejected; adapters.local entries must stay inside the project tree"
|
|
378
|
-
}).transform((s) => s.split(/[\\/]/).filter((part) => part !== "" && part !== ".").join("/"));
|
|
379
|
-
var AdaptersConfigSchema = z.object({
|
|
380
|
-
enabled: z.boolean().default(false),
|
|
381
|
-
local: z.array(AdapterLocalPathSchema).default([])
|
|
382
|
-
}).passthrough().optional();
|
|
383
|
-
var TelemetryConfigSchema = z.object({
|
|
384
|
-
adapters: z.boolean().default(false)
|
|
385
|
-
}).passthrough().optional();
|
|
386
|
-
var LSPConfigSchema = z.object({
|
|
387
|
-
enabled: z.boolean().default(false),
|
|
388
|
-
servers: z.array(z.object({
|
|
389
|
-
language: z.string(),
|
|
390
|
-
command: z.string(),
|
|
391
|
-
// F-014 (closed 2026-05-06): explicit opt-in to spawn SUID/SGID
|
|
392
|
-
// binaries. Default false — argv[0] with the SUID bit is rejected
|
|
393
|
-
// unless this is true. Decision is auditable in the YAML.
|
|
394
|
-
allow_setuid: z.boolean().default(false),
|
|
395
|
-
// F-015 (closed 2026-05-06): per-server RSS budget (MB). Watchdog
|
|
396
|
-
// SIGKILLs the server after sustained breach. Default 1024 MB.
|
|
397
|
-
// Set to 0 to disable the watchdog for this server.
|
|
398
|
-
max_rss_mb: z.number().int().nonnegative().default(1024)
|
|
399
|
-
})).default([]),
|
|
400
|
-
autoDetect: z.object({
|
|
401
|
-
viaPortScan: z.boolean().default(false)
|
|
402
|
-
}).optional()
|
|
403
|
-
}).passthrough();
|
|
404
|
-
var RawConfigSchema = z.object({
|
|
405
|
-
schema_version: z.union([z.literal(1), z.literal(2)]).default(1),
|
|
406
|
-
project: z.object({
|
|
407
|
-
name: z.string().default("my-project"),
|
|
408
|
-
root: z.string().default("auto")
|
|
409
|
-
}).default({ name: "my-project", root: "auto" }),
|
|
410
|
-
framework: FrameworkConfigSchema.default({
|
|
411
|
-
type: "typescript",
|
|
412
|
-
router: "none",
|
|
413
|
-
orm: "none",
|
|
414
|
-
ui: "none"
|
|
415
|
-
}),
|
|
416
|
-
paths: PathsConfigSchema.default({ source: "src", aliases: { "@": "src" } }),
|
|
417
|
-
toolPrefix: z.string().default("massu"),
|
|
418
|
-
dbAccessPattern: z.string().optional(),
|
|
419
|
-
knownMismatches: z.record(z.string(), z.record(z.string(), z.string())).optional(),
|
|
420
|
-
accessScopes: z.array(z.string()).optional(),
|
|
421
|
-
domains: z.array(DomainConfigSchema).default([]),
|
|
422
|
-
rules: z.array(PatternRuleConfigSchema).default([]),
|
|
423
|
-
// P-M-036 (plan-stage-d-medium-sweep): customer-authored CR-style
|
|
424
|
-
// governance rules. DISTINCT from `rules:` above (path-scoped lint hints
|
|
425
|
-
// used by pattern-scanner). At config-refresh time these entries are
|
|
426
|
-
// loaded into the `knowledge_rules` SQLite table with
|
|
427
|
-
// `source = 'customer-config'` so `massu_knowledge_rule` and the
|
|
428
|
-
// governance docs surface customer-defined rules alongside framework CRs.
|
|
429
|
-
governance_rules: z.array(
|
|
430
|
-
z.object({
|
|
431
|
-
id: z.string().min(1, "governance_rules[].id is required"),
|
|
432
|
-
title: z.string().min(1, "governance_rules[].title is required"),
|
|
433
|
-
description: z.string().min(1, "governance_rules[].description is required"),
|
|
434
|
-
vr_type: z.string().default("VR-CUSTOM"),
|
|
435
|
-
reference_path: z.string().optional(),
|
|
436
|
-
severity: z.enum(["critical", "high", "medium", "low", "info"]).default("medium")
|
|
437
|
-
}).passthrough()
|
|
438
|
-
).default([]),
|
|
439
|
-
analytics: AnalyticsConfigSchema,
|
|
440
|
-
governance: GovernanceConfigSchema,
|
|
441
|
-
security: SecurityConfigSchema,
|
|
442
|
-
team: TeamConfigSchema,
|
|
443
|
-
regression: RegressionConfigSchema,
|
|
444
|
-
cloud: CloudConfigSchema,
|
|
445
|
-
conventions: ConventionsConfigSchema,
|
|
446
|
-
autoLearning: AutoLearningConfigSchema,
|
|
447
|
-
python: PythonConfigSchema,
|
|
448
|
-
// P2-004 / P2-005 / P2-006 / P2-008: v2 extensions (all optional)
|
|
449
|
-
verification: VerificationConfigSchema,
|
|
450
|
-
canonical_paths: CanonicalPathsSchema,
|
|
451
|
-
verification_types: VerificationTypesSchema,
|
|
452
|
-
detection: DetectionConfigSchema,
|
|
453
|
-
// Plan #2: detector-owned per-language conventions (free-form passthrough)
|
|
454
|
-
detected: DetectedConfigSchema,
|
|
455
|
-
// Plan 3a: file-watcher daemon tunables
|
|
456
|
-
watch: WatchConfigSchema,
|
|
457
|
-
// Plan 3c: third-party adapter registry kill-switch + signing override + local-path opt-in.
|
|
458
|
-
adapters: AdaptersConfigSchema,
|
|
459
|
-
// Plan 3c: anonymous adapter-discovery telemetry opt-in (default off).
|
|
460
|
-
telemetry: TelemetryConfigSchema,
|
|
461
|
-
// Plan 3b Phase 4: optional LSP enrichment of AST adapter results.
|
|
462
|
-
lsp: LSPConfigSchema.optional()
|
|
463
|
-
}).passthrough();
|
|
464
|
-
var _config = null;
|
|
465
|
-
var _resolvedApiKeySource = "none";
|
|
466
|
-
var _projectRoot = null;
|
|
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";
|
|
467
251
|
function findProjectRoot() {
|
|
468
252
|
const cwd = process.cwd();
|
|
469
253
|
let dir = cwd;
|
|
@@ -563,6 +347,7 @@ Hint: run \`massu config refresh\` to regenerate a valid config or fix the liste
|
|
|
563
347
|
team: parsed.team,
|
|
564
348
|
regression: parsed.regression,
|
|
565
349
|
cloud: parsed.cloud,
|
|
350
|
+
memory: parsed.memory,
|
|
566
351
|
conventions: parsed.conventions,
|
|
567
352
|
autoLearning: parsed.autoLearning,
|
|
568
353
|
python: parsed.python,
|
|
@@ -627,20 +412,1282 @@ function getResolvedPaths() {
|
|
|
627
412
|
settingsLocalPath: resolve2(root, claudeDirName, "settings.local.json")
|
|
628
413
|
};
|
|
629
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
|
+
});
|
|
630
1519
|
|
|
631
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
|
+
}
|
|
632
1608
|
function getMemoryDb() {
|
|
633
1609
|
const dbPath = getResolvedPaths().memoryDbPath;
|
|
634
|
-
const dir =
|
|
635
|
-
if (!
|
|
636
|
-
|
|
1610
|
+
const dir = dirname5(dbPath);
|
|
1611
|
+
if (!existsSync6(dir)) {
|
|
1612
|
+
mkdirSync4(dir, { recursive: true });
|
|
637
1613
|
}
|
|
638
|
-
const
|
|
1614
|
+
const preExisting = existsSync6(dbPath);
|
|
1615
|
+
const db = new Database2(dbPath);
|
|
639
1616
|
db.pragma("journal_mode = WAL");
|
|
640
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
|
+
}
|
|
641
1626
|
initMemorySchema(db);
|
|
1627
|
+
if (onDisk !== MEMORY_SCHEMA_VERSION) {
|
|
1628
|
+
db.pragma(`user_version = ${MEMORY_SCHEMA_VERSION}`);
|
|
1629
|
+
}
|
|
642
1630
|
return db;
|
|
643
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
|
+
}
|
|
644
1691
|
function migrateAuditLogCheckExtension(db) {
|
|
645
1692
|
const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='audit_log'").get();
|
|
646
1693
|
if (!row) return;
|
|
@@ -653,7 +1700,17 @@ function migrateAuditLogCheckExtension(db) {
|
|
|
653
1700
|
"compaction",
|
|
654
1701
|
"rule_candidate_emitted",
|
|
655
1702
|
"rule_promoted",
|
|
656
|
-
"rule_dismissed"
|
|
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"
|
|
657
1714
|
];
|
|
658
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);
|
|
659
1716
|
if (checkClauseMatch) {
|
|
@@ -670,7 +1727,9 @@ function migrateAuditLogCheckExtension(db) {
|
|
|
670
1727
|
timestamp TEXT DEFAULT (datetime('now')),
|
|
671
1728
|
event_type TEXT NOT NULL CHECK(event_type IN (
|
|
672
1729
|
'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
|
|
673
|
-
'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed'
|
|
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'
|
|
674
1733
|
)),
|
|
675
1734
|
actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
|
|
676
1735
|
model_id TEXT,
|
|
@@ -742,6 +1801,11 @@ function initMemorySchema(db) {
|
|
|
742
1801
|
importance INTEGER NOT NULL DEFAULT 3 CHECK(importance BETWEEN 1 AND 5),
|
|
743
1802
|
recurrence_count INTEGER NOT NULL DEFAULT 1,
|
|
744
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',
|
|
745
1809
|
created_at TEXT NOT NULL,
|
|
746
1810
|
created_at_epoch INTEGER NOT NULL,
|
|
747
1811
|
FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
@@ -840,6 +1904,27 @@ function initMemorySchema(db) {
|
|
|
840
1904
|
value TEXT NOT NULL
|
|
841
1905
|
);
|
|
842
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
|
+
`);
|
|
843
1928
|
db.exec(`
|
|
844
1929
|
CREATE TABLE IF NOT EXISTS conversation_turns (
|
|
845
1930
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -992,7 +2077,9 @@ function initMemorySchema(db) {
|
|
|
992
2077
|
timestamp TEXT DEFAULT (datetime('now')),
|
|
993
2078
|
event_type TEXT NOT NULL CHECK(event_type IN (
|
|
994
2079
|
'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
|
|
995
|
-
'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed'
|
|
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'
|
|
996
2083
|
)),
|
|
997
2084
|
actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
|
|
998
2085
|
model_id TEXT,
|
|
@@ -1012,6 +2099,17 @@ function initMemorySchema(db) {
|
|
|
1012
2099
|
ON audit_log (event_type, json_extract(metadata, '$.prompt_hash'))
|
|
1013
2100
|
WHERE event_type = 'rule_promoted';
|
|
1014
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
|
+
`);
|
|
1015
2113
|
db.exec(`
|
|
1016
2114
|
CREATE TABLE IF NOT EXISTS prompt_outcomes_signal_blacklist (
|
|
1017
2115
|
signal TEXT PRIMARY KEY,
|
|
@@ -1023,6 +2121,30 @@ function initMemorySchema(db) {
|
|
|
1023
2121
|
ON prompt_outcomes_signal_blacklist(dismissal_count DESC);
|
|
1024
2122
|
`);
|
|
1025
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
|
+
`);
|
|
1026
2148
|
db.exec(`
|
|
1027
2149
|
CREATE TABLE IF NOT EXISTS validation_results (
|
|
1028
2150
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -1244,6 +2366,55 @@ function initMemorySchema(db) {
|
|
|
1244
2366
|
if (!outboundCols.some((c) => c.name === "review_attestation_json")) {
|
|
1245
2367
|
db.exec(`ALTER TABLE team_promotion_outbound ADD COLUMN review_attestation_json TEXT`);
|
|
1246
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
|
+
`);
|
|
1247
2418
|
db.exec(`
|
|
1248
2419
|
CREATE TABLE IF NOT EXISTS failure_classes (
|
|
1249
2420
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -1263,6 +2434,20 @@ function initMemorySchema(db) {
|
|
|
1263
2434
|
CREATE INDEX IF NOT EXISTS idx_fc_name ON failure_classes(name);
|
|
1264
2435
|
CREATE INDEX IF NOT EXISTS idx_fc_needs_review ON failure_classes(needs_review);
|
|
1265
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);
|
|
1266
2451
|
}
|
|
1267
2452
|
function enqueueSyncPayload(db, payload) {
|
|
1268
2453
|
db.prepare("INSERT INTO pending_sync (payload) VALUES (?)").run(payload);
|
|
@@ -1274,6 +2459,69 @@ function getMemoryMeta(db, key) {
|
|
|
1274
2459
|
function setMemoryMeta(db, key, value) {
|
|
1275
2460
|
db.prepare("INSERT OR REPLACE INTO memory_meta (key, value) VALUES (?, ?)").run(key, value);
|
|
1276
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
|
+
}
|
|
1277
2525
|
function drainTeamPromotions(db) {
|
|
1278
2526
|
const rows = db.prepare(`
|
|
1279
2527
|
SELECT prompt_hash, destination, draft_text, score, signals_json, content_hash, hardened, review_attestation_json
|
|
@@ -1292,6 +2540,12 @@ function drainTeamPromotions(db) {
|
|
|
1292
2540
|
review_attestation: r.review_attestation_json ? safeJsonParse(r.review_attestation_json) : void 0
|
|
1293
2541
|
}));
|
|
1294
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
|
+
}
|
|
1295
2549
|
function drainTeamRevocations(db) {
|
|
1296
2550
|
const rows = db.prepare(
|
|
1297
2551
|
`SELECT prompt_hash FROM team_revocation_outbound ORDER BY created_at ASC LIMIT 1000`
|
|
@@ -1315,7 +2569,27 @@ function getRecurrenceCountForPromptHash(db, promptHash) {
|
|
|
1315
2569
|
return null;
|
|
1316
2570
|
}
|
|
1317
2571
|
}
|
|
1318
|
-
|
|
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
|
+
}
|
|
1319
2593
|
function drainRulePromotionEvents(db) {
|
|
1320
2594
|
const rows = db.prepare(`
|
|
1321
2595
|
SELECT id, prompt_hash, event_type, metadata_json, created_at
|
|
@@ -1393,9 +2667,33 @@ function incrementRetryCount(db, id, error) {
|
|
|
1393
2667
|
"UPDATE pending_sync SET retry_count = retry_count + 1, last_error = ? WHERE id = ?"
|
|
1394
2668
|
).run(error, id);
|
|
1395
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
|
+
}
|
|
1396
2694
|
function autoDetectTaskId(planFile) {
|
|
1397
2695
|
if (!planFile) return null;
|
|
1398
|
-
const base =
|
|
2696
|
+
const base = basename2(planFile);
|
|
1399
2697
|
return base.replace(/\.md$/, "");
|
|
1400
2698
|
}
|
|
1401
2699
|
function createSession(db, sessionId, opts) {
|
|
@@ -1412,6 +2710,85 @@ function endSession(db, sessionId, status = "completed") {
|
|
|
1412
2710
|
UPDATE sessions SET status = ?, ended_at = ?, ended_at_epoch = ? WHERE session_id = ?
|
|
1413
2711
|
`).run(status, now.toISOString(), Math.floor(now.getTime() / 1e3), sessionId);
|
|
1414
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
|
+
}
|
|
1415
2792
|
function addSummary(db, sessionId, summary) {
|
|
1416
2793
|
const now = /* @__PURE__ */ new Date();
|
|
1417
2794
|
db.prepare(`
|
|
@@ -1433,6 +2810,243 @@ function addSummary(db, sessionId, summary) {
|
|
|
1433
2810
|
Math.floor(now.getTime() / 1e3)
|
|
1434
2811
|
);
|
|
1435
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
|
+
}
|
|
1436
3050
|
function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantResponse, toolCallsJson, toolCallCount, promptTokens, responseTokens) {
|
|
1437
3051
|
const result = db.prepare(`
|
|
1438
3052
|
INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens)
|
|
@@ -1471,10 +3085,276 @@ function getLastProcessedLine(db, sessionId) {
|
|
|
1471
3085
|
function setLastProcessedLine(db, sessionId, lineNumber) {
|
|
1472
3086
|
db.prepare("INSERT OR REPLACE INTO memory_meta (key, value) VALUES (?, ?)").run(`last_processed_line:${sessionId}`, String(lineNumber));
|
|
1473
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/session-end.ts
|
|
3353
|
+
init_memory_db();
|
|
1474
3354
|
|
|
1475
3355
|
// src/session-archiver.ts
|
|
1476
|
-
import { existsSync as
|
|
1477
|
-
import { resolve as
|
|
3356
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync5, renameSync } from "fs";
|
|
3357
|
+
import { resolve as resolve5, dirname as dirname6 } from "path";
|
|
1478
3358
|
|
|
1479
3359
|
// src/session-state-generator.ts
|
|
1480
3360
|
function generateCurrentMd(db, sessionId) {
|
|
@@ -1619,19 +3499,20 @@ function safeParseJson(json, fallback) {
|
|
|
1619
3499
|
}
|
|
1620
3500
|
|
|
1621
3501
|
// src/session-archiver.ts
|
|
3502
|
+
init_config();
|
|
1622
3503
|
function archiveAndRegenerate(db, sessionId) {
|
|
1623
3504
|
const resolved = getResolvedPaths();
|
|
1624
3505
|
const currentMdPath = resolved.sessionStatePath;
|
|
1625
3506
|
const archiveDir = resolved.sessionArchivePath;
|
|
1626
3507
|
let archived = false;
|
|
1627
3508
|
let archivePath;
|
|
1628
|
-
if (
|
|
1629
|
-
const existingContent =
|
|
3509
|
+
if (existsSync7(currentMdPath)) {
|
|
3510
|
+
const existingContent = readFileSync4(currentMdPath, "utf-8");
|
|
1630
3511
|
if (existingContent.trim().length > 10) {
|
|
1631
3512
|
const { date, slug } = extractArchiveInfo(existingContent);
|
|
1632
|
-
archivePath =
|
|
1633
|
-
if (!
|
|
1634
|
-
|
|
3513
|
+
archivePath = resolve5(archiveDir, `${date}-${slug}.md`);
|
|
3514
|
+
if (!existsSync7(archiveDir)) {
|
|
3515
|
+
mkdirSync5(archiveDir, { recursive: true });
|
|
1635
3516
|
}
|
|
1636
3517
|
try {
|
|
1637
3518
|
renameSync(currentMdPath, archivePath);
|
|
@@ -1643,9 +3524,9 @@ function archiveAndRegenerate(db, sessionId) {
|
|
|
1643
3524
|
}
|
|
1644
3525
|
}
|
|
1645
3526
|
const newContent = generateCurrentMd(db, sessionId);
|
|
1646
|
-
const dir =
|
|
1647
|
-
if (!
|
|
1648
|
-
|
|
3527
|
+
const dir = dirname6(currentMdPath);
|
|
3528
|
+
if (!existsSync7(dir)) {
|
|
3529
|
+
mkdirSync5(dir, { recursive: true });
|
|
1649
3530
|
}
|
|
1650
3531
|
writeFileSync2(currentMdPath, newContent, "utf-8");
|
|
1651
3532
|
return { archived, archivePath, newContent };
|
|
@@ -1747,7 +3628,18 @@ function estimateTokens(text) {
|
|
|
1747
3628
|
return Math.ceil(text.length / 4);
|
|
1748
3629
|
}
|
|
1749
3630
|
|
|
3631
|
+
// src/cloud-sync.ts
|
|
3632
|
+
init_config();
|
|
3633
|
+
init_memory_db();
|
|
3634
|
+
|
|
3635
|
+
// src/observation-extractor.ts
|
|
3636
|
+
init_memory_db();
|
|
3637
|
+
|
|
3638
|
+
// src/adr-generator.ts
|
|
3639
|
+
init_config();
|
|
3640
|
+
|
|
1750
3641
|
// src/observation-extractor.ts
|
|
3642
|
+
init_config();
|
|
1751
3643
|
var PRIVATE_PATTERNS = [
|
|
1752
3644
|
/\/Users\/\w+/,
|
|
1753
3645
|
// Absolute macOS paths
|
|
@@ -1936,14 +3828,17 @@ async function drainSyncQueue(db) {
|
|
|
1936
3828
|
}
|
|
1937
3829
|
}
|
|
1938
3830
|
function sleep(ms) {
|
|
1939
|
-
return new Promise((
|
|
3831
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
1940
3832
|
}
|
|
1941
3833
|
|
|
1942
3834
|
// src/team-rule-sync.ts
|
|
1943
|
-
|
|
1944
|
-
import {
|
|
3835
|
+
init_config();
|
|
3836
|
+
import { existsSync as existsSync8, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, mkdirSync as mkdirSync6 } from "fs";
|
|
3837
|
+
import { join as join4, dirname as dirname7 } from "path";
|
|
1945
3838
|
|
|
1946
3839
|
// src/license.ts
|
|
3840
|
+
init_config();
|
|
3841
|
+
init_memory_db();
|
|
1947
3842
|
import { createHash } from "crypto";
|
|
1948
3843
|
|
|
1949
3844
|
// src/security/ed25519-envelope-verifier.ts
|
|
@@ -2145,6 +4040,10 @@ function getCachedOrgId(memDb) {
|
|
|
2145
4040
|
}
|
|
2146
4041
|
|
|
2147
4042
|
// src/auto-learning-entitlement.ts
|
|
4043
|
+
var AUTO_LEARNING_MIN_TIER = "pro";
|
|
4044
|
+
function entitledForAutoLearning(tier) {
|
|
4045
|
+
return tierLevel(tier) >= tierLevel(AUTO_LEARNING_MIN_TIER);
|
|
4046
|
+
}
|
|
2148
4047
|
var TEAM_SHARED_PROMOTION_MIN_TIER = "team";
|
|
2149
4048
|
function entitledForTeamSharedPromotion(tier) {
|
|
2150
4049
|
return tierLevel(tier) >= tierLevel(TEAM_SHARED_PROMOTION_MIN_TIER);
|
|
@@ -2170,6 +4069,22 @@ function verifyPromotionEnvelope(payload) {
|
|
|
2170
4069
|
);
|
|
2171
4070
|
}
|
|
2172
4071
|
|
|
4072
|
+
// src/lib/fileLock.ts
|
|
4073
|
+
import * as lockfile from "proper-lockfile";
|
|
4074
|
+
|
|
4075
|
+
// src/rule-candidate-applier.ts
|
|
4076
|
+
init_config();
|
|
4077
|
+
init_memory_path();
|
|
4078
|
+
|
|
4079
|
+
// src/audit-trail.ts
|
|
4080
|
+
init_config();
|
|
4081
|
+
|
|
4082
|
+
// src/rule-candidate-applier.ts
|
|
4083
|
+
init_memory_db();
|
|
4084
|
+
|
|
4085
|
+
// src/rule-candidate-funnel.ts
|
|
4086
|
+
init_memory_db();
|
|
4087
|
+
|
|
2173
4088
|
// src/rule-candidate-hardened.ts
|
|
2174
4089
|
var TEAM_HARDENED_SHAREABLE_DESTINATIONS = [
|
|
2175
4090
|
"pattern-scanner",
|
|
@@ -2189,6 +4104,7 @@ function isTeamShareableDestination(destination) {
|
|
|
2189
4104
|
}
|
|
2190
4105
|
|
|
2191
4106
|
// src/team-knowledge.ts
|
|
4107
|
+
init_config();
|
|
2192
4108
|
function shareObservation(db, developerId, project, observationType, summary, opts) {
|
|
2193
4109
|
const result = db.prepare(`
|
|
2194
4110
|
INSERT INTO shared_observations
|
|
@@ -2208,6 +4124,7 @@ function shareObservation(db, developerId, project, observationType, summary, op
|
|
|
2208
4124
|
}
|
|
2209
4125
|
|
|
2210
4126
|
// src/team-rule-sync.ts
|
|
4127
|
+
init_memory_db();
|
|
2211
4128
|
var CURSOR_KEY = "team_promotions_cursor";
|
|
2212
4129
|
var DEFAULT_TIMEOUT_MS = 2e3;
|
|
2213
4130
|
var PROMPT_HASH_RE = /^[0-9a-f]{16}$/;
|
|
@@ -2286,7 +4203,7 @@ async function pullTeamPromotions(db, opts = {}) {
|
|
|
2286
4203
|
result.revoked_handled += 1;
|
|
2287
4204
|
continue;
|
|
2288
4205
|
}
|
|
2289
|
-
if (
|
|
4206
|
+
if (existsSync8(candidatePath) || alreadyApplied(db, p.prompt_hash)) {
|
|
2290
4207
|
result.skipped += 1;
|
|
2291
4208
|
continue;
|
|
2292
4209
|
}
|
|
@@ -2322,7 +4239,7 @@ function isValidWirePromotion(p) {
|
|
|
2322
4239
|
return typeof r.prompt_hash === "string" && PROMPT_HASH_RE.test(r.prompt_hash) && typeof r.destination === "string" && typeof r.draft_text === "string" && typeof r.promoted_by === "string" && typeof r.promoted_at === "string";
|
|
2323
4240
|
}
|
|
2324
4241
|
function sidecarPath(projectRoot, promptHash) {
|
|
2325
|
-
return
|
|
4242
|
+
return join4(projectRoot, ".massu", "rule-candidates", `${promptHash}.json`);
|
|
2326
4243
|
}
|
|
2327
4244
|
function alreadyApplied(db, promptHash) {
|
|
2328
4245
|
try {
|
|
@@ -2336,9 +4253,9 @@ function alreadyApplied(db, promptHash) {
|
|
|
2336
4253
|
}
|
|
2337
4254
|
}
|
|
2338
4255
|
function handleRevocation(db, projectRoot, candidatePath, promptHash) {
|
|
2339
|
-
if (
|
|
4256
|
+
if (existsSync8(candidatePath)) {
|
|
2340
4257
|
try {
|
|
2341
|
-
|
|
4258
|
+
unlinkSync2(candidatePath);
|
|
2342
4259
|
} catch {
|
|
2343
4260
|
}
|
|
2344
4261
|
return;
|
|
@@ -2384,8 +4301,8 @@ function materializeCandidate(db, projectRoot, candidatePath, p, orgId) {
|
|
|
2384
4301
|
draft_text: p.draft_text,
|
|
2385
4302
|
...p.review_attestation !== void 0 ? { publisher_review_attestation: p.review_attestation } : {}
|
|
2386
4303
|
};
|
|
2387
|
-
const dir =
|
|
2388
|
-
if (!
|
|
4304
|
+
const dir = dirname7(candidatePath);
|
|
4305
|
+
if (!existsSync8(dir)) mkdirSync6(dir, { recursive: true });
|
|
2389
4306
|
writeFileSync3(candidatePath, JSON.stringify(sidecar, null, 2), "utf-8");
|
|
2390
4307
|
try {
|
|
2391
4308
|
shareObservation(db, p.promoted_by, getProjectName(), "rule_promotion", promptText, {
|
|
@@ -2429,7 +4346,11 @@ function emitDropTelemetry(db, eventType, data) {
|
|
|
2429
4346
|
);
|
|
2430
4347
|
}
|
|
2431
4348
|
|
|
4349
|
+
// src/hooks/session-end.ts
|
|
4350
|
+
init_config();
|
|
4351
|
+
|
|
2432
4352
|
// src/analytics.ts
|
|
4353
|
+
init_config();
|
|
2433
4354
|
var DEFAULT_WEIGHTS = {
|
|
2434
4355
|
bug_found: -5,
|
|
2435
4356
|
vr_failure: -10,
|
|
@@ -2504,6 +4425,7 @@ function backfillQualityScores(db) {
|
|
|
2504
4425
|
}
|
|
2505
4426
|
|
|
2506
4427
|
// src/cost-tracker.ts
|
|
4428
|
+
init_config();
|
|
2507
4429
|
var DEFAULT_MODEL_PRICING = {
|
|
2508
4430
|
"claude-opus-4-6": { input_per_million: 15, output_per_million: 75, cache_read_per_million: 1.5, cache_write_per_million: 18.75 },
|
|
2509
4431
|
"claude-sonnet-4-6": { input_per_million: 3, output_per_million: 15, cache_read_per_million: 0.3, cache_write_per_million: 3.75 },
|
|
@@ -2574,6 +4496,7 @@ function storeSessionCost(db, sessionId, usage, cost) {
|
|
|
2574
4496
|
}
|
|
2575
4497
|
|
|
2576
4498
|
// src/prompt-analyzer.ts
|
|
4499
|
+
init_config();
|
|
2577
4500
|
import { createHash as createHash2 } from "crypto";
|
|
2578
4501
|
|
|
2579
4502
|
// src/security-utils.ts
|
|
@@ -2678,12 +4601,1040 @@ function analyzeSessionPrompts(db, sessionId) {
|
|
|
2678
4601
|
return stored;
|
|
2679
4602
|
}
|
|
2680
4603
|
|
|
4604
|
+
// src/memory-supersede.ts
|
|
4605
|
+
init_config();
|
|
4606
|
+
init_memory_embedder();
|
|
4607
|
+
init_memory_vector();
|
|
4608
|
+
|
|
4609
|
+
// src/memory-hybrid-search.ts
|
|
4610
|
+
init_memory_db();
|
|
4611
|
+
init_memory_vector();
|
|
4612
|
+
function temporalPredicate(asOf, includeSuperseded, prefix) {
|
|
4613
|
+
const c = (name) => `${prefix}${name}`;
|
|
4614
|
+
if (asOf != null && Number.isFinite(asOf)) {
|
|
4615
|
+
const t = Math.floor(Number(asOf) / 1e3);
|
|
4616
|
+
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})`;
|
|
4617
|
+
}
|
|
4618
|
+
if (!includeSuperseded) {
|
|
4619
|
+
return `${c("expired_at_epoch")} IS NULL`;
|
|
4620
|
+
}
|
|
4621
|
+
return "";
|
|
4622
|
+
}
|
|
4623
|
+
var RRF_K = 10;
|
|
4624
|
+
var RECENCY_HALF_LIFE_DAYS = 180;
|
|
4625
|
+
var DEFAULT_LIMIT = 8;
|
|
4626
|
+
var DEFAULT_POOL = 30;
|
|
4627
|
+
var ALL_SOURCES = [
|
|
4628
|
+
"observation",
|
|
4629
|
+
"architecture_decision",
|
|
4630
|
+
"knowledge_chunk",
|
|
4631
|
+
"failure_class"
|
|
4632
|
+
];
|
|
4633
|
+
function snippetOf(text, max = 160) {
|
|
4634
|
+
if (!text) return "";
|
|
4635
|
+
const clean = text.replace(/\s+/g, " ").trim();
|
|
4636
|
+
return clean.length > max ? clean.slice(0, max - 1) + "\u2026" : clean;
|
|
4637
|
+
}
|
|
4638
|
+
function ageDaysFrom(epochMs, nowMs) {
|
|
4639
|
+
const days = (nowMs - epochMs) / 864e5;
|
|
4640
|
+
return days < 0 ? 0 : days;
|
|
4641
|
+
}
|
|
4642
|
+
function recencyWeight(ageDays) {
|
|
4643
|
+
const freshness = Math.pow(0.5, ageDays / RECENCY_HALF_LIFE_DAYS);
|
|
4644
|
+
return 0.7 + 0.3 * freshness;
|
|
4645
|
+
}
|
|
4646
|
+
function importanceWeightOf(importance) {
|
|
4647
|
+
const clamped = Math.max(1, Math.min(5, importance));
|
|
4648
|
+
return 0.7 + 0.3 * (clamped / 5);
|
|
4649
|
+
}
|
|
4650
|
+
function collectObservations(memDb, queryText, pool, nowMs, loadVec, modelId, dim, asOf, includeSuperseded) {
|
|
4651
|
+
const map = /* @__PURE__ */ new Map();
|
|
4652
|
+
const bm25Temporal = temporalPredicate(asOf, includeSuperseded, "o.");
|
|
4653
|
+
const plainTemporal = temporalPredicate(asOf, includeSuperseded, "");
|
|
4654
|
+
try {
|
|
4655
|
+
const rows = memDb.prepare(
|
|
4656
|
+
`SELECT o.id, o.title, o.detail, o.importance, o.created_at_epoch
|
|
4657
|
+
FROM observations_fts
|
|
4658
|
+
JOIN observations o ON observations_fts.rowid = o.id
|
|
4659
|
+
WHERE observations_fts MATCH ?${bm25Temporal ? ` AND ${bm25Temporal}` : ""}
|
|
4660
|
+
ORDER BY rank LIMIT ?`
|
|
4661
|
+
).all(sanitizeFts5QueryOr(queryText), pool);
|
|
4662
|
+
rows.forEach((r, i) => {
|
|
4663
|
+
map.set(r.id, {
|
|
4664
|
+
id: r.id,
|
|
4665
|
+
source: "observation",
|
|
4666
|
+
title: r.title,
|
|
4667
|
+
snippet: snippetOf(r.detail ?? r.title),
|
|
4668
|
+
importance: r.importance,
|
|
4669
|
+
ageDays: ageDaysFrom(r.created_at_epoch * 1e3, nowMs),
|
|
4670
|
+
bm25Order: i,
|
|
4671
|
+
vecs: []
|
|
4672
|
+
});
|
|
4673
|
+
});
|
|
4674
|
+
} catch {
|
|
4675
|
+
}
|
|
4676
|
+
const recent = memDb.prepare(
|
|
4677
|
+
`SELECT id, title, detail, importance, created_at_epoch
|
|
4678
|
+
FROM observations${plainTemporal ? ` WHERE ${plainTemporal}` : ""} ORDER BY created_at_epoch DESC LIMIT ?`
|
|
4679
|
+
).all(pool);
|
|
4680
|
+
for (const r of recent) {
|
|
4681
|
+
if (!map.has(r.id)) {
|
|
4682
|
+
map.set(r.id, {
|
|
4683
|
+
id: r.id,
|
|
4684
|
+
source: "observation",
|
|
4685
|
+
title: r.title,
|
|
4686
|
+
snippet: snippetOf(r.detail ?? r.title),
|
|
4687
|
+
importance: r.importance,
|
|
4688
|
+
ageDays: ageDaysFrom(r.created_at_epoch * 1e3, nowMs),
|
|
4689
|
+
bm25Order: null,
|
|
4690
|
+
vecs: []
|
|
4691
|
+
});
|
|
4692
|
+
}
|
|
4693
|
+
}
|
|
4694
|
+
if (loadVec && map.size > 0) {
|
|
4695
|
+
loadEmbeddings(
|
|
4696
|
+
memDb,
|
|
4697
|
+
"observation_embeddings",
|
|
4698
|
+
"observation_id",
|
|
4699
|
+
[...map.keys()],
|
|
4700
|
+
modelId,
|
|
4701
|
+
dim,
|
|
4702
|
+
(id, vec) => {
|
|
4703
|
+
const c = map.get(id);
|
|
4704
|
+
if (c) c.vecs.push(vec);
|
|
4705
|
+
}
|
|
4706
|
+
);
|
|
4707
|
+
}
|
|
4708
|
+
return [...map.values()];
|
|
4709
|
+
}
|
|
4710
|
+
function collectLikeSource(db, source, cfg, queryText, pool, nowMs) {
|
|
4711
|
+
const map = /* @__PURE__ */ new Map();
|
|
4712
|
+
const tokens = queryText.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3).slice(0, 8);
|
|
4713
|
+
const ageExpr = (created) => {
|
|
4714
|
+
const ms = cfg.createdIsEpoch ? Number(created) * 1e3 : Date.parse(String(created));
|
|
4715
|
+
return Number.isFinite(ms) ? ageDaysFrom(ms, nowMs) : 0;
|
|
4716
|
+
};
|
|
4717
|
+
if (tokens.length > 0) {
|
|
4718
|
+
const likeClause = cfg.searchCols.map((col) => tokens.map(() => `${col} LIKE ?`).join(" OR ")).join(" OR ");
|
|
4719
|
+
const params = [];
|
|
4720
|
+
for (const _col of cfg.searchCols) for (const t of tokens) params.push(`%${t}%`);
|
|
4721
|
+
try {
|
|
4722
|
+
const rows = db.prepare(
|
|
4723
|
+
`SELECT ${cfg.idCol} AS id, ${cfg.titleExpr} AS title,
|
|
4724
|
+
${cfg.snippetCol} AS snippet, ${cfg.createdCol} AS created
|
|
4725
|
+
FROM ${cfg.table} WHERE (${likeClause})${cfg.temporalClause ? ` AND ${cfg.temporalClause}` : ""}
|
|
4726
|
+
ORDER BY ${cfg.createdCol} DESC LIMIT ?`
|
|
4727
|
+
).all(...params, pool);
|
|
4728
|
+
rows.forEach((r, i) => {
|
|
4729
|
+
map.set(r.id, {
|
|
4730
|
+
id: r.id,
|
|
4731
|
+
source,
|
|
4732
|
+
title: r.title,
|
|
4733
|
+
snippet: snippetOf(r.snippet ?? r.title),
|
|
4734
|
+
importance: 3,
|
|
4735
|
+
ageDays: ageExpr(r.created),
|
|
4736
|
+
bm25Order: i,
|
|
4737
|
+
vecs: []
|
|
4738
|
+
});
|
|
4739
|
+
});
|
|
4740
|
+
} catch {
|
|
4741
|
+
}
|
|
4742
|
+
}
|
|
4743
|
+
const recent = db.prepare(
|
|
4744
|
+
`SELECT ${cfg.idCol} AS id, ${cfg.titleExpr} AS title,
|
|
4745
|
+
${cfg.snippetCol} AS snippet, ${cfg.createdCol} AS created
|
|
4746
|
+
FROM ${cfg.table}${cfg.temporalClause ? ` WHERE ${cfg.temporalClause}` : ""} ORDER BY ${cfg.createdCol} DESC LIMIT ?`
|
|
4747
|
+
).all(pool);
|
|
4748
|
+
for (const r of recent) {
|
|
4749
|
+
if (!map.has(r.id)) {
|
|
4750
|
+
map.set(r.id, {
|
|
4751
|
+
id: r.id,
|
|
4752
|
+
source,
|
|
4753
|
+
title: r.title,
|
|
4754
|
+
snippet: snippetOf(r.snippet ?? r.title),
|
|
4755
|
+
importance: 3,
|
|
4756
|
+
ageDays: ageExpr(r.created),
|
|
4757
|
+
bm25Order: null,
|
|
4758
|
+
vecs: []
|
|
4759
|
+
});
|
|
4760
|
+
}
|
|
4761
|
+
}
|
|
4762
|
+
return [...map.values()];
|
|
4763
|
+
}
|
|
4764
|
+
function collectKnowledgeChunks(knowledgeDb, queryText, pool, nowMs, loadVec, modelId, dim) {
|
|
4765
|
+
const map = /* @__PURE__ */ new Map();
|
|
4766
|
+
try {
|
|
4767
|
+
const rows = knowledgeDb.prepare(
|
|
4768
|
+
`SELECT kc.id AS id, kc.heading AS heading, kc.content AS content,
|
|
4769
|
+
kd.indexed_at_epoch AS epoch
|
|
4770
|
+
FROM knowledge_fts
|
|
4771
|
+
JOIN knowledge_chunks kc ON knowledge_fts.rowid = kc.id
|
|
4772
|
+
JOIN knowledge_documents kd ON kd.id = kc.document_id
|
|
4773
|
+
WHERE knowledge_fts MATCH ?
|
|
4774
|
+
ORDER BY rank LIMIT ?`
|
|
4775
|
+
).all(sanitizeFts5QueryOr(queryText), pool);
|
|
4776
|
+
rows.forEach((r, i) => {
|
|
4777
|
+
map.set(r.id, {
|
|
4778
|
+
id: r.id,
|
|
4779
|
+
source: "knowledge_chunk",
|
|
4780
|
+
title: r.heading || snippetOf(r.content, 60),
|
|
4781
|
+
snippet: snippetOf(r.content),
|
|
4782
|
+
importance: 3,
|
|
4783
|
+
ageDays: ageDaysFrom((r.epoch ?? 0) * 1e3, nowMs),
|
|
4784
|
+
bm25Order: i,
|
|
4785
|
+
vecs: []
|
|
4786
|
+
});
|
|
4787
|
+
});
|
|
4788
|
+
} catch {
|
|
4789
|
+
}
|
|
4790
|
+
if (loadVec && map.size > 0) {
|
|
4791
|
+
loadEmbeddings(
|
|
4792
|
+
knowledgeDb,
|
|
4793
|
+
"knowledge_chunk_embeddings",
|
|
4794
|
+
"chunk_id",
|
|
4795
|
+
[...map.keys()],
|
|
4796
|
+
modelId,
|
|
4797
|
+
dim,
|
|
4798
|
+
(id, vec) => {
|
|
4799
|
+
const c = map.get(id);
|
|
4800
|
+
if (c) c.vecs.push(vec);
|
|
4801
|
+
}
|
|
4802
|
+
);
|
|
4803
|
+
}
|
|
4804
|
+
return [...map.values()];
|
|
4805
|
+
}
|
|
4806
|
+
function loadEmbeddings(db, table, idCol, ids, modelId, dim, assign) {
|
|
4807
|
+
if (!modelId || !dim || ids.length === 0) return;
|
|
4808
|
+
try {
|
|
4809
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
4810
|
+
const rows = db.prepare(
|
|
4811
|
+
`SELECT ${idCol} AS id, vec FROM ${table}
|
|
4812
|
+
WHERE ${idCol} IN (${placeholders}) AND model_id = ? AND dim = ?
|
|
4813
|
+
LIMIT ?`
|
|
4814
|
+
).all(...ids, modelId, dim, ids.length);
|
|
4815
|
+
for (const r of rows) {
|
|
4816
|
+
const v = blobToFloat32(r.vec);
|
|
4817
|
+
if (v && v.length === dim) assign(r.id, v);
|
|
4818
|
+
}
|
|
4819
|
+
} catch {
|
|
4820
|
+
}
|
|
4821
|
+
}
|
|
4822
|
+
function hybridSearch(memDb, knowledgeDb, opts) {
|
|
4823
|
+
const sources = opts.sources ?? ALL_SOURCES;
|
|
4824
|
+
const limit = opts.limit ?? DEFAULT_LIMIT;
|
|
4825
|
+
const minScore = opts.minScore ?? 0;
|
|
4826
|
+
const pool = opts.candidatePool ?? DEFAULT_POOL;
|
|
4827
|
+
const nowMs = opts.now ?? Date.now();
|
|
4828
|
+
const queryVec = opts.queryVec ?? null;
|
|
4829
|
+
const modelId = queryVec ? opts.modelId ?? null : null;
|
|
4830
|
+
const dim = queryVec ? opts.dim ?? null : null;
|
|
4831
|
+
const loadVec = !!(queryVec && modelId && dim);
|
|
4832
|
+
const includeSuperseded = opts.includeSuperseded ?? false;
|
|
4833
|
+
let candidates = [];
|
|
4834
|
+
if (sources.includes("observation")) {
|
|
4835
|
+
candidates = candidates.concat(
|
|
4836
|
+
collectObservations(
|
|
4837
|
+
memDb,
|
|
4838
|
+
opts.queryText,
|
|
4839
|
+
pool,
|
|
4840
|
+
nowMs,
|
|
4841
|
+
loadVec,
|
|
4842
|
+
modelId,
|
|
4843
|
+
dim,
|
|
4844
|
+
opts.asOf,
|
|
4845
|
+
includeSuperseded
|
|
4846
|
+
)
|
|
4847
|
+
);
|
|
4848
|
+
}
|
|
4849
|
+
if (sources.includes("architecture_decision")) {
|
|
4850
|
+
candidates = candidates.concat(
|
|
4851
|
+
collectLikeSource(
|
|
4852
|
+
memDb,
|
|
4853
|
+
"architecture_decision",
|
|
4854
|
+
{
|
|
4855
|
+
table: "architecture_decisions",
|
|
4856
|
+
idCol: "id",
|
|
4857
|
+
titleExpr: "title",
|
|
4858
|
+
snippetCol: "decision",
|
|
4859
|
+
searchCols: ["title", "decision", "context"],
|
|
4860
|
+
createdCol: "created_at",
|
|
4861
|
+
createdIsEpoch: false,
|
|
4862
|
+
temporalClause: temporalPredicate(opts.asOf, includeSuperseded, "")
|
|
4863
|
+
},
|
|
4864
|
+
opts.queryText,
|
|
4865
|
+
pool,
|
|
4866
|
+
nowMs
|
|
4867
|
+
)
|
|
4868
|
+
);
|
|
4869
|
+
}
|
|
4870
|
+
if (sources.includes("failure_class")) {
|
|
4871
|
+
candidates = candidates.concat(
|
|
4872
|
+
collectLikeSource(
|
|
4873
|
+
memDb,
|
|
4874
|
+
"failure_class",
|
|
4875
|
+
{
|
|
4876
|
+
table: "failure_classes",
|
|
4877
|
+
idCol: "id",
|
|
4878
|
+
titleExpr: "name",
|
|
4879
|
+
snippetCol: "description",
|
|
4880
|
+
searchCols: ["name", "description", "known_message"],
|
|
4881
|
+
createdCol: "created_at",
|
|
4882
|
+
createdIsEpoch: false
|
|
4883
|
+
},
|
|
4884
|
+
opts.queryText,
|
|
4885
|
+
pool,
|
|
4886
|
+
nowMs
|
|
4887
|
+
)
|
|
4888
|
+
);
|
|
4889
|
+
}
|
|
4890
|
+
if (sources.includes("knowledge_chunk") && knowledgeDb) {
|
|
4891
|
+
candidates = candidates.concat(
|
|
4892
|
+
collectKnowledgeChunks(knowledgeDb, opts.queryText, pool, nowMs, loadVec, modelId, dim)
|
|
4893
|
+
);
|
|
4894
|
+
}
|
|
4895
|
+
if (candidates.length === 0) return [];
|
|
4896
|
+
const worstBm25 = pool + 1;
|
|
4897
|
+
let cosineOrder = null;
|
|
4898
|
+
if (loadVec && queryVec) {
|
|
4899
|
+
const scored = candidates.map((c) => ({
|
|
4900
|
+
key: `${c.source}:${c.id}`,
|
|
4901
|
+
// MAX-POOL: score the record by its BEST-matching passage.
|
|
4902
|
+
sim: c.vecs.length ? Math.max(...c.vecs.map((v) => cosineSim(queryVec, v))) : -Infinity
|
|
4903
|
+
})).filter((s) => s.sim > -Infinity).sort((a, b) => b.sim - a.sim);
|
|
4904
|
+
cosineOrder = /* @__PURE__ */ new Map();
|
|
4905
|
+
scored.forEach((s, i) => cosineOrder.set(s.key, i));
|
|
4906
|
+
}
|
|
4907
|
+
const worstCosine = candidates.length + 1;
|
|
4908
|
+
const ranked = candidates.map((c) => {
|
|
4909
|
+
const key = `${c.source}:${c.id}`;
|
|
4910
|
+
const bm25Rank = c.bm25Order ?? worstBm25;
|
|
4911
|
+
let rrf = 1 / (RRF_K + bm25Rank);
|
|
4912
|
+
if (cosineOrder) {
|
|
4913
|
+
const cRank = cosineOrder.has(key) ? cosineOrder.get(key) : worstCosine;
|
|
4914
|
+
rrf += 1 / (RRF_K + cRank);
|
|
4915
|
+
}
|
|
4916
|
+
const score = rrf * importanceWeightOf(c.importance) * recencyWeight(c.ageDays);
|
|
4917
|
+
return {
|
|
4918
|
+
id: c.id,
|
|
4919
|
+
source: c.source,
|
|
4920
|
+
title: c.title,
|
|
4921
|
+
snippet: c.snippet,
|
|
4922
|
+
score,
|
|
4923
|
+
importance: c.importance,
|
|
4924
|
+
ageDays: c.ageDays
|
|
4925
|
+
};
|
|
4926
|
+
});
|
|
4927
|
+
const top = ranked.filter((r) => r.score >= minScore).sort((a, b) => b.score - a.score || a.ageDays - b.ageDays).slice(0, limit);
|
|
4928
|
+
if (includeSuperseded && opts.asOf == null) {
|
|
4929
|
+
for (const r of top) {
|
|
4930
|
+
const table = r.source === "observation" ? "observations" : r.source === "architecture_decision" ? "architecture_decisions" : null;
|
|
4931
|
+
if (!table) continue;
|
|
4932
|
+
try {
|
|
4933
|
+
const row = memDb.prepare(`SELECT valid_to, superseded_by, expired_at_epoch FROM ${table} WHERE id = ?`).get(r.id);
|
|
4934
|
+
if (row && row.expired_at_epoch != null) {
|
|
4935
|
+
const when = row.valid_to ? row.valid_to.slice(0, 10) : "an earlier date";
|
|
4936
|
+
const by = row.superseded_by != null ? ` by #${row.superseded_by}` : "";
|
|
4937
|
+
r.snippet = `(superseded on ${when}${by}) ${r.snippet}`;
|
|
4938
|
+
}
|
|
4939
|
+
} catch {
|
|
4940
|
+
}
|
|
4941
|
+
}
|
|
4942
|
+
}
|
|
4943
|
+
return top;
|
|
4944
|
+
}
|
|
4945
|
+
|
|
4946
|
+
// src/memory-supersede.ts
|
|
4947
|
+
init_memory_db();
|
|
4948
|
+
var DEFAULT_CONFIG = {
|
|
4949
|
+
enabled: true,
|
|
4950
|
+
similarityThreshold: 0.6,
|
|
4951
|
+
dedupThreshold: 0.93,
|
|
4952
|
+
gatedTypes: ["decision", "cr_violation", "failed_attempt"],
|
|
4953
|
+
annotateSuperseded: false,
|
|
4954
|
+
budgetMs: 800
|
|
4955
|
+
};
|
|
4956
|
+
var REPLACEMENT_SIGNAL = /\b(instead of|no longer|not\s+\w+\s+anymore|switch(?:ing|ed)?\s+(?:from|to)|replac(?:e|es|ed|ing)|deprecat(?:e|ed|es)|supersed(?:e|ed|es)|now\s+(?:we|use|using)|actually|correction|revert(?:ed|ing)?|changed?\s+to|moved?\s+(?:from|to)|abandon(?:ed)?|rolled?\s+back)\b/i;
|
|
4957
|
+
function hasReplacementSignal(text) {
|
|
4958
|
+
return REPLACEMENT_SIGNAL.test(text);
|
|
4959
|
+
}
|
|
4960
|
+
function resolveContradictionConfig() {
|
|
4961
|
+
try {
|
|
4962
|
+
const c = getConfig().memory?.contradiction;
|
|
4963
|
+
if (!c) return { ...DEFAULT_CONFIG };
|
|
4964
|
+
return { ...DEFAULT_CONFIG, ...c };
|
|
4965
|
+
} catch {
|
|
4966
|
+
return { ...DEFAULT_CONFIG };
|
|
4967
|
+
}
|
|
4968
|
+
}
|
|
4969
|
+
function judgeContradiction(newText, candidates, cfg) {
|
|
4970
|
+
if (candidates.length === 0) {
|
|
4971
|
+
return { op: "ADD", reason: "no related candidates" };
|
|
4972
|
+
}
|
|
4973
|
+
const top = [...candidates].sort((a, b) => b.cosine - a.cosine)[0];
|
|
4974
|
+
if (top.cosine >= cfg.dedupThreshold) {
|
|
4975
|
+
return { op: "NOOP", targetId: top.id, targetSource: top.source, reason: `near-duplicate (cos=${top.cosine.toFixed(3)})` };
|
|
4976
|
+
}
|
|
4977
|
+
if (top.cosine >= cfg.similarityThreshold && hasReplacementSignal(newText)) {
|
|
4978
|
+
return {
|
|
4979
|
+
op: "UPDATE",
|
|
4980
|
+
targetId: top.id,
|
|
4981
|
+
targetSource: top.source,
|
|
4982
|
+
reason: `contradiction: related (cos=${top.cosine.toFixed(3)}) + replacement signal`
|
|
4983
|
+
};
|
|
4984
|
+
}
|
|
4985
|
+
return { op: "ADD", reason: `related but no contradiction (top cos=${top.cosine.toFixed(3)})` };
|
|
4986
|
+
}
|
|
4987
|
+
async function judgeViaEndpoint(endpoint, newText, candidates, budgetMs) {
|
|
4988
|
+
try {
|
|
4989
|
+
const controller = new AbortController();
|
|
4990
|
+
const timer = setTimeout(() => controller.abort(), budgetMs);
|
|
4991
|
+
let resp;
|
|
4992
|
+
try {
|
|
4993
|
+
resp = await fetch(endpoint, {
|
|
4994
|
+
method: "POST",
|
|
4995
|
+
headers: { "content-type": "application/json" },
|
|
4996
|
+
body: JSON.stringify({ text: newText, candidates }),
|
|
4997
|
+
signal: controller.signal
|
|
4998
|
+
});
|
|
4999
|
+
} finally {
|
|
5000
|
+
clearTimeout(timer);
|
|
5001
|
+
}
|
|
5002
|
+
if (!resp.ok) return null;
|
|
5003
|
+
const data = await resp.json();
|
|
5004
|
+
if (data.op !== "ADD" && data.op !== "UPDATE" && data.op !== "NOOP") return null;
|
|
5005
|
+
if (data.op === "UPDATE" && typeof data.targetId !== "number") return null;
|
|
5006
|
+
const match = candidates.find((c) => c.id === data.targetId);
|
|
5007
|
+
return {
|
|
5008
|
+
op: data.op,
|
|
5009
|
+
targetId: data.op === "UPDATE" ? data.targetId : void 0,
|
|
5010
|
+
targetSource: match?.source,
|
|
5011
|
+
reason: "external judge"
|
|
5012
|
+
};
|
|
5013
|
+
} catch {
|
|
5014
|
+
return null;
|
|
5015
|
+
}
|
|
5016
|
+
}
|
|
5017
|
+
async function candidateVec(memDb, source, id) {
|
|
5018
|
+
try {
|
|
5019
|
+
if (source === "observation") {
|
|
5020
|
+
const row = memDb.prepare(
|
|
5021
|
+
`SELECT vec FROM observation_embeddings
|
|
5022
|
+
WHERE observation_id = ? ORDER BY chunk_ix ASC LIMIT 1`
|
|
5023
|
+
).get(id);
|
|
5024
|
+
if (row?.vec) {
|
|
5025
|
+
const v = blobToFloat32(row.vec);
|
|
5026
|
+
if (v) return v;
|
|
5027
|
+
}
|
|
5028
|
+
const obs = memDb.prepare(`SELECT title, detail FROM observations WHERE id = ?`).get(id);
|
|
5029
|
+
if (!obs) return null;
|
|
5030
|
+
return await embed(`${obs.title}
|
|
5031
|
+
${obs.detail ?? ""}`.trim());
|
|
5032
|
+
}
|
|
5033
|
+
const ad = memDb.prepare(`SELECT title, decision FROM architecture_decisions WHERE id = ?`).get(id);
|
|
5034
|
+
if (!ad) return null;
|
|
5035
|
+
return await embed(`${ad.title}
|
|
5036
|
+
${ad.decision}`.trim());
|
|
5037
|
+
} catch {
|
|
5038
|
+
return null;
|
|
5039
|
+
}
|
|
5040
|
+
}
|
|
5041
|
+
async function supersedeIfContradicted(memDb, knowledgeDb, args) {
|
|
5042
|
+
const cfg = args.config ?? resolveContradictionConfig();
|
|
5043
|
+
if (!cfg.enabled) return { op: "ADD", superseded: null, reason: "disabled" };
|
|
5044
|
+
const start = Date.now();
|
|
5045
|
+
try {
|
|
5046
|
+
const queryVec = await embed(args.text);
|
|
5047
|
+
if (!queryVec) return { op: "ADD", superseded: null, reason: "no embedder (fail-open)" };
|
|
5048
|
+
const active = getActiveEmbedModel();
|
|
5049
|
+
const results = hybridSearch(memDb, knowledgeDb, {
|
|
5050
|
+
queryText: args.text,
|
|
5051
|
+
queryVec,
|
|
5052
|
+
modelId: active?.modelId ?? null,
|
|
5053
|
+
dim: active?.dim ?? null,
|
|
5054
|
+
sources: [args.source],
|
|
5055
|
+
limit: 5,
|
|
5056
|
+
candidatePool: 20
|
|
5057
|
+
});
|
|
5058
|
+
const scored = [];
|
|
5059
|
+
for (const r of results) {
|
|
5060
|
+
if (r.id === args.newId) continue;
|
|
5061
|
+
if (Date.now() - start > cfg.budgetMs) break;
|
|
5062
|
+
const vec = await candidateVec(memDb, args.source, r.id);
|
|
5063
|
+
if (vec) scored.push({ id: r.id, source: args.source, cosine: cosineSim(queryVec, vec) });
|
|
5064
|
+
}
|
|
5065
|
+
let verdict = null;
|
|
5066
|
+
if (cfg.judgeEndpoint && Date.now() - start < cfg.budgetMs) {
|
|
5067
|
+
verdict = await judgeViaEndpoint(cfg.judgeEndpoint, args.text, scored, cfg.budgetMs);
|
|
5068
|
+
}
|
|
5069
|
+
if (!verdict) verdict = judgeContradiction(args.text, scored, cfg);
|
|
5070
|
+
if (verdict.op === "UPDATE" && typeof verdict.targetId === "number") {
|
|
5071
|
+
const table = args.source === "observation" ? "observations" : "architecture_decisions";
|
|
5072
|
+
const ok = markRecordSuperseded(memDb, table, verdict.targetId, args.newId, args.nowEpochSec);
|
|
5073
|
+
return { op: "UPDATE", superseded: ok ? verdict.targetId : null, reason: verdict.reason };
|
|
5074
|
+
}
|
|
5075
|
+
return { op: verdict.op, superseded: null, reason: verdict.reason };
|
|
5076
|
+
} catch (e) {
|
|
5077
|
+
return { op: "ADD", superseded: null, reason: `fail-open: ${e.message}` };
|
|
5078
|
+
}
|
|
5079
|
+
}
|
|
5080
|
+
var SWEEP_MAX_RECORDS = 500;
|
|
5081
|
+
async function runSessionSupersedeSweep(memDb, sessionId, opts) {
|
|
5082
|
+
const cfg = opts?.config ?? resolveContradictionConfig();
|
|
5083
|
+
if (!cfg.enabled) return { superseded: 0 };
|
|
5084
|
+
const overallBudget = opts?.budgetMs ?? 4e3;
|
|
5085
|
+
const start = Date.now();
|
|
5086
|
+
let count = 0;
|
|
5087
|
+
const stillLive = (table, id) => {
|
|
5088
|
+
const row = memDb.prepare(`SELECT expired_at FROM ${table} WHERE id = ?`).get(id);
|
|
5089
|
+
return !!row && row.expired_at == null;
|
|
5090
|
+
};
|
|
5091
|
+
try {
|
|
5092
|
+
if (cfg.gatedTypes.length > 0) {
|
|
5093
|
+
const placeholders = cfg.gatedTypes.map(() => "?").join(",");
|
|
5094
|
+
const obs = memDb.prepare(
|
|
5095
|
+
`SELECT id, title, detail FROM observations
|
|
5096
|
+
WHERE session_id = ? AND expired_at IS NULL AND type IN (${placeholders})
|
|
5097
|
+
ORDER BY created_at_epoch DESC LIMIT ?`
|
|
5098
|
+
).all(sessionId, ...cfg.gatedTypes, SWEEP_MAX_RECORDS);
|
|
5099
|
+
for (const o of obs) {
|
|
5100
|
+
if (Date.now() - start > overallBudget) break;
|
|
5101
|
+
if (!stillLive("observations", o.id)) continue;
|
|
5102
|
+
const res = await supersedeIfContradicted(memDb, null, {
|
|
5103
|
+
text: `${o.title}
|
|
5104
|
+
${o.detail ?? ""}`.trim(),
|
|
5105
|
+
source: "observation",
|
|
5106
|
+
newId: o.id,
|
|
5107
|
+
config: cfg,
|
|
5108
|
+
nowEpochSec: opts?.nowEpochSec
|
|
5109
|
+
});
|
|
5110
|
+
if (res.superseded != null) count++;
|
|
5111
|
+
}
|
|
5112
|
+
}
|
|
5113
|
+
const decs = memDb.prepare(
|
|
5114
|
+
`SELECT id, title, decision FROM architecture_decisions
|
|
5115
|
+
WHERE session_id = ? AND expired_at IS NULL ORDER BY id DESC LIMIT ?`
|
|
5116
|
+
).all(sessionId, SWEEP_MAX_RECORDS);
|
|
5117
|
+
for (const d of decs) {
|
|
5118
|
+
if (Date.now() - start > overallBudget) break;
|
|
5119
|
+
if (!stillLive("architecture_decisions", d.id)) continue;
|
|
5120
|
+
const res = await supersedeIfContradicted(memDb, null, {
|
|
5121
|
+
text: `${d.title}
|
|
5122
|
+
${d.decision}`.trim(),
|
|
5123
|
+
source: "architecture_decision",
|
|
5124
|
+
newId: d.id,
|
|
5125
|
+
config: cfg,
|
|
5126
|
+
nowEpochSec: opts?.nowEpochSec
|
|
5127
|
+
});
|
|
5128
|
+
if (res.superseded != null) count++;
|
|
5129
|
+
}
|
|
5130
|
+
} catch {
|
|
5131
|
+
}
|
|
5132
|
+
return { superseded: count };
|
|
5133
|
+
}
|
|
5134
|
+
|
|
5135
|
+
// src/memory-consolidate.ts
|
|
5136
|
+
init_memory_db();
|
|
5137
|
+
import { createHash as createHash3 } from "crypto";
|
|
5138
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
5139
|
+
import { join as join5 } from "path";
|
|
5140
|
+
|
|
5141
|
+
// src/consolidation-config.ts
|
|
5142
|
+
init_config();
|
|
5143
|
+
var DEFAULT_CONSOLIDATION_CONFIG = {
|
|
5144
|
+
enabled: true,
|
|
5145
|
+
sessionSweepEnabled: true,
|
|
5146
|
+
// llmEndpoint / llmModel deliberately UNSET: the shipped default is
|
|
5147
|
+
// zero-LLM, zero-network, works offline on any machine.
|
|
5148
|
+
summarizeAfterDays: 5,
|
|
5149
|
+
// inside the 7-day conversation_turns prune window
|
|
5150
|
+
retentionDays: 90,
|
|
5151
|
+
importanceFloor: 2,
|
|
5152
|
+
protectedTypes: ["decision", "cr_violation", "incident_near_miss"],
|
|
5153
|
+
usageWarmupDays: 30,
|
|
5154
|
+
usageDecay: 0.9,
|
|
5155
|
+
reweightIntervalDays: 1,
|
|
5156
|
+
promoteMinOccurrences: 3,
|
|
5157
|
+
budgetMs: 3e3,
|
|
5158
|
+
suggestUpgrades: true,
|
|
5159
|
+
suggestIntervalDays: 30
|
|
5160
|
+
};
|
|
5161
|
+
function resolveConsolidationConfig() {
|
|
5162
|
+
try {
|
|
5163
|
+
const c = getConfig().memory?.consolidation;
|
|
5164
|
+
if (!c) return { ...DEFAULT_CONSOLIDATION_CONFIG };
|
|
5165
|
+
return { ...DEFAULT_CONSOLIDATION_CONFIG, ...c };
|
|
5166
|
+
} catch {
|
|
5167
|
+
return { ...DEFAULT_CONSOLIDATION_CONFIG };
|
|
5168
|
+
}
|
|
5169
|
+
}
|
|
5170
|
+
|
|
5171
|
+
// src/memory-llm.ts
|
|
5172
|
+
var LLM_API_KEY_ENV = "MASSU_MEMORY_LLM_API_KEY";
|
|
5173
|
+
var DEFAULT_BUDGET_MS = 2e4;
|
|
5174
|
+
var DEFAULT_MAX_CHARS = 900;
|
|
5175
|
+
var NOISE_PATTERNS = [
|
|
5176
|
+
/<command-(name|message|args)>/i,
|
|
5177
|
+
/<local-command-[^>]*>/i,
|
|
5178
|
+
/^\s*<[a-z-]+>\s*$/i,
|
|
5179
|
+
/^\s*(ok|okay|thanks|thank you|yes|no|yep|nope|sure|continue|proceed|go ahead)\s*[.!]?\s*$/i,
|
|
5180
|
+
/^\s*\/[a-z-]+\s*$/i,
|
|
5181
|
+
// a bare slash-command invocation
|
|
5182
|
+
/system-reminder/i
|
|
5183
|
+
];
|
|
5184
|
+
function isSummarizableSignal(text) {
|
|
5185
|
+
const t = text.trim();
|
|
5186
|
+
if (t.length < 25) return false;
|
|
5187
|
+
return !NOISE_PATTERNS.some((re) => re.test(t));
|
|
5188
|
+
}
|
|
5189
|
+
var CREDENTIAL_LABELS = ["api[_-]?key", "secret", "password", "token"];
|
|
5190
|
+
var LABELLED_CREDENTIAL = new RegExp(
|
|
5191
|
+
`\\b[A-Za-z0-9_-]*(?:${CREDENTIAL_LABELS.join("|")})["'\\s:=]+[A-Za-z0-9_\\-/+]{16,}`,
|
|
5192
|
+
"gi"
|
|
5193
|
+
);
|
|
5194
|
+
var SECRET_PATTERNS = [
|
|
5195
|
+
[/\bms_live_[A-Za-z0-9_-]{6,}/g, "ms_live_[REDACTED]", "MASSU_LIVE_KEY"],
|
|
5196
|
+
[/\bsk-[A-Za-z0-9_-]{16,}/g, "sk-[REDACTED]", "OPENAI_STYLE_KEY"],
|
|
5197
|
+
[/\b(gh[pousr]|github_pat)_[A-Za-z0-9_]{16,}/g, "[REDACTED_TOKEN]", "GITHUB_TOKEN"],
|
|
5198
|
+
[/\bsbp_[A-Za-z0-9]{16,}/g, "sbp_[REDACTED]", "SUPABASE_TOKEN"],
|
|
5199
|
+
[/\bAKIA[0-9A-Z]{12,}/g, "[REDACTED_AWS_KEY]", "AWS_ACCESS_KEY"],
|
|
5200
|
+
[
|
|
5201
|
+
/\bey[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{6,}/g,
|
|
5202
|
+
"[REDACTED_JWT]",
|
|
5203
|
+
"JWT"
|
|
5204
|
+
],
|
|
5205
|
+
[LABELLED_CREDENTIAL, "[REDACTED_CREDENTIAL]", "LABELLED_CREDENTIAL"]
|
|
5206
|
+
];
|
|
5207
|
+
function redactSecrets(text) {
|
|
5208
|
+
let out = text;
|
|
5209
|
+
for (const [re, replacement] of SECRET_PATTERNS) out = out.replace(re, replacement);
|
|
5210
|
+
return out;
|
|
5211
|
+
}
|
|
5212
|
+
function extractiveSummary(sources, maxChars = DEFAULT_MAX_CHARS) {
|
|
5213
|
+
const cleaned = sources.map((s, i) => ({ ...s, i, text: s.text.replace(/\s+/g, " ").trim() })).filter((s) => isSummarizableSignal(s.text));
|
|
5214
|
+
if (cleaned.length === 0) return "";
|
|
5215
|
+
const ranked = [...cleaned].sort((a, b) => b.weight - a.weight || a.i - b.i);
|
|
5216
|
+
const picked = [];
|
|
5217
|
+
let used = 0;
|
|
5218
|
+
for (const s of ranked) {
|
|
5219
|
+
const cost = s.text.length + 1;
|
|
5220
|
+
if (used + cost > maxChars) continue;
|
|
5221
|
+
picked.push(s);
|
|
5222
|
+
used += cost;
|
|
5223
|
+
}
|
|
5224
|
+
if (picked.length === 0) {
|
|
5225
|
+
return ranked[0].text.slice(0, maxChars);
|
|
5226
|
+
}
|
|
5227
|
+
picked.sort((a, b) => a.i - b.i);
|
|
5228
|
+
return picked.map((s) => s.text).join(" ");
|
|
5229
|
+
}
|
|
5230
|
+
async function summarizeViaEndpoint(endpoint, model, material, budgetMs, maxChars) {
|
|
5231
|
+
try {
|
|
5232
|
+
const controller = new AbortController();
|
|
5233
|
+
const timer = setTimeout(() => controller.abort(), budgetMs);
|
|
5234
|
+
const apiKey = process.env[LLM_API_KEY_ENV];
|
|
5235
|
+
const headers = { "content-type": "application/json" };
|
|
5236
|
+
if (apiKey) headers["authorization"] = `Bearer ${apiKey}`;
|
|
5237
|
+
let resp;
|
|
5238
|
+
try {
|
|
5239
|
+
resp = await fetch(`${endpoint.replace(/\/+$/, "")}/v1/chat/completions`, {
|
|
5240
|
+
method: "POST",
|
|
5241
|
+
headers,
|
|
5242
|
+
body: JSON.stringify({
|
|
5243
|
+
model,
|
|
5244
|
+
// a NAME/ALIAS the user configured — never a physical model id
|
|
5245
|
+
messages: [
|
|
5246
|
+
{
|
|
5247
|
+
role: "system",
|
|
5248
|
+
content: "You distill a software engineering session into ONE durable lesson a developer will read months later. Be concrete and factual. State what broke, what was tried and rejected, and what actually worked. Invent nothing that is not in the input."
|
|
5249
|
+
},
|
|
5250
|
+
{ role: "user", content: material }
|
|
5251
|
+
],
|
|
5252
|
+
max_tokens: 400,
|
|
5253
|
+
temperature: 0.2
|
|
5254
|
+
}),
|
|
5255
|
+
signal: controller.signal
|
|
5256
|
+
});
|
|
5257
|
+
} finally {
|
|
5258
|
+
clearTimeout(timer);
|
|
5259
|
+
}
|
|
5260
|
+
if (!resp.ok) return null;
|
|
5261
|
+
const data = await resp.json();
|
|
5262
|
+
const text = data.choices?.[0]?.message?.content;
|
|
5263
|
+
if (typeof text !== "string" || !text.trim()) return null;
|
|
5264
|
+
return text.trim().slice(0, maxChars);
|
|
5265
|
+
} catch {
|
|
5266
|
+
return null;
|
|
5267
|
+
}
|
|
5268
|
+
}
|
|
5269
|
+
async function summarizeText(sources, opts = {}) {
|
|
5270
|
+
const cfg = opts.config ?? resolveConsolidationConfig();
|
|
5271
|
+
const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
|
|
5272
|
+
const budgetMs = opts.budgetMs ?? DEFAULT_BUDGET_MS;
|
|
5273
|
+
const extractive = extractiveSummary(sources, maxChars);
|
|
5274
|
+
if (!cfg.llmEndpoint || !cfg.llmModel || !extractive) {
|
|
5275
|
+
return { text: extractive, tier: "extractive" };
|
|
5276
|
+
}
|
|
5277
|
+
const material = redactSecrets(
|
|
5278
|
+
sources.map((s) => s.text.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n")
|
|
5279
|
+
);
|
|
5280
|
+
const viaModel = await summarizeViaEndpoint(
|
|
5281
|
+
cfg.llmEndpoint,
|
|
5282
|
+
cfg.llmModel,
|
|
5283
|
+
material,
|
|
5284
|
+
budgetMs,
|
|
5285
|
+
maxChars
|
|
5286
|
+
);
|
|
5287
|
+
return viaModel ? { text: viaModel, tier: "model" } : { text: extractive, tier: "extractive" };
|
|
5288
|
+
}
|
|
5289
|
+
|
|
5290
|
+
// src/memory-consolidate.ts
|
|
5291
|
+
init_memory_embedder();
|
|
5292
|
+
init_memory_vector();
|
|
5293
|
+
var LEASE_KEY = "consolidate_lease";
|
|
5294
|
+
var DEDUPE_CURSOR = "consolidate_cursor_dedupe";
|
|
5295
|
+
var MAX_ROWS_PER_STAGE = 500;
|
|
5296
|
+
function emptyResult() {
|
|
5297
|
+
return {
|
|
5298
|
+
deduped: 0,
|
|
5299
|
+
summarized: 0,
|
|
5300
|
+
promoted: 0,
|
|
5301
|
+
reweighted: 0,
|
|
5302
|
+
expired: 0,
|
|
5303
|
+
sessionsMissed: 0,
|
|
5304
|
+
candidatesRefusedByTier: 0,
|
|
5305
|
+
embedderUnavailable: false,
|
|
5306
|
+
summaryTier: null,
|
|
5307
|
+
stagesRun: [],
|
|
5308
|
+
stagesFailed: [],
|
|
5309
|
+
warmingUp: false
|
|
5310
|
+
};
|
|
5311
|
+
}
|
|
5312
|
+
function acquireLease(db, now, ttlSec) {
|
|
5313
|
+
const raw = getMemoryMeta(db, LEASE_KEY);
|
|
5314
|
+
if (raw) {
|
|
5315
|
+
const expiry = Number(raw.split(":")[1]);
|
|
5316
|
+
if (Number.isFinite(expiry) && expiry > now) return false;
|
|
5317
|
+
}
|
|
5318
|
+
setMemoryMeta(db, LEASE_KEY, `${process.pid}:${now + ttlSec}`);
|
|
5319
|
+
return true;
|
|
5320
|
+
}
|
|
5321
|
+
function releaseLease(db) {
|
|
5322
|
+
try {
|
|
5323
|
+
setMemoryMeta(db, LEASE_KEY, "");
|
|
5324
|
+
} catch {
|
|
5325
|
+
}
|
|
5326
|
+
}
|
|
5327
|
+
async function stageDedupe(db, cfg, deadline, now) {
|
|
5328
|
+
const gated = ["decision", "cr_violation", "failed_attempt"];
|
|
5329
|
+
const cursor = Number(getMemoryMeta(db, DEDUPE_CURSOR) ?? "0") || 0;
|
|
5330
|
+
const rows = db.prepare(
|
|
5331
|
+
`SELECT id, title, detail FROM observations
|
|
5332
|
+
WHERE expired_at IS NULL AND id > ?
|
|
5333
|
+
AND type IN (${gated.map(() => "?").join(",")})
|
|
5334
|
+
AND title NOT LIKE ?
|
|
5335
|
+
ORDER BY id ASC LIMIT ?`
|
|
5336
|
+
).all(cursor, ...gated, MEMORY_FILE_TITLE_LIKE, MAX_ROWS_PER_STAGE);
|
|
5337
|
+
if (rows.length === 0) {
|
|
5338
|
+
setMemoryMeta(db, DEDUPE_CURSOR, "0");
|
|
5339
|
+
return 0;
|
|
5340
|
+
}
|
|
5341
|
+
let deduped = 0;
|
|
5342
|
+
for (const r of rows) {
|
|
5343
|
+
if (Date.now() > deadline) break;
|
|
5344
|
+
const live = db.prepare(`SELECT expired_at FROM observations WHERE id = ?`).get(r.id);
|
|
5345
|
+
if (!live || live.expired_at != null) continue;
|
|
5346
|
+
const res = await supersedeIfContradicted(db, null, {
|
|
5347
|
+
text: `${r.title}
|
|
5348
|
+
${r.detail ?? ""}`.trim(),
|
|
5349
|
+
source: "observation",
|
|
5350
|
+
newId: r.id,
|
|
5351
|
+
nowEpochSec: now
|
|
5352
|
+
});
|
|
5353
|
+
if (res.superseded != null) deduped++;
|
|
5354
|
+
setMemoryMeta(db, DEDUPE_CURSOR, String(r.id));
|
|
5355
|
+
}
|
|
5356
|
+
return deduped;
|
|
5357
|
+
}
|
|
5358
|
+
async function stageSummarize(db, cfg, deadline, now) {
|
|
5359
|
+
const cutoff = now - cfg.summarizeAfterDays * 86400;
|
|
5360
|
+
const sessions = db.prepare(
|
|
5361
|
+
`SELECT s.session_id AS sid,
|
|
5362
|
+
(SELECT MAX(t.created_at_epoch) FROM conversation_turns t
|
|
5363
|
+
WHERE t.session_id = s.session_id) AS newest_turn
|
|
5364
|
+
FROM sessions s
|
|
5365
|
+
WHERE s.consolidated_at IS NULL
|
|
5366
|
+
ORDER BY s.session_id ASC
|
|
5367
|
+
LIMIT ?`
|
|
5368
|
+
).all(MAX_ROWS_PER_STAGE);
|
|
5369
|
+
let summarized = 0;
|
|
5370
|
+
let sessionsMissed = 0;
|
|
5371
|
+
let tier = null;
|
|
5372
|
+
const stamp = db.prepare(
|
|
5373
|
+
`UPDATE sessions
|
|
5374
|
+
SET consolidated_at = ?, consolidated_at_epoch = ?, consolidated_status = ?
|
|
5375
|
+
WHERE session_id = ?`
|
|
5376
|
+
);
|
|
5377
|
+
const iso = new Date(now * 1e3).toISOString();
|
|
5378
|
+
for (const s of sessions) {
|
|
5379
|
+
if (Date.now() > deadline) break;
|
|
5380
|
+
if (s.newest_turn == null) {
|
|
5381
|
+
stamp.run(iso, now, "no_turns", s.sid);
|
|
5382
|
+
sessionsMissed++;
|
|
5383
|
+
continue;
|
|
5384
|
+
}
|
|
5385
|
+
if (s.newest_turn > cutoff) continue;
|
|
5386
|
+
const obs = db.prepare(
|
|
5387
|
+
`SELECT type, title, detail, importance FROM observations
|
|
5388
|
+
WHERE session_id = ? AND expired_at IS NULL
|
|
5389
|
+
AND COALESCE(evidence,'') != ?
|
|
5390
|
+
ORDER BY importance DESC LIMIT 40`
|
|
5391
|
+
).all(s.sid, CONSOLIDATION_LESSON_EVIDENCE);
|
|
5392
|
+
const sources = obs.map((o) => ({
|
|
5393
|
+
text: redactSecrets(`${o.type}: ${o.title}${o.detail ? ` \u2014 ${o.detail}` : ""}`),
|
|
5394
|
+
weight: o.importance
|
|
5395
|
+
}));
|
|
5396
|
+
const summary = sources.length > 0 ? await summarizeText(sources, { config: cfg }) : null;
|
|
5397
|
+
if (summary) tier = summary.tier;
|
|
5398
|
+
if (!summary || !summary.text) {
|
|
5399
|
+
stamp.run(iso, now, "no_signal", s.sid);
|
|
5400
|
+
continue;
|
|
5401
|
+
}
|
|
5402
|
+
addObservation(
|
|
5403
|
+
db,
|
|
5404
|
+
s.sid,
|
|
5405
|
+
"discovery",
|
|
5406
|
+
`Session lesson: ${s.sid.slice(0, 8)}`,
|
|
5407
|
+
redactSecrets(summary.text),
|
|
5408
|
+
// also redact model output — it echoes its input
|
|
5409
|
+
{ importance: 4, evidence: CONSOLIDATION_LESSON_EVIDENCE }
|
|
5410
|
+
);
|
|
5411
|
+
stamp.run(iso, now, "summarized", s.sid);
|
|
5412
|
+
summarized++;
|
|
5413
|
+
}
|
|
5414
|
+
return { summarized, sessionsMissed, tier };
|
|
5415
|
+
}
|
|
5416
|
+
async function stagePromote(db, cfg, projectRoot, deadline, now, dryRun) {
|
|
5417
|
+
const tier = getCachedTierReadOnly(db);
|
|
5418
|
+
if (!entitledForAutoLearning(tier)) {
|
|
5419
|
+
const wouldHave = db.prepare(
|
|
5420
|
+
`SELECT COUNT(*) AS n FROM observations
|
|
5421
|
+
WHERE type IN ('cr_violation','failed_attempt') AND recurrence_count >= ?`
|
|
5422
|
+
).get(cfg.promoteMinOccurrences);
|
|
5423
|
+
return { promoted: 0, refusedByTier: wouldHave.n, embedderUnavailable: false };
|
|
5424
|
+
}
|
|
5425
|
+
const rows = db.prepare(
|
|
5426
|
+
`SELECT id, title, detail, session_id, recurrence_count FROM observations
|
|
5427
|
+
WHERE type IN ('cr_violation','failed_attempt')
|
|
5428
|
+
ORDER BY id DESC LIMIT ?`
|
|
5429
|
+
).all(MAX_ROWS_PER_STAGE);
|
|
5430
|
+
if (rows.length === 0) return { promoted: 0, refusedByTier: 0, embedderUnavailable: false };
|
|
5431
|
+
const vecs = /* @__PURE__ */ new Map();
|
|
5432
|
+
for (const r of rows) {
|
|
5433
|
+
if (Date.now() > deadline) break;
|
|
5434
|
+
const v = await embed(`${r.title}
|
|
5435
|
+
${r.detail ?? ""}`.trim());
|
|
5436
|
+
if (v) vecs.set(r.id, v);
|
|
5437
|
+
}
|
|
5438
|
+
if (vecs.size === 0) {
|
|
5439
|
+
return { promoted: 0, refusedByTier: 0, embedderUnavailable: true };
|
|
5440
|
+
}
|
|
5441
|
+
const CLUSTER_THRESHOLD = 0.8;
|
|
5442
|
+
const used = /* @__PURE__ */ new Set();
|
|
5443
|
+
const clusters = [];
|
|
5444
|
+
for (const r of rows) {
|
|
5445
|
+
if (used.has(r.id) || !vecs.has(r.id)) continue;
|
|
5446
|
+
const cluster = [r];
|
|
5447
|
+
used.add(r.id);
|
|
5448
|
+
for (const other of rows) {
|
|
5449
|
+
if (used.has(other.id) || !vecs.has(other.id)) continue;
|
|
5450
|
+
if (cosineSim(vecs.get(r.id), vecs.get(other.id)) >= CLUSTER_THRESHOLD) {
|
|
5451
|
+
cluster.push(other);
|
|
5452
|
+
used.add(other.id);
|
|
5453
|
+
}
|
|
5454
|
+
}
|
|
5455
|
+
clusters.push(cluster);
|
|
5456
|
+
}
|
|
5457
|
+
const candidateDir = join5(projectRoot, ".massu", "rule-candidates");
|
|
5458
|
+
let promoted = 0;
|
|
5459
|
+
for (const cluster of clusters) {
|
|
5460
|
+
const occurrences = cluster.reduce((n, c) => n + (c.recurrence_count || 1), 0);
|
|
5461
|
+
const sessions = new Set(cluster.map((c) => c.session_id));
|
|
5462
|
+
if (occurrences < cfg.promoteMinOccurrences || sessions.size < 2) continue;
|
|
5463
|
+
const representative = [...cluster].sort(
|
|
5464
|
+
(a, b) => (b.recurrence_count || 1) - (a.recurrence_count || 1)
|
|
5465
|
+
)[0];
|
|
5466
|
+
const promptText = `${representative.title}${representative.detail ? `
|
|
5467
|
+
${representative.detail}` : ""}`;
|
|
5468
|
+
const clusterKey = cluster.map((c) => c.title.toLowerCase().replace(/\s+/g, " ").trim()).sort().join("|");
|
|
5469
|
+
const promptHash = createHash3("sha256").update(clusterKey).digest("hex").slice(0, 16);
|
|
5470
|
+
const candidatePath = join5(candidateDir, `${promptHash}.json`);
|
|
5471
|
+
if (existsSync9(candidatePath)) continue;
|
|
5472
|
+
if (dryRun) {
|
|
5473
|
+
promoted++;
|
|
5474
|
+
continue;
|
|
5475
|
+
}
|
|
5476
|
+
mkdirSync7(candidateDir, { recursive: true });
|
|
5477
|
+
writeFileSync4(
|
|
5478
|
+
candidatePath,
|
|
5479
|
+
JSON.stringify(
|
|
5480
|
+
{
|
|
5481
|
+
prompt: promptText,
|
|
5482
|
+
prompt_hash: promptHash,
|
|
5483
|
+
score: Math.min(100, 60 + occurrences * 5 + sessions.size * 5),
|
|
5484
|
+
signals: [
|
|
5485
|
+
{
|
|
5486
|
+
type: "consolidation-cluster",
|
|
5487
|
+
occurrences,
|
|
5488
|
+
sessions: sessions.size,
|
|
5489
|
+
detail: `This correction has recurred ${occurrences}x across ${sessions.size} sessions.`
|
|
5490
|
+
}
|
|
5491
|
+
],
|
|
5492
|
+
prior_turn_files: [],
|
|
5493
|
+
timestamp: new Date(now * 1e3).toISOString(),
|
|
5494
|
+
session_id: representative.session_id,
|
|
5495
|
+
// Marks this as machine-clustered, so /massu-rule can label it and
|
|
5496
|
+
// does not re-classify it as an ordinary per-prompt local candidate.
|
|
5497
|
+
provenance: { origin: "consolidation" }
|
|
5498
|
+
},
|
|
5499
|
+
null,
|
|
5500
|
+
2
|
|
5501
|
+
)
|
|
5502
|
+
);
|
|
5503
|
+
promoted++;
|
|
5504
|
+
}
|
|
5505
|
+
return { promoted, refusedByTier: 0, embedderUnavailable: false };
|
|
5506
|
+
}
|
|
5507
|
+
function stageReweight(db, cfg, now) {
|
|
5508
|
+
const staleCutoff = now - cfg.retentionDays * 86400;
|
|
5509
|
+
const reweightCutoff = now - cfg.reweightIntervalDays * 86400;
|
|
5510
|
+
db.prepare(`UPDATE memory_usage SET hits_windowed = hits_windowed * ?`).run(cfg.usageDecay);
|
|
5511
|
+
let changed = 0;
|
|
5512
|
+
const promote = db.prepare(
|
|
5513
|
+
`SELECT u.record_id AS id FROM memory_usage u
|
|
5514
|
+
JOIN observations o ON o.id = u.record_id
|
|
5515
|
+
WHERE u.source = 'observation'
|
|
5516
|
+
AND u.hits_windowed >= 2
|
|
5517
|
+
AND o.importance < 5
|
|
5518
|
+
AND o.expired_at IS NULL
|
|
5519
|
+
AND (u.last_reweight_epoch IS NULL OR u.last_reweight_epoch <= ?)
|
|
5520
|
+
LIMIT ?`
|
|
5521
|
+
).all(reweightCutoff, MAX_ROWS_PER_STAGE);
|
|
5522
|
+
const demote = db.prepare(
|
|
5523
|
+
`SELECT o.id AS id FROM observations o
|
|
5524
|
+
LEFT JOIN memory_usage u
|
|
5525
|
+
ON u.source = 'observation' AND u.record_id = o.id
|
|
5526
|
+
WHERE o.expired_at IS NULL
|
|
5527
|
+
AND o.created_at_epoch < ?
|
|
5528
|
+
AND o.importance > 1
|
|
5529
|
+
AND COALESCE(o.evidence,'') != ?
|
|
5530
|
+
AND o.title NOT LIKE ?
|
|
5531
|
+
AND COALESCE(u.hit_count, 0) = 0
|
|
5532
|
+
AND (u.last_reweight_epoch IS NULL OR u.last_reweight_epoch <= ?)
|
|
5533
|
+
LIMIT ?`
|
|
5534
|
+
).all(
|
|
5535
|
+
staleCutoff,
|
|
5536
|
+
CONSOLIDATION_LESSON_EVIDENCE,
|
|
5537
|
+
MEMORY_FILE_TITLE_LIKE,
|
|
5538
|
+
reweightCutoff,
|
|
5539
|
+
MAX_ROWS_PER_STAGE
|
|
5540
|
+
);
|
|
5541
|
+
const bump = db.prepare(`UPDATE observations SET importance = importance + 1 WHERE id = ?`);
|
|
5542
|
+
const drop = db.prepare(`UPDATE observations SET importance = importance - 1 WHERE id = ?`);
|
|
5543
|
+
const mark = db.prepare(
|
|
5544
|
+
`INSERT INTO memory_usage (source, record_id, hit_count, hits_windowed, last_reweight_epoch)
|
|
5545
|
+
VALUES ('observation', ?, 0, 0, ?)
|
|
5546
|
+
ON CONFLICT(source, record_id) DO UPDATE SET last_reweight_epoch = excluded.last_reweight_epoch`
|
|
5547
|
+
);
|
|
5548
|
+
const tx = db.transaction(() => {
|
|
5549
|
+
for (const r of promote) {
|
|
5550
|
+
bump.run(r.id);
|
|
5551
|
+
mark.run(r.id, now);
|
|
5552
|
+
changed++;
|
|
5553
|
+
}
|
|
5554
|
+
for (const r of demote) {
|
|
5555
|
+
drop.run(r.id);
|
|
5556
|
+
mark.run(r.id, now);
|
|
5557
|
+
changed++;
|
|
5558
|
+
}
|
|
5559
|
+
});
|
|
5560
|
+
tx();
|
|
5561
|
+
return changed;
|
|
5562
|
+
}
|
|
5563
|
+
async function runConsolidation(db, opts = {}) {
|
|
5564
|
+
const cfg = opts.config ?? resolveConsolidationConfig();
|
|
5565
|
+
const result = emptyResult();
|
|
5566
|
+
if (!cfg.enabled) return { ...result, skipped: "disabled" };
|
|
5567
|
+
const now = opts.nowEpochSec ?? Math.floor(Date.now() / 1e3);
|
|
5568
|
+
const budgetMs = opts.budgetMs ?? 6e4;
|
|
5569
|
+
const deadline = Date.now() + budgetMs;
|
|
5570
|
+
const dryRun = opts.dryRun === true;
|
|
5571
|
+
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
5572
|
+
if (!dryRun) armUsageCounter(db, now);
|
|
5573
|
+
result.warmingUp = !usageWarmupElapsed(db, cfg.usageWarmupDays, now);
|
|
5574
|
+
if (!dryRun && !acquireLease(db, now, Math.ceil(budgetMs * 2 / 1e3))) {
|
|
5575
|
+
return { ...result, skipped: "lease-held" };
|
|
5576
|
+
}
|
|
5577
|
+
try {
|
|
5578
|
+
try {
|
|
5579
|
+
result.deduped = await stageDedupe(db, cfg, deadline, now);
|
|
5580
|
+
result.stagesRun.push("dedupe");
|
|
5581
|
+
} catch {
|
|
5582
|
+
result.stagesFailed.push("dedupe");
|
|
5583
|
+
}
|
|
5584
|
+
try {
|
|
5585
|
+
if (!dryRun) {
|
|
5586
|
+
const s = await stageSummarize(db, cfg, deadline, now);
|
|
5587
|
+
result.summarized = s.summarized;
|
|
5588
|
+
result.sessionsMissed = s.sessionsMissed;
|
|
5589
|
+
result.summaryTier = s.tier;
|
|
5590
|
+
}
|
|
5591
|
+
result.stagesRun.push("summarize");
|
|
5592
|
+
} catch {
|
|
5593
|
+
result.stagesFailed.push("summarize");
|
|
5594
|
+
}
|
|
5595
|
+
try {
|
|
5596
|
+
const p = await stagePromote(db, cfg, projectRoot, deadline, now, dryRun);
|
|
5597
|
+
result.promoted = p.promoted;
|
|
5598
|
+
result.candidatesRefusedByTier = p.refusedByTier;
|
|
5599
|
+
result.embedderUnavailable = p.embedderUnavailable;
|
|
5600
|
+
result.stagesRun.push("promote");
|
|
5601
|
+
} catch {
|
|
5602
|
+
result.stagesFailed.push("promote");
|
|
5603
|
+
}
|
|
5604
|
+
try {
|
|
5605
|
+
if (!dryRun) {
|
|
5606
|
+
result.expired = expireOldLowValueObservations(db, {
|
|
5607
|
+
retentionDays: cfg.retentionDays,
|
|
5608
|
+
importanceFloor: cfg.importanceFloor,
|
|
5609
|
+
protectedTypes: cfg.protectedTypes,
|
|
5610
|
+
usageWarmupDays: cfg.usageWarmupDays,
|
|
5611
|
+
reweightIntervalDays: cfg.reweightIntervalDays,
|
|
5612
|
+
nowEpochSec: now
|
|
5613
|
+
});
|
|
5614
|
+
}
|
|
5615
|
+
result.stagesRun.push("expire");
|
|
5616
|
+
} catch {
|
|
5617
|
+
result.stagesFailed.push("expire");
|
|
5618
|
+
}
|
|
5619
|
+
try {
|
|
5620
|
+
if (!dryRun) result.reweighted = stageReweight(db, cfg, now);
|
|
5621
|
+
result.stagesRun.push("reweight");
|
|
5622
|
+
} catch {
|
|
5623
|
+
result.stagesFailed.push("reweight");
|
|
5624
|
+
}
|
|
5625
|
+
} finally {
|
|
5626
|
+
if (!dryRun) releaseLease(db);
|
|
5627
|
+
}
|
|
5628
|
+
return result;
|
|
5629
|
+
}
|
|
5630
|
+
|
|
2681
5631
|
// src/hooks/session-end.ts
|
|
5632
|
+
init_hook_failure_signal();
|
|
2682
5633
|
async function main() {
|
|
2683
5634
|
try {
|
|
2684
5635
|
const input = await readStdin();
|
|
2685
5636
|
const hookInput = JSON.parse(input);
|
|
2686
|
-
const { session_id } = hookInput;
|
|
5637
|
+
const { session_id, cwd } = hookInput;
|
|
2687
5638
|
const db = getMemoryDb();
|
|
2688
5639
|
try {
|
|
2689
5640
|
createSession(db, session_id);
|
|
@@ -2718,6 +5669,25 @@ async function main() {
|
|
|
2718
5669
|
analyzeSessionPrompts(db, session_id);
|
|
2719
5670
|
} catch (_promptErr) {
|
|
2720
5671
|
}
|
|
5672
|
+
try {
|
|
5673
|
+
await embedMissingObservations(db, { budgetMs: 3e3 });
|
|
5674
|
+
} catch (_embedErr) {
|
|
5675
|
+
}
|
|
5676
|
+
try {
|
|
5677
|
+
await runSessionSupersedeSweep(db, session_id, { budgetMs: 4e3 });
|
|
5678
|
+
} catch (_supersedeErr) {
|
|
5679
|
+
}
|
|
5680
|
+
try {
|
|
5681
|
+
const consolidationCfg = resolveConsolidationConfig();
|
|
5682
|
+
if (consolidationCfg.enabled && consolidationCfg.sessionSweepEnabled) {
|
|
5683
|
+
await runConsolidation(db, {
|
|
5684
|
+
config: consolidationCfg,
|
|
5685
|
+
budgetMs: consolidationCfg.budgetMs,
|
|
5686
|
+
projectRoot: cwd
|
|
5687
|
+
});
|
|
5688
|
+
}
|
|
5689
|
+
} catch (_consolidateErr) {
|
|
5690
|
+
}
|
|
2721
5691
|
endSession(db, session_id, "completed");
|
|
2722
5692
|
archiveAndRegenerate(db, session_id);
|
|
2723
5693
|
try {
|
|
@@ -2735,7 +5705,8 @@ async function main() {
|
|
|
2735
5705
|
} finally {
|
|
2736
5706
|
db.close();
|
|
2737
5707
|
}
|
|
2738
|
-
} catch (
|
|
5708
|
+
} catch (err) {
|
|
5709
|
+
recordHookFailure("session-end", err);
|
|
2739
5710
|
}
|
|
2740
5711
|
process.exit(0);
|
|
2741
5712
|
}
|
|
@@ -2990,14 +5961,14 @@ function extractFilesFromToolCall(toolName, input) {
|
|
|
2990
5961
|
return [];
|
|
2991
5962
|
}
|
|
2992
5963
|
function readStdin() {
|
|
2993
|
-
return new Promise((
|
|
5964
|
+
return new Promise((resolve6) => {
|
|
2994
5965
|
let data = "";
|
|
2995
5966
|
process.stdin.setEncoding("utf-8");
|
|
2996
5967
|
process.stdin.on("data", (chunk) => {
|
|
2997
5968
|
data += chunk;
|
|
2998
5969
|
});
|
|
2999
|
-
process.stdin.on("end", () =>
|
|
3000
|
-
setTimeout(() =>
|
|
5970
|
+
process.stdin.on("end", () => resolve6(data));
|
|
5971
|
+
setTimeout(() => resolve6(data), 5e3);
|
|
3001
5972
|
});
|
|
3002
5973
|
}
|
|
3003
5974
|
main();
|