@kanadego/dsh-heartbeat 1.9.0 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -68
- package/assets/frontwin.ps1 +1 -0
- package/assets/idle.ps1 +1 -0
- package/assets/screenpulse.ps1 +4 -2
- package/config/profile-schema.json +306 -306
- package/dist/{bindings-XPPSKILN.js → bindings-225SI7KC.js} +3 -2
- package/dist/{chunk-IPOZFZPY.js → chunk-2NB4QILF.js} +48 -10
- package/dist/chunk-2NB4QILF.js.map +1 -0
- package/dist/{chunk-IFTFDHZX.js → chunk-7TW6DD6Q.js} +11 -6
- package/dist/chunk-7TW6DD6Q.js.map +1 -0
- package/dist/{chunk-63FUJLIF.js → chunk-BQCXYE7N.js} +17 -2
- package/dist/chunk-BQCXYE7N.js.map +1 -0
- package/dist/chunk-HT57FOEF.js +2454 -0
- package/dist/chunk-HT57FOEF.js.map +1 -0
- package/dist/chunk-IV2ZWQA3.js +56 -0
- package/dist/chunk-IV2ZWQA3.js.map +1 -0
- package/dist/{chunk-PQTSXW4F.js → chunk-KOOQOMQY.js} +97 -71
- package/dist/chunk-KOOQOMQY.js.map +1 -0
- package/dist/{chunk-VIZNIQLK.js → chunk-OYVNWB5G.js} +82 -74
- package/dist/chunk-OYVNWB5G.js.map +1 -0
- package/dist/{chunk-RB3NLLGF.js → chunk-QOQ7EPZZ.js} +3 -3
- package/dist/{chunk-J6ZTRFFW.js → chunk-TY6UP74X.js} +6 -2
- package/dist/chunk-TY6UP74X.js.map +1 -0
- package/dist/chunk-WRJCINZ7.js +569 -0
- package/dist/chunk-WRJCINZ7.js.map +1 -0
- package/dist/chunk-WRUTATW4.js +47 -0
- package/dist/chunk-WRUTATW4.js.map +1 -0
- package/dist/chunk-ZLV4NMFX.js +333 -0
- package/dist/chunk-ZLV4NMFX.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +565 -497
- package/dist/cli/index.js.map +1 -1
- package/dist/{inbox-57NHQHY3.js → inbox-JLT5G7TL.js} +5 -3
- package/dist/index.d.ts +517 -0
- package/dist/index.js +570 -2719
- package/dist/index.js.map +1 -1
- package/dist/{migrate-N6NXMZ3G.js → migrate-WDVFHUP7.js} +4 -3
- package/dist/orchestrator-2ZLU7RGE.js +60 -0
- package/dist/{pool-7I4VACG2.js → pool-B34NTH6D.js} +5 -3
- package/dist/{snapshot-ZBCSX67Y.js → snapshot-JQDJ346Z.js} +5 -4
- package/dist/snapshot-JQDJ346Z.js.map +1 -0
- package/package.json +10 -3
- package/config/watchlist.json +0 -9
- package/dist/chunk-2SYXKKZF.js +0 -837
- package/dist/chunk-2SYXKKZF.js.map +0 -1
- package/dist/chunk-63FUJLIF.js.map +0 -1
- package/dist/chunk-IFTFDHZX.js.map +0 -1
- package/dist/chunk-IPOZFZPY.js.map +0 -1
- package/dist/chunk-J6ZTRFFW.js.map +0 -1
- package/dist/chunk-PQTSXW4F.js.map +0 -1
- package/dist/chunk-VIZNIQLK.js.map +0 -1
- /package/dist/{bindings-XPPSKILN.js.map → bindings-225SI7KC.js.map} +0 -0
- /package/dist/{chunk-RB3NLLGF.js.map → chunk-QOQ7EPZZ.js.map} +0 -0
- /package/dist/{inbox-57NHQHY3.js.map → inbox-JLT5G7TL.js.map} +0 -0
- /package/dist/{migrate-N6NXMZ3G.js.map → migrate-WDVFHUP7.js.map} +0 -0
- /package/dist/{pool-7I4VACG2.js.map → orchestrator-2ZLU7RGE.js.map} +0 -0
- /package/dist/{snapshot-ZBCSX67Y.js.map → pool-B34NTH6D.js.map} +0 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/core/file-lock.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
var STALE_MS = 15e3;
|
|
4
|
+
var WAIT_MS = 3e3;
|
|
5
|
+
var POLL_MS = 25;
|
|
6
|
+
function sleepSync(ms) {
|
|
7
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
8
|
+
}
|
|
9
|
+
function tryAcquire(lock) {
|
|
10
|
+
try {
|
|
11
|
+
const fd = fs.openSync(lock, "wx");
|
|
12
|
+
fs.writeSync(fd, `${process.pid} ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
13
|
+
`);
|
|
14
|
+
fs.closeSync(fd);
|
|
15
|
+
return true;
|
|
16
|
+
} catch (e) {
|
|
17
|
+
if (e.code !== "EEXIST") throw e;
|
|
18
|
+
try {
|
|
19
|
+
if (Date.now() - fs.statSync(lock).mtimeMs > STALE_MS) {
|
|
20
|
+
fs.rmSync(lock, { force: true });
|
|
21
|
+
return tryAcquire(lock);
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
return tryAcquire(lock);
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function withFileLock(target, fn) {
|
|
30
|
+
const lock = `${target}.lock`;
|
|
31
|
+
const deadline = Date.now() + WAIT_MS;
|
|
32
|
+
let held = false;
|
|
33
|
+
for (; ; ) {
|
|
34
|
+
if (tryAcquire(lock)) {
|
|
35
|
+
held = true;
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
if (Date.now() >= deadline) break;
|
|
39
|
+
sleepSync(POLL_MS);
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
return fn();
|
|
43
|
+
} finally {
|
|
44
|
+
if (held) {
|
|
45
|
+
try {
|
|
46
|
+
fs.rmSync(lock, { force: true });
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
withFileLock
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=chunk-IV2ZWQA3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/file-lock.ts"],"sourcesContent":["// Cross-process advisory file lock (H-20, minimal tier).\n//\n// The seed pool and the observation inbox are WHOLE-FILE rewrites of DPAPI\n// ciphertext — the vault cannot append to them — and the CLI runs as a second\n// process against the same files. Two interleaved load→mutate→save cycles\n// therefore let one side silently overwrite the other's write. The full fix\n// (append-only journal + compaction) is v2.0 work; this keeps the writers\n// honest in the meantime.\n//\n// The lock itself is a sibling file created with O_EXCL. A lock older than\n// STALE_MS is treated as abandoned (its holder died mid-write) and reclaimed.\n\nimport fs from 'node:fs';\n\nconst STALE_MS = 15_000;\nconst WAIT_MS = 3_000;\nconst POLL_MS = 25;\n\nfunction sleepSync(ms: number): void {\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);\n}\n\nfunction tryAcquire(lock: string): boolean {\n try {\n const fd = fs.openSync(lock, 'wx');\n fs.writeSync(fd, `${process.pid} ${new Date().toISOString()}\\n`);\n fs.closeSync(fd);\n return true;\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code !== 'EEXIST') throw e;\n // Reclaim a lock whose holder is gone.\n try {\n if (Date.now() - fs.statSync(lock).mtimeMs > STALE_MS) {\n fs.rmSync(lock, { force: true });\n return tryAcquire(lock);\n }\n } catch {\n return tryAcquire(lock); // vanished between open and stat\n }\n return false;\n }\n}\n\n/**\n * Run `fn` while holding the lock for `target`. Acquisition waits up to\n * WAIT_MS, then gives up and runs anyway: a heartbeat must not be stopped by a\n * stale lock, and the caller's own read-modify-write is still the last writer\n * in the common (uncontended) case.\n */\nexport function withFileLock<T>(target: string, fn: () => T): T {\n const lock = `${target}.lock`;\n const deadline = Date.now() + WAIT_MS;\n let held = false;\n for (;;) {\n if (tryAcquire(lock)) {\n held = true;\n break;\n }\n if (Date.now() >= deadline) break;\n sleepSync(POLL_MS);\n }\n try {\n return fn();\n } finally {\n if (held) {\n try {\n fs.rmSync(lock, { force: true });\n } catch { /* already gone */ }\n }\n }\n}\n"],"mappings":";AAYA,OAAO,QAAQ;AAEf,IAAM,WAAW;AACjB,IAAM,UAAU;AAChB,IAAM,UAAU;AAEhB,SAAS,UAAU,IAAkB;AACnC,UAAQ,KAAK,IAAI,WAAW,IAAI,kBAAkB,CAAC,CAAC,GAAG,GAAG,GAAG,EAAE;AACjE;AAEA,SAAS,WAAW,MAAuB;AACzC,MAAI;AACF,UAAM,KAAK,GAAG,SAAS,MAAM,IAAI;AACjC,OAAG,UAAU,IAAI,GAAG,QAAQ,GAAG,KAAI,oBAAI,KAAK,GAAE,YAAY,CAAC;AAAA,CAAI;AAC/D,OAAG,UAAU,EAAE;AACf,WAAO;AAAA,EACT,SAAS,GAAG;AACV,QAAK,EAA4B,SAAS,SAAU,OAAM;AAE1D,QAAI;AACF,UAAI,KAAK,IAAI,IAAI,GAAG,SAAS,IAAI,EAAE,UAAU,UAAU;AACrD,WAAG,OAAO,MAAM,EAAE,OAAO,KAAK,CAAC;AAC/B,eAAO,WAAW,IAAI;AAAA,MACxB;AAAA,IACF,QAAQ;AACN,aAAO,WAAW,IAAI;AAAA,IACxB;AACA,WAAO;AAAA,EACT;AACF;AAQO,SAAS,aAAgB,QAAgB,IAAgB;AAC9D,QAAM,OAAO,GAAG,MAAM;AACtB,QAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,MAAI,OAAO;AACX,aAAS;AACP,QAAI,WAAW,IAAI,GAAG;AACpB,aAAO;AACP;AAAA,IACF;AACA,QAAI,KAAK,IAAI,KAAK,SAAU;AAC5B,cAAU,OAAO;AAAA,EACnB;AACA,MAAI;AACF,WAAO,GAAG;AAAA,EACZ,UAAE;AACA,QAAI,MAAM;AACR,UAAI;AACF,WAAG,OAAO,MAAM,EAAE,OAAO,KAAK,CAAC;AAAA,MACjC,QAAQ;AAAA,MAAqB;AAAA,IAC/B;AAAA,EACF;AACF;","names":[]}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
withFileLock
|
|
3
|
+
} from "./chunk-IV2ZWQA3.js";
|
|
1
4
|
import {
|
|
2
5
|
loadEncryptedText,
|
|
3
6
|
saveEncryptedText
|
|
4
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-7TW6DD6Q.js";
|
|
5
8
|
|
|
6
9
|
// src/seeds/pool.ts
|
|
7
10
|
import path from "path";
|
|
@@ -69,6 +72,9 @@ function savePool(guard, file, db) {
|
|
|
69
72
|
const body = db.seeds.map((s) => JSON.stringify(s)).join("\n");
|
|
70
73
|
saveEncryptedText(guard, file, body ? body + "\n" : "");
|
|
71
74
|
}
|
|
75
|
+
function update(guard, file, fn) {
|
|
76
|
+
return withFileLock(file, () => fn(loadPool(guard, file)));
|
|
77
|
+
}
|
|
72
78
|
var activeSeeds = (db) => db.seeds.filter((s) => s.status === "active");
|
|
73
79
|
var archivedSeeds = (db) => db.seeds.filter((s) => s.status === "archived");
|
|
74
80
|
function evictionScore(seed, policy, now) {
|
|
@@ -103,7 +109,14 @@ function archiveSeed(seed, reason, now) {
|
|
|
103
109
|
function retireLimit(s, policy) {
|
|
104
110
|
return normalizeCategory(s.category) === "chat" ? 1 : policy.seeds.retireAfterUsed;
|
|
105
111
|
}
|
|
112
|
+
function isConsumed(s, policy, prevUsedAt, sinceEvidence) {
|
|
113
|
+
if (s.used < retireLimit(s, policy)) return false;
|
|
114
|
+
return !(prevUsedAt > 0 && sinceEvidence > prevUsedAt);
|
|
115
|
+
}
|
|
106
116
|
function addSeed(guard, file, policy, input, now = Date.now(), audit = NO_AUDIT) {
|
|
117
|
+
return withFileLock(file, () => addSeedLocked(guard, file, policy, input, now, audit));
|
|
118
|
+
}
|
|
119
|
+
function addSeedLocked(guard, file, policy, input, now, audit) {
|
|
107
120
|
const db = loadPool(guard, file);
|
|
108
121
|
const text = input.text.trim();
|
|
109
122
|
if (!text) throw new Error("seed text must not be empty");
|
|
@@ -189,84 +202,97 @@ function addSeed(guard, file, policy, input, now = Date.now(), audit = NO_AUDIT)
|
|
|
189
202
|
return { kind: "added", seed, evicted };
|
|
190
203
|
}
|
|
191
204
|
function gcPool(guard, file, policy, now = Date.now(), audit = NO_AUDIT) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
205
|
+
return update(guard, file, (db) => {
|
|
206
|
+
const report = { consumed: 0, expired: 0, coldBench: 0, activeAfter: 0, archiveTrimmed: 0 };
|
|
207
|
+
for (const s of activeSeeds(db)) {
|
|
208
|
+
const ageMs = now - parseIso(s.bornAt);
|
|
209
|
+
const sinceEvidence = parseIso(s.lastEvidenceAt);
|
|
210
|
+
const sinceUsed = s.lastUsedAt ? parseIso(s.lastUsedAt) : 0;
|
|
211
|
+
if (isConsumed(s, policy, sinceUsed, sinceEvidence)) {
|
|
212
|
+
archiveSeed(s, "consumed", now);
|
|
213
|
+
report.consumed += 1;
|
|
214
|
+
emit(audit, { event: "seed_retired", id: s.id, reason: "consumed", via: "gc", used: s.used });
|
|
215
|
+
} else if (now > parseIso(s.expiresAt)) {
|
|
216
|
+
archiveSeed(s, "expired", now);
|
|
217
|
+
report.expired += 1;
|
|
218
|
+
emit(audit, { event: "seed_retired", id: s.id, reason: "expired", via: "gc", used: s.used });
|
|
219
|
+
} else if (s.used === 0 && ageMs >= policy.seeds.coldBenchDays * DAY_MS) {
|
|
220
|
+
archiveSeed(s, "cold_bench", now);
|
|
221
|
+
report.coldBench += 1;
|
|
222
|
+
emit(audit, { event: "seed_retired", id: s.id, reason: "cold_bench", via: "gc", used: s.used });
|
|
223
|
+
}
|
|
210
224
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
225
|
+
const archived = db.seeds.filter((s) => s.status === "archived");
|
|
226
|
+
if (archived.length > policy.seeds.archiveCap) {
|
|
227
|
+
const drop = [...archived].sort((a, b) => parseIso(a.retiredAt ?? "") - parseIso(b.retiredAt ?? "")).slice(0, archived.length - policy.seeds.archiveCap);
|
|
228
|
+
const dropped = new Set(drop.map((s) => s.id));
|
|
229
|
+
db.seeds = db.seeds.filter((s) => !dropped.has(s.id));
|
|
230
|
+
report.archiveTrimmed = drop.length;
|
|
231
|
+
emit(audit, { event: "seed_archive_trimmed", count: drop.length, ids: drop.slice(0, 50).map((s) => s.id) });
|
|
232
|
+
}
|
|
233
|
+
report.activeAfter = activeSeeds(db).length;
|
|
234
|
+
savePool(guard, file, db);
|
|
235
|
+
return report;
|
|
236
|
+
});
|
|
223
237
|
}
|
|
224
238
|
function surfaceSeed(guard, file, policy, id, now = Date.now(), audit = NO_AUDIT) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
239
|
+
return update(guard, file, (db) => {
|
|
240
|
+
const s = db.seeds.find((x) => x.id === id && x.status === "active");
|
|
241
|
+
if (!s) return null;
|
|
242
|
+
const prevUsedAt = s.lastUsedAt ? parseIso(s.lastUsedAt) : 0;
|
|
243
|
+
s.used += 1;
|
|
244
|
+
s.lastUsedAt = new Date(now).toISOString();
|
|
245
|
+
if (isConsumed(s, policy, prevUsedAt, parseIso(s.lastEvidenceAt))) {
|
|
246
|
+
archiveSeed(s, "consumed", now);
|
|
247
|
+
emit(audit, { event: "seed_retired", id: s.id, reason: "consumed", via: "surface", used: s.used });
|
|
248
|
+
}
|
|
249
|
+
savePool(guard, file, db);
|
|
250
|
+
return s;
|
|
251
|
+
});
|
|
236
252
|
}
|
|
237
253
|
function archiveSeedById(guard, file, id, reason = "completed", now = Date.now(), audit = NO_AUDIT) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
254
|
+
return update(guard, file, (db) => {
|
|
255
|
+
const s = db.seeds.find((x) => x.id === id && x.status === "active");
|
|
256
|
+
if (!s) return null;
|
|
257
|
+
archiveSeed(s, reason, now);
|
|
258
|
+
emit(audit, { event: "seed_retired", id: s.id, reason, via: "manual" });
|
|
259
|
+
savePool(guard, file, db);
|
|
260
|
+
return s;
|
|
261
|
+
});
|
|
245
262
|
}
|
|
246
263
|
function restoreSeed(guard, file, policy, id, now = Date.now(), audit = NO_AUDIT) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
264
|
+
return update(guard, file, (db) => {
|
|
265
|
+
const s = db.seeds.find((x) => x.id === id && x.status === "archived");
|
|
266
|
+
if (!s) return { ok: false, reason: "archived seed not found" };
|
|
267
|
+
const category = normalizeCategory(s.category);
|
|
268
|
+
const catCap = SEED_CATEGORY_CAPS[category];
|
|
269
|
+
const inCategory = activeSeeds(db).filter((x) => normalizeCategory(x.category) === category).length;
|
|
270
|
+
if (inCategory >= catCap) {
|
|
271
|
+
return { ok: false, reason: `category full (${category}: ${catCap}); archive something in it first` };
|
|
272
|
+
}
|
|
273
|
+
if (activeSeeds(db).length >= policy.seeds.maxActive) {
|
|
274
|
+
return { ok: false, reason: `pool full (${policy.seeds.maxActive}); archive something first` };
|
|
275
|
+
}
|
|
276
|
+
s.status = "active";
|
|
277
|
+
s.retireReason = void 0;
|
|
278
|
+
s.retiredAt = void 0;
|
|
279
|
+
s.bornAt = new Date(now).toISOString();
|
|
280
|
+
s.expiresAt = new Date(now + (policy.seeds.ttlDays[s.tag] || 14) * DAY_MS).toISOString();
|
|
281
|
+
s.lastEvidenceAt = new Date(now).toISOString();
|
|
282
|
+
emit(audit, { event: "seed_restored", id: s.id });
|
|
283
|
+
savePool(guard, file, db);
|
|
284
|
+
return { ok: true, seed: s };
|
|
285
|
+
});
|
|
261
286
|
}
|
|
262
287
|
function deleteSeed(guard, file, id, audit = NO_AUDIT) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
288
|
+
return update(guard, file, (db) => {
|
|
289
|
+
const before = db.seeds.length;
|
|
290
|
+
db.seeds = db.seeds.filter((x) => x.id !== id);
|
|
291
|
+
if (db.seeds.length === before) return false;
|
|
292
|
+
emit(audit, { event: "seed_deleted", id });
|
|
293
|
+
savePool(guard, file, db);
|
|
294
|
+
return true;
|
|
295
|
+
});
|
|
270
296
|
}
|
|
271
297
|
|
|
272
298
|
export {
|
|
@@ -288,4 +314,4 @@ export {
|
|
|
288
314
|
restoreSeed,
|
|
289
315
|
deleteSeed
|
|
290
316
|
};
|
|
291
|
-
//# sourceMappingURL=chunk-
|
|
317
|
+
//# sourceMappingURL=chunk-KOOQOMQY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/seeds/pool.ts","../src/seeds/types.ts"],"sourcesContent":["// Material pool engine (design doc §4). All four eviction rules are\n// deterministic code - no LLM involvement (§4.2). Storage: data/seeds.jsonl,\n// one seed per JSON line, DPAPI-encrypted at rest (D14).\n\nimport path from 'node:path';\nimport type { PathGuard } from '../core/path-guard.js';\nimport { loadEncryptedText, saveEncryptedText } from '../vault/vault.js';\nimport { withFileLock } from '../core/file-lock.js';\nimport type { Policy } from '../config/schema.js';\nimport {\n SEED_SOURCE_DEFAULT_CONFIDENCE,\n emptySeedDb,\n type Seed,\n type SeedCategory,\n type SeedDb,\n type SeedRetireReason,\n type SeedSource,\n type SeedTag,\n} from './types.js';\n\nconst DAY_MS = 86_400_000;\n\n// ── lifecycle audit (2026-10-06 / v1.9.0) ───────────────────────────────\n// The pool used to mutate in silence: a seed could appear, be evicted for\n// capacity, or be dropped from the archive with nothing in the log. Every\n// mutator now takes an optional sink and reports what it did. IDs and\n// metadata ONLY — heartbeat.jsonl is plaintext by charter and must never\n// carry conversation text; the seed text itself stays in the DPAPI pool.\nexport type SeedAuditFn = (entry: Record<string, unknown>) => void;\n\nconst NO_AUDIT: SeedAuditFn = () => {};\n\n/** Audit must never break the pool (or the beat that called it). */\nfunction emit(audit: SeedAuditFn, entry: Record<string, unknown>): void {\n try {\n audit(entry);\n } catch {\n /* audit is diagnostic; a failed append is not a pool failure */\n }\n}\n\n// Per-category pool caps (2026-09-18 spec ⑤): topic stock vs conversation-grown\n// chat material, decoupled from `source`; sum == the global maxActive default.\nexport const SEED_CATEGORY_CAPS: Record<SeedCategory, number> = { topic: 16, chat: 14 };\n\nconst CATEGORY_VALUES: readonly string[] = ['topic', 'chat'];\n\n/** Lenient category coercion for on-disk rows: anything missing/unknown (all\n * pre-v1.5 seeds) counts as topic stock. */\nexport function normalizeCategory(v: unknown): SeedCategory {\n return typeof v === 'string' && (CATEGORY_VALUES as readonly string[]).includes(v)\n ? (v as SeedCategory)\n : 'topic';\n}\n\nexport interface AddSeedInput {\n text: string;\n topic?: string;\n tag?: SeedTag;\n source?: SeedSource;\n /** Loop role (spec ⑤); defaults from source: chat->chat, everything else->topic. */\n category?: SeedCategory;\n confidence?: number;\n}\n\nexport interface AddSeedResult {\n kind: 'added' | 'merged' | 'duplicate';\n seed: Seed;\n evicted?: Seed;\n}\n\nexport interface GcReport {\n consumed: number;\n expired: number;\n coldBench: number;\n activeAfter: number;\n /** v1.9.0: archived rows dropped to honor seeds.archiveCap (oldest first). */\n archiveTrimmed: number;\n}\n\nexport const TTL_KEYS: readonly SeedTag[] = ['news', 'fandom', 'scene', 'promise'];\n\nexport function normalizeTag(tag: string | undefined): SeedTag {\n if (tag && (TTL_KEYS as readonly string[]).includes(tag)) return tag as SeedTag;\n return 'scene';\n}\n\nfunction parseIso(v: string): number {\n const t = Date.parse(v);\n return Number.isFinite(t) ? t : 0;\n}\n\n// ── storage ─────────────────────────────────────────────────────────────\n\nexport function seedsFilePath(dataDir: string): string {\n return path.join(dataDir, 'seeds.jsonl');\n}\n\nexport function loadPool(guard: PathGuard, file: string): SeedDb {\n const raw = loadEncryptedText(guard, file);\n if (raw === null) return emptySeedDb();\n const db = emptySeedDb();\n for (const line of raw.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n try {\n const obj = JSON.parse(trimmed) as Seed;\n if (typeof obj.id === 'string' && obj.id.startsWith('s')) {\n // spec ⑤ loadPool tolerance: only the id is load-bearing; a missing or\n // unknown category (every pre-v1.5 row) falls back to topic stock.\n obj.category = normalizeCategory(obj.category);\n db.seeds.push(obj);\n const n = Number(obj.id.slice(1));\n if (Number.isFinite(n) && n > db.seq) db.seq = n;\n }\n } catch {\n // corrupt line: skip (pool is a cache; journal-level audit lives elsewhere)\n }\n }\n return db;\n}\n\nexport function savePool(guard: PathGuard, file: string, db: SeedDb): void {\n const body = db.seeds.map((s) => JSON.stringify(s)).join('\\n');\n saveEncryptedText(guard, file, body ? body + '\\n' : '');\n}\n\n/**\n * H-20 (minimal tier): every pool writer goes through here, so the load happens\n * INSIDE the cross-process lock and cannot be based on a read that went stale\n * while it was being mutated — the CLI is a second process sharing this file,\n * and an interleaved write used to be silently overwritten. The ciphertext is a\n * whole-file rewrite, so append/merge does not apply here; the append-only pool\n * with compaction is v2.0 work (ledger #8649f9).\n */\nfunction update<T>(guard: PathGuard, file: string, fn: (db: SeedDb) => T): T {\n return withFileLock(file, () => fn(loadPool(guard, file)));\n}\n\n// ── queries ─────────────────────────────────────────────────────────────\n\nexport const activeSeeds = (db: SeedDb): Seed[] => db.seeds.filter((s) => s.status === 'active');\nexport const archivedSeeds = (db: SeedDb): Seed[] => db.seeds.filter((s) => s.status === 'archived');\n\n// ── eviction scoring (rule 4) ───────────────────────────────────────────\n\n/**\n * Comprehensive score (§4.2): freshness x0.4 + unused x0.3 + confidence x0.3.\n * Higher = keep. Lowest-scoring active is evicted when the pool is full.\n */\nexport function evictionScore(seed: Seed, policy: Policy, now: number): number {\n const ttlDays = policy.seeds.ttlDays[seed.tag] || 14;\n const daysStale = Math.max(0, (now - parseIso(seed.lastEvidenceAt)) / DAY_MS);\n const freshness = Math.max(0, 1 - daysStale / ttlDays);\n const unused = seed.used === 0 ? 1 : 1 / seed.used;\n const w = policy.seeds.scoreWeights;\n return freshness * w.freshness + unused * w.unused + seed.confidence * w.confidence;\n}\n\n/** Pick the eviction victim: unprotected actives first (§4.2 加权保护); with\n * `category`, only that category's actives compete (spec ⑤ per-category caps). */\nexport function pickEvictionVictim(db: SeedDb, policy: Policy, now: number, category?: SeedCategory): Seed | null {\n const actives = activeSeeds(db).filter((s) => category === undefined || normalizeCategory(s.category) === category);\n if (actives.length === 0) return null;\n const unprotected = actives.filter((s) => !s.protected);\n const candidates = unprotected.length > 0 ? unprotected : actives;\n let worst: Seed | null = null;\n let worstScore = Number.POSITIVE_INFINITY;\n for (const s of candidates) {\n const score = evictionScore(s, policy, now);\n if (score < worstScore) {\n worstScore = score;\n worst = s;\n }\n }\n return worst;\n}\n\nfunction archiveSeed(seed: Seed, reason: SeedRetireReason, now: number): void {\n seed.status = 'archived';\n seed.retireReason = reason;\n seed.retiredAt = new Date(now).toISOString();\n}\n\n/** Spec ④: chat-grown material is consumed ONCE (conversation freshness);\n * every other category follows the policy-wide retireAfterUsed. */\nfunction retireLimit(s: Seed, policy: Policy): number {\n return normalizeCategory(s.category) === 'chat' ? 1 : policy.seeds.retireAfterUsed;\n}\n\n/**\n * Rule 1 (§4.2): used up to its limit AND no evidence newer than the PREVIOUS\n * use. `prevUsedAt` MUST be read before bumping `lastUsedAt` — comparing the\n * just-written value against `lastEvidenceAt` is vacuously true (H-22), which\n * retired seeds that had received fresh evidence in the same beat. Both\n * callers (gc and surface) share this, so the rule cannot drift again.\n *\n * `prevUsedAt === 0` means \"never used before\": there is no earlier use for\n * the evidence to be newer THAN, so the exemption cannot apply — a one-shot\n * (chat) seed still retires on its first surfacing (spec ④), which is the\n * contract, not a regression.\n */\nfunction isConsumed(s: Seed, policy: Policy, prevUsedAt: number, sinceEvidence: number): boolean {\n if (s.used < retireLimit(s, policy)) return false;\n return !(prevUsedAt > 0 && sinceEvidence > prevUsedAt);\n}\n\n// ── operations ──────────────────────────────────────────────────────────\n\nexport function addSeed(\n guard: PathGuard,\n file: string,\n policy: Policy,\n input: AddSeedInput,\n now = Date.now(),\n audit: SeedAuditFn = NO_AUDIT,\n): AddSeedResult {\n // H-20: the read-modify-write below runs under the cross-process pool lock\n // (same guard as `update`, kept as a named body to avoid re-indenting it).\n return withFileLock(file, () => addSeedLocked(guard, file, policy, input, now, audit));\n}\n\nfunction addSeedLocked(\n guard: PathGuard,\n file: string,\n policy: Policy,\n input: AddSeedInput,\n now: number,\n audit: SeedAuditFn,\n): AddSeedResult {\n const db = loadPool(guard, file);\n const text = input.text.trim();\n if (!text) throw new Error('seed text must not be empty');\n const tag = normalizeTag(input.tag);\n const source: SeedSource = input.source ?? 'chat';\n const category = normalizeCategory(input.category ?? (source === 'chat' ? 'chat' : 'topic'));\n const confidence = input.confidence ?? SEED_SOURCE_DEFAULT_CONFIDENCE[source];\n\n // Exact-text dedup against actives.\n const dup = activeSeeds(db).find((s) => s.text === text);\n if (dup) return { kind: 'duplicate', seed: dup };\n\n const nowIso = new Date(now).toISOString();\n const ttlMs = (policy.seeds.ttlDays[tag] || 14) * DAY_MS;\n\n // Same-topic merge (§4.3): collapse all same-topic actives + the new item.\n const sameTopic = activeSeeds(db).filter((s) => s.topic === (input.topic ?? text.slice(0, 24)));\n let seed: Seed;\n if (sameTopic.length > 0) {\n const kept = sameTopic[0]!;\n kept.text = text; // brief takes the latest\n kept.tag = tag;\n kept.source = source;\n kept.category = normalizeCategory(input.category ?? kept.category ?? category);\n kept.confidence = Math.max(kept.confidence, confidence);\n kept.used = sameTopic.reduce((acc, s) => acc + s.used, 0); // used accumulates\n kept.lastEvidenceAt = [kept.lastEvidenceAt, nowIso, ...sameTopic.map((s) => s.lastEvidenceAt)]\n .reduce((a, b) => (parseIso(b) > parseIso(a) ? b : a));\n kept.expiresAt = new Date(Math.max(parseIso(kept.expiresAt), now + ttlMs)).toISOString();\n kept.protected = kept.protected || source === 'hand' || (source === 'profile' && confidence >= 0.7);\n // remove the other same-topic actives (merged into kept)\n for (const extra of sameTopic.slice(1)) {\n extra.status = 'archived';\n extra.retireReason = 'completed';\n extra.retiredAt = nowIso;\n emit(audit, { event: 'seed_retired', id: extra.id, reason: 'completed', via: 'merge', into: kept.id });\n }\n seed = kept;\n // capacity still applies after growth check below if kept is somehow over cap\n if (activeSeeds(db).length > policy.seeds.maxActive) {\n const victim = pickEvictionVictim(db, policy, now, normalizeCategory(kept.category));\n if (victim && victim.id !== kept.id) {\n archiveSeed(victim, 'pool_cap', now);\n emit(audit, { event: 'seed_added', id: kept.id, kind: 'merged', tag: kept.tag, source: kept.source, category: kept.category, confidence: kept.confidence, mergedFrom: sameTopic.length - 1 });\n emit(audit, { event: 'seed_retired', id: victim.id, reason: 'pool_cap', via: 'merge', into: kept.id });\n savePool(guard, file, db);\n return { kind: 'merged', seed: kept, evicted: victim };\n }\n }\n emit(audit, { event: 'seed_added', id: kept.id, kind: 'merged', tag: kept.tag, source: kept.source, category: kept.category, confidence: kept.confidence, mergedFrom: sameTopic.length - 1 });\n savePool(guard, file, db);\n return { kind: 'merged', seed: kept };\n }\n\n // Capacity first (rules 4 + spec ⑤): per-category cap evicts within its own\n // pool; the global cap evicts in the incoming seed's category (a full topic\n // stock must not push chat material out and vice versa).\n let evicted: Seed | undefined;\n const catCount = activeSeeds(db).filter((s) => normalizeCategory(s.category) === category).length;\n if (catCount >= SEED_CATEGORY_CAPS[category]) {\n const victim = pickEvictionVictim(db, policy, now, category);\n if (victim) {\n archiveSeed(victim, 'pool_cap', now);\n evicted = victim;\n }\n }\n if (activeSeeds(db).length >= policy.seeds.maxActive) {\n const victim = pickEvictionVictim(db, policy, now, category);\n if (victim) {\n archiveSeed(victim, 'pool_cap', now);\n evicted = evicted ?? victim;\n }\n }\n\n db.seq += 1;\n seed = {\n id: `s${db.seq}`,\n text,\n topic: input.topic ?? text.slice(0, 24),\n tag,\n source,\n category,\n confidence,\n protected: source === 'hand' || (source === 'profile' && confidence >= 0.7),\n used: 0,\n bornAt: nowIso,\n expiresAt: new Date(now + ttlMs).toISOString(),\n lastUsedAt: null,\n lastEvidenceAt: nowIso,\n status: 'active',\n };\n db.seeds.push(seed);\n emit(audit, { event: 'seed_added', id: seed.id, kind: 'added', tag: seed.tag, source: seed.source, category: seed.category, confidence: seed.confidence });\n if (evicted) emit(audit, { event: 'seed_retired', id: evicted.id, reason: 'pool_cap', via: 'add', into: seed.id });\n savePool(guard, file, db);\n return { kind: 'added', seed, evicted };\n}\n\n/**\n * Deterministic gc (rules 1-3 of §4.2). Rule 4 (pool cap) fires on add only.\n * v1.9.0: the archive area is ALSO capped (seeds.archiveCap) — the archive is\n * a forensic trail, not a hoard; trim oldest-retired first.\n */\nexport function gcPool(guard: PathGuard, file: string, policy: Policy, now = Date.now(), audit: SeedAuditFn = NO_AUDIT): GcReport {\n return update(guard, file, (db) => {\n const report: GcReport = { consumed: 0, expired: 0, coldBench: 0, activeAfter: 0, archiveTrimmed: 0 };\n for (const s of activeSeeds(db)) {\n const ageMs = now - parseIso(s.bornAt);\n const sinceEvidence = parseIso(s.lastEvidenceAt);\n const sinceUsed = s.lastUsedAt ? parseIso(s.lastUsedAt) : 0;\n if (isConsumed(s, policy, sinceUsed, sinceEvidence)) {\n archiveSeed(s, 'consumed', now);\n report.consumed += 1;\n emit(audit, { event: 'seed_retired', id: s.id, reason: 'consumed', via: 'gc', used: s.used });\n } else if (now > parseIso(s.expiresAt)) {\n archiveSeed(s, 'expired', now);\n report.expired += 1;\n emit(audit, { event: 'seed_retired', id: s.id, reason: 'expired', via: 'gc', used: s.used });\n } else if (s.used === 0 && ageMs >= policy.seeds.coldBenchDays * DAY_MS) {\n archiveSeed(s, 'cold_bench', now);\n report.coldBench += 1;\n emit(audit, { event: 'seed_retired', id: s.id, reason: 'cold_bench', via: 'gc', used: s.used });\n }\n }\n // archive cap: drop the oldest-retired rows beyond the cap (retiredAt may be\n // missing on pre-v1.5 rows — parseIso('') = 0 sorts them oldest).\n const archived = db.seeds.filter((s) => s.status === 'archived');\n if (archived.length > policy.seeds.archiveCap) {\n const drop = [...archived]\n .sort((a, b) => parseIso(a.retiredAt ?? '') - parseIso(b.retiredAt ?? ''))\n .slice(0, archived.length - policy.seeds.archiveCap);\n const dropped = new Set(drop.map((s) => s.id));\n db.seeds = db.seeds.filter((s) => !dropped.has(s.id));\n report.archiveTrimmed = drop.length;\n // These rows are GONE (not archived — deleted): the log is the only trace\n // left, so name them. IDs only, and cap the list in case a first GC trims\n // a legacy archive wholesale.\n emit(audit, { event: 'seed_archive_trimmed', count: drop.length, ids: drop.slice(0, 50).map((s) => s.id) });\n }\n report.activeAfter = activeSeeds(db).length;\n savePool(guard, file, db);\n return report;\n });\n}\n\n/** The seed surfaced in a real expression: count + maybe retire (rule 1). */\nexport function surfaceSeed(\n guard: PathGuard,\n file: string,\n policy: Policy,\n id: string,\n now = Date.now(),\n audit: SeedAuditFn = NO_AUDIT,\n): Seed | null {\n return update(guard, file, (db) => {\n const s = db.seeds.find((x) => x.id === id && x.status === 'active');\n if (!s) return null;\n // H-22: snapshot the PREVIOUS use time before bumping it — the retirement\n // check must compare against the last use, not against this one.\n const prevUsedAt = s.lastUsedAt ? parseIso(s.lastUsedAt) : 0;\n s.used += 1;\n s.lastUsedAt = new Date(now).toISOString();\n if (isConsumed(s, policy, prevUsedAt, parseIso(s.lastEvidenceAt))) {\n archiveSeed(s, 'consumed', now);\n emit(audit, { event: 'seed_retired', id: s.id, reason: 'consumed', via: 'surface', used: s.used });\n }\n savePool(guard, file, db);\n return s;\n });\n}\n\n/** Deliberate retirement (item completed / no longer relevant). */\nexport function archiveSeedById(\n guard: PathGuard,\n file: string,\n id: string,\n reason: SeedRetireReason = 'completed',\n now = Date.now(),\n audit: SeedAuditFn = NO_AUDIT,\n): Seed | null {\n return update(guard, file, (db) => {\n const s = db.seeds.find((x) => x.id === id && x.status === 'active');\n if (!s) return null;\n archiveSeed(s, reason, now);\n emit(audit, { event: 'seed_retired', id: s.id, reason, via: 'manual' });\n savePool(guard, file, db);\n return s;\n });\n}\n\n/** Restore an archived seed to the active pool (UI operation; resets TTL). */\nexport function restoreSeed(\n guard: PathGuard,\n file: string,\n policy: Policy,\n id: string,\n now = Date.now(),\n audit: SeedAuditFn = NO_AUDIT,\n): { ok: true; seed: Seed } | { ok: false; reason: string } {\n return update<{ ok: true; seed: Seed } | { ok: false; reason: string }>(guard, file, (db) => {\n const s = db.seeds.find((x) => x.id === id && x.status === 'archived');\n if (!s) return { ok: false, reason: 'archived seed not found' };\n // H-23: a restore must satisfy the same two caps an add does — the global\n // one AND the per-category one. The add path evicts a neighbour instead, but\n // restoring a memory is a user action: refuse it rather than silently\n // pushing someone else's material out.\n const category = normalizeCategory(s.category);\n const catCap = SEED_CATEGORY_CAPS[category];\n const inCategory = activeSeeds(db).filter((x) => normalizeCategory(x.category) === category).length;\n if (inCategory >= catCap) {\n return { ok: false, reason: `category full (${category}: ${catCap}); archive something in it first` };\n }\n if (activeSeeds(db).length >= policy.seeds.maxActive) {\n return { ok: false, reason: `pool full (${policy.seeds.maxActive}); archive something first` };\n }\n s.status = 'active';\n s.retireReason = undefined;\n s.retiredAt = undefined;\n // H-23: reset bornAt as well — cold-bench eviction (used === 0 && age >=\n // coldBenchDays) keys off bornAt, so a restored seed used to be re-archived\n // by the very next maintenance beat (\"restored, then gone one beat later\").\n s.bornAt = new Date(now).toISOString();\n s.expiresAt = new Date(now + (policy.seeds.ttlDays[s.tag] || 14) * DAY_MS).toISOString();\n s.lastEvidenceAt = new Date(now).toISOString();\n emit(audit, { event: 'seed_restored', id: s.id });\n savePool(guard, file, db);\n return { ok: true, seed: s };\n });\n}\n\n/** Hard delete (UI explicit action with confirm; the caller audits it). */\nexport function deleteSeed(guard: PathGuard, file: string, id: string, audit: SeedAuditFn = NO_AUDIT): boolean {\n return update(guard, file, (db) => {\n const before = db.seeds.length;\n db.seeds = db.seeds.filter((x) => x.id !== id);\n if (db.seeds.length === before) return false;\n emit(audit, { event: 'seed_deleted', id });\n savePool(guard, file, db);\n return true;\n });\n}\n","// Material pool (\"素材池\") types. Charter (axiom 3): this pool is a CACHE,\n// not memory - volatile, evictable, burnable; long-term memory only absorbs\n// conversation-validated content. Everything here may be destroyed.\n\nexport type SeedTag = 'news' | 'fandom' | 'scene' | 'promise';\nexport type SeedSource = 'chat' | 'screen' | 'browse' | 'hand' | 'profile';\nexport type SeedCategory = 'topic' | 'chat';\nexport type SeedRetireReason = 'consumed' | 'expired' | 'cold_bench' | 'pool_cap' | 'completed';\n\nexport interface Seed {\n id: string;\n /** Short brief shown to the reflection prompt. */\n text: string;\n /** Merge key (§4.3): same-topic actives collapse into one. */\n topic: string;\n tag: SeedTag;\n source: SeedSource;\n /** Loop role, decoupled from source (2026-09-18 spec ⑤): 'topic' = wander/\n * hand/profile stock (random pick), 'chat' = conversation-grown, consumed\n * once. Absent on pre-v1.5 rows; loadPool defaults them to 'topic'. */\n category?: SeedCategory;\n /** 0..1 trust level; handwriting and high-confidence profile sources gain eviction protection. */\n confidence: number;\n /** Eviction protection (§4.2 pool-cap rule): handwritten / high-confidence profile sources. */\n protected: boolean;\n /** Times this seed surfaced in an actual expression. */\n used: number;\n bornAt: string;\n /** bornAt + ttlDays[tag]; archived when passed (rule 2). */\n expiresAt: string;\n lastUsedAt: string | null;\n /** Fresh-evidence watermark: rule 1 spares seeds with evidence newer than last use. */\n lastEvidenceAt: string;\n status: 'active' | 'archived';\n retireReason?: SeedRetireReason;\n retiredAt?: string;\n}\n\nexport interface SeedDb {\n seq: number;\n seeds: Seed[];\n}\n\nexport function emptySeedDb(): SeedDb {\n return { seq: 0, seeds: [] };\n}\n\nexport const SEED_SOURCE_DEFAULT_CONFIDENCE: Record<SeedSource, number> = {\n hand: 1.0,\n profile: 0.7,\n chat: 0.6,\n screen: 0.4,\n browse: 0.4,\n};\n"],"mappings":"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","names":[]}
|
|
@@ -3,55 +3,28 @@ import {
|
|
|
3
3
|
readText,
|
|
4
4
|
saveJson,
|
|
5
5
|
writeText
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-7TW6DD6Q.js";
|
|
7
|
+
import {
|
|
8
|
+
atomicWriteFileSync
|
|
9
|
+
} from "./chunk-WRUTATW4.js";
|
|
7
10
|
|
|
8
11
|
// src/core/audit-log.ts
|
|
9
|
-
import fs2 from "fs";
|
|
10
|
-
import path2 from "path";
|
|
11
|
-
|
|
12
|
-
// src/core/atomic-fs.ts
|
|
13
12
|
import fs from "fs";
|
|
14
13
|
import path from "path";
|
|
15
|
-
|
|
16
|
-
function tmpSibling(target, tag = "w") {
|
|
17
|
-
return path.join(
|
|
18
|
-
path.dirname(target),
|
|
19
|
-
`.${path.basename(target)}.${tag}-${randomUUID().slice(0, 8)}.tmp`
|
|
20
|
-
);
|
|
21
|
-
}
|
|
22
|
-
function atomicWriteFileSync(target, data) {
|
|
23
|
-
const tmp = tmpSibling(target);
|
|
14
|
+
function appendAuditLine(file, event) {
|
|
24
15
|
try {
|
|
25
|
-
fs.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
function atomicWriteJsonSync(target, value) {
|
|
32
|
-
atomicWriteFileSync(target, JSON.stringify(value, null, 2));
|
|
33
|
-
}
|
|
34
|
-
function shredFileSync(target, passes = 3) {
|
|
35
|
-
const stat = fs.statSync(target);
|
|
36
|
-
if (!stat.isFile()) throw new Error(`shred: not a file: ${target}`);
|
|
37
|
-
const buf = Buffer.alloc(Math.max(stat.size, 1));
|
|
38
|
-
for (let i = 0; i < passes; i++) {
|
|
39
|
-
randomFillSync(buf);
|
|
40
|
-
fs.writeFileSync(target, buf);
|
|
16
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
17
|
+
const line = JSON.stringify({ ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(), ...event });
|
|
18
|
+
fs.appendFileSync(file, line + "\n", "utf8");
|
|
19
|
+
} catch (e) {
|
|
20
|
+
process.stderr.write(`[heartbeat] audit append failed: ${file}: ${String(e)}
|
|
21
|
+
`);
|
|
41
22
|
}
|
|
42
|
-
fs.rmSync(target, { force: true });
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// src/core/audit-log.ts
|
|
46
|
-
function appendAuditLine(file, event) {
|
|
47
|
-
fs2.mkdirSync(path2.dirname(file), { recursive: true });
|
|
48
|
-
const line = JSON.stringify({ ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(), ...event });
|
|
49
|
-
fs2.appendFileSync(file, line + "\n", "utf8");
|
|
50
23
|
}
|
|
51
24
|
function readAuditLines(file) {
|
|
52
|
-
if (!
|
|
25
|
+
if (!fs.existsSync(file)) return [];
|
|
53
26
|
const out = [];
|
|
54
|
-
const raw =
|
|
27
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
55
28
|
for (const line of raw.split("\n")) {
|
|
56
29
|
const trimmed = line.trim();
|
|
57
30
|
if (!trimmed) continue;
|
|
@@ -64,7 +37,7 @@ function readAuditLines(file) {
|
|
|
64
37
|
return out;
|
|
65
38
|
}
|
|
66
39
|
function pruneAuditFile(file, maxAgeMs, now = Date.now()) {
|
|
67
|
-
if (!
|
|
40
|
+
if (!fs.existsSync(file)) return 0;
|
|
68
41
|
const lines = readAuditLines(file);
|
|
69
42
|
const kept = lines.filter((e) => {
|
|
70
43
|
const ev = e;
|
|
@@ -80,9 +53,9 @@ function pruneAuditFile(file, maxAgeMs, now = Date.now()) {
|
|
|
80
53
|
}
|
|
81
54
|
|
|
82
55
|
// src/profile/store.ts
|
|
83
|
-
import
|
|
84
|
-
import { randomUUID
|
|
85
|
-
import
|
|
56
|
+
import path3 from "path";
|
|
57
|
+
import { randomUUID } from "crypto";
|
|
58
|
+
import fs3 from "fs";
|
|
86
59
|
|
|
87
60
|
// src/profile/types.ts
|
|
88
61
|
var PARTITIONS = ["interest", "projects", "comm", "psy"];
|
|
@@ -101,18 +74,34 @@ function emptyProfile() {
|
|
|
101
74
|
}
|
|
102
75
|
|
|
103
76
|
// src/profile/schema.ts
|
|
104
|
-
import
|
|
105
|
-
import
|
|
77
|
+
import fs2 from "fs";
|
|
78
|
+
import path2 from "path";
|
|
106
79
|
function loadProfileSchema(paths) {
|
|
107
|
-
const userPath =
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
80
|
+
const userPath = path2.join(paths.settingsDir, "profile-schema.json");
|
|
81
|
+
const factoryPath = path2.join(paths.configDir, "profile-schema.json");
|
|
82
|
+
const read = (file) => {
|
|
83
|
+
try {
|
|
84
|
+
const raw = JSON.parse(fs2.readFileSync(file, "utf8"));
|
|
85
|
+
if (!raw.partitions) return { error: "partitions missing" };
|
|
86
|
+
return { schema: raw, error: "" };
|
|
87
|
+
} catch (e) {
|
|
88
|
+
return { error: String(e) };
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const user = fs2.existsSync(userPath) ? read(userPath) : null;
|
|
92
|
+
if (user?.schema) return { schema: user.schema, source: "user" };
|
|
93
|
+
const factory = read(factoryPath);
|
|
94
|
+
if (factory.schema) {
|
|
95
|
+
return user ? {
|
|
96
|
+
schema: factory.schema,
|
|
97
|
+
source: "factory",
|
|
98
|
+
fallbackReason: `user profile-schema.json unusable (${user.error}) \u2014 fell back to the factory copy`
|
|
99
|
+
} : { schema: factory.schema, source: "factory" };
|
|
115
100
|
}
|
|
101
|
+
return {
|
|
102
|
+
source: "none",
|
|
103
|
+
fallbackReason: `no usable profile-schema.json (user: ${user?.error ?? "absent"}; factory: ${factory.error})`
|
|
104
|
+
};
|
|
116
105
|
}
|
|
117
106
|
function checkAddAgainstSchema(schema, partition, topic, subTopic, nominated) {
|
|
118
107
|
const p = schema.partitions[partition];
|
|
@@ -137,15 +126,15 @@ function checkAddAgainstSchema(schema, partition, topic, subTopic, nominated) {
|
|
|
137
126
|
// src/profile/store.ts
|
|
138
127
|
var DAY_MS = 864e5;
|
|
139
128
|
function profileFilePath(dataDir) {
|
|
140
|
-
return
|
|
129
|
+
return path3.join(dataDir, "profile.json");
|
|
141
130
|
}
|
|
142
131
|
function journalFilePath(dataDir) {
|
|
143
|
-
return
|
|
132
|
+
return path3.join(dataDir, "profile_journal.jsonl");
|
|
144
133
|
}
|
|
145
134
|
var SNAPSHOT_FILE = "profile_snapshot.json";
|
|
146
135
|
var ARCHIVE_PREFIX = "profile_journal.archive-";
|
|
147
136
|
function snapshotFilePath(dataDir) {
|
|
148
|
-
return
|
|
137
|
+
return path3.join(dataDir, SNAPSHOT_FILE);
|
|
149
138
|
}
|
|
150
139
|
function loadSnapshot(guard, dataDir) {
|
|
151
140
|
const snap = loadJson(guard, snapshotFilePath(dataDir));
|
|
@@ -154,7 +143,7 @@ function loadSnapshot(guard, dataDir) {
|
|
|
154
143
|
}
|
|
155
144
|
function listArchiveFiles(guard, dataDir) {
|
|
156
145
|
try {
|
|
157
|
-
return
|
|
146
|
+
return fs3.readdirSync(guard.assert(dataDir)).filter((f) => f.startsWith(ARCHIVE_PREFIX) && f.endsWith(".jsonl")).sort();
|
|
158
147
|
} catch {
|
|
159
148
|
return [];
|
|
160
149
|
}
|
|
@@ -185,7 +174,7 @@ function replayJournal(guard, dataDir) {
|
|
|
185
174
|
const stats = { records: snap?.recordsFolded ?? 0, truncatedTail: 0, isLive: false };
|
|
186
175
|
for (const file of listArchiveFiles(guard, dataDir)) {
|
|
187
176
|
try {
|
|
188
|
-
replayText(doc,
|
|
177
|
+
replayText(doc, fs3.readFileSync(path3.join(dataDir, file), "utf8"), baselineTs, { records: 0, truncatedTail: 0, isLive: false });
|
|
189
178
|
} catch {
|
|
190
179
|
}
|
|
191
180
|
}
|
|
@@ -193,6 +182,17 @@ function replayJournal(guard, dataDir) {
|
|
|
193
182
|
replayText(doc, readText(guard, journalFilePath(dataDir), ""), baselineTs, stats);
|
|
194
183
|
return { doc, truncatedTail: stats.truncatedTail, records: stats.records };
|
|
195
184
|
}
|
|
185
|
+
function readJournalTexts(guard, dataDir) {
|
|
186
|
+
const texts = [];
|
|
187
|
+
for (const file of listArchiveFiles(guard, dataDir)) {
|
|
188
|
+
try {
|
|
189
|
+
texts.push(fs3.readFileSync(path3.join(dataDir, file), "utf8"));
|
|
190
|
+
} catch {
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
texts.push(readText(guard, journalFilePath(dataDir), ""));
|
|
194
|
+
return texts;
|
|
195
|
+
}
|
|
196
196
|
function loadProfile(guard, file) {
|
|
197
197
|
const doc = loadJson(guard, file);
|
|
198
198
|
if (!doc || !doc.partitions) return emptyProfile();
|
|
@@ -208,9 +208,9 @@ function parseIso(v) {
|
|
|
208
208
|
function refExists(guard, dataDir, ref) {
|
|
209
209
|
const base = ref.split("#")[0] ?? "";
|
|
210
210
|
if (!base) return false;
|
|
211
|
-
const target =
|
|
211
|
+
const target = path3.join(dataDir, base);
|
|
212
212
|
try {
|
|
213
|
-
return
|
|
213
|
+
return fs3.existsSync(guard.assert(target));
|
|
214
214
|
} catch {
|
|
215
215
|
return false;
|
|
216
216
|
}
|
|
@@ -240,10 +240,19 @@ function applyOpsToDoc(guard, dataDir, doc, ops, schema, policy, now) {
|
|
|
240
240
|
rejected.push({ op, reason: "psy partition is disabled" });
|
|
241
241
|
continue;
|
|
242
242
|
}
|
|
243
|
-
|
|
244
|
-
if (
|
|
245
|
-
|
|
243
|
+
let temporal = "stable";
|
|
244
|
+
if (schema) {
|
|
245
|
+
const check = checkAddAgainstSchema(schema, op.partition, op.topic, op.subTopic, op.temporal);
|
|
246
|
+
if (!check.ok) {
|
|
247
|
+
rejected.push({ op, reason: check.reason });
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
temporal = check.temporal;
|
|
251
|
+
} else if (!(op.partition in doc.partitions)) {
|
|
252
|
+
rejected.push({ op, reason: `unknown partition: ${op.partition}` });
|
|
246
253
|
continue;
|
|
254
|
+
} else {
|
|
255
|
+
temporal = op.temporal === "volatile" ? "volatile" : "stable";
|
|
247
256
|
}
|
|
248
257
|
if (!op.evidence || op.evidence.length === 0) {
|
|
249
258
|
rejected.push({ op, reason: "ADD without evidence (no provenance, axiom 1)" });
|
|
@@ -262,13 +271,13 @@ function applyOpsToDoc(guard, dataDir, doc, ops, schema, policy, now) {
|
|
|
262
271
|
}
|
|
263
272
|
dbSeq += 1;
|
|
264
273
|
const entry = {
|
|
265
|
-
id: `p${dbSeq.toString(36)}${
|
|
274
|
+
id: `p${dbSeq.toString(36)}${randomUUID().slice(0, 4)}`,
|
|
266
275
|
partition: op.partition,
|
|
267
276
|
topic: op.topic,
|
|
268
277
|
subTopic: op.subTopic,
|
|
269
278
|
content: op.content.trim(),
|
|
270
279
|
confidence: Math.min(op.confidence ?? cap, cap),
|
|
271
|
-
temporal
|
|
280
|
+
temporal,
|
|
272
281
|
validFrom: nowIso,
|
|
273
282
|
validTo: null,
|
|
274
283
|
supersededBy: null,
|
|
@@ -357,17 +366,17 @@ function persistWithJournal(guard, dataDir, doc, record) {
|
|
|
357
366
|
const line = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...record });
|
|
358
367
|
const journal = journalFilePath(dataDir);
|
|
359
368
|
try {
|
|
360
|
-
|
|
369
|
+
fs3.appendFileSync(guard.assert(journal), line + "\n", "utf8");
|
|
361
370
|
} catch {
|
|
362
|
-
|
|
363
|
-
|
|
371
|
+
fs3.mkdirSync(dataDir, { recursive: true });
|
|
372
|
+
fs3.appendFileSync(guard.assert(journal), line + "\n", "utf8");
|
|
364
373
|
}
|
|
365
374
|
}
|
|
366
375
|
function applyOpPermissive(doc, op, ts) {
|
|
367
376
|
if (op.op === "ADD") {
|
|
368
377
|
dbSeq += 1;
|
|
369
378
|
doc.partitions[op.partition].entries.push({
|
|
370
|
-
id: op.assignedId ?? `r${dbSeq.toString(36)}${
|
|
379
|
+
id: op.assignedId ?? `r${dbSeq.toString(36)}${randomUUID().slice(0, 4)}`,
|
|
371
380
|
partition: op.partition,
|
|
372
381
|
topic: op.topic,
|
|
373
382
|
subTopic: op.subTopic,
|
|
@@ -442,7 +451,7 @@ function rebuildProfile(guard, dataDir, opts = {}) {
|
|
|
442
451
|
if (replayed.truncatedTail > 0) {
|
|
443
452
|
writeText(
|
|
444
453
|
guard,
|
|
445
|
-
|
|
454
|
+
path3.join(dataDir, "logs", "rebuild-report.txt"),
|
|
446
455
|
`rebuild truncated ${replayed.truncatedTail} torn line(s) at journal tail; ${replayed.records} records applied
|
|
447
456
|
`
|
|
448
457
|
);
|
|
@@ -451,8 +460,6 @@ function rebuildProfile(guard, dataDir, opts = {}) {
|
|
|
451
460
|
}
|
|
452
461
|
|
|
453
462
|
export {
|
|
454
|
-
atomicWriteJsonSync,
|
|
455
|
-
shredFileSync,
|
|
456
463
|
appendAuditLine,
|
|
457
464
|
readAuditLines,
|
|
458
465
|
pruneAuditFile,
|
|
@@ -462,6 +469,7 @@ export {
|
|
|
462
469
|
ARCHIVE_PREFIX,
|
|
463
470
|
snapshotFilePath,
|
|
464
471
|
replayJournal,
|
|
472
|
+
readJournalTexts,
|
|
465
473
|
loadProfile,
|
|
466
474
|
applyOpsToDoc,
|
|
467
475
|
runDeterministicAging,
|
|
@@ -469,4 +477,4 @@ export {
|
|
|
469
477
|
verifyProfile,
|
|
470
478
|
rebuildProfile
|
|
471
479
|
};
|
|
472
|
-
//# sourceMappingURL=chunk-
|
|
480
|
+
//# sourceMappingURL=chunk-OYVNWB5G.js.map
|