@kanadego/dsh-heartbeat 1.8.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 +50 -69
- package/assets/frontwin.ps1 +1 -0
- package/assets/idle.ps1 +1 -0
- package/assets/screenpulse.ps1 +4 -2
- package/config/policy.json +73 -72
- package/config/profile-schema.json +306 -306
- package/dist/{bindings-XPPSKILN.js → bindings-225SI7KC.js} +3 -2
- package/dist/{chunk-UCJR6TC7.js → chunk-2NB4QILF.js} +73 -21
- 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-MXJPX3FM.js → chunk-KOOQOMQY.js} +117 -62
- 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 -491
- 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 +482 -2418
- package/dist/index.js.map +1 -1
- package/dist/{migrate-43PUK25G.js → migrate-WDVFHUP7.js} +4 -3
- package/dist/orchestrator-2ZLU7RGE.js +60 -0
- package/dist/{pool-JQC5JWIZ.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-63FUJLIF.js.map +0 -1
- package/dist/chunk-IFTFDHZX.js.map +0 -1
- package/dist/chunk-J6ZTRFFW.js.map +0 -1
- package/dist/chunk-MXJPX3FM.js.map +0 -1
- package/dist/chunk-SQJLKP7L.js +0 -729
- package/dist/chunk-SQJLKP7L.js.map +0 -1
- package/dist/chunk-UCJR6TC7.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-43PUK25G.js.map → migrate-WDVFHUP7.js.map} +0 -0
- /package/dist/{pool-JQC5JWIZ.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";
|
|
@@ -20,6 +23,14 @@ var SEED_SOURCE_DEFAULT_CONFIDENCE = {
|
|
|
20
23
|
|
|
21
24
|
// src/seeds/pool.ts
|
|
22
25
|
var DAY_MS = 864e5;
|
|
26
|
+
var NO_AUDIT = () => {
|
|
27
|
+
};
|
|
28
|
+
function emit(audit, entry) {
|
|
29
|
+
try {
|
|
30
|
+
audit(entry);
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
}
|
|
23
34
|
var SEED_CATEGORY_CAPS = { topic: 16, chat: 14 };
|
|
24
35
|
var CATEGORY_VALUES = ["topic", "chat"];
|
|
25
36
|
function normalizeCategory(v) {
|
|
@@ -61,6 +72,9 @@ function savePool(guard, file, db) {
|
|
|
61
72
|
const body = db.seeds.map((s) => JSON.stringify(s)).join("\n");
|
|
62
73
|
saveEncryptedText(guard, file, body ? body + "\n" : "");
|
|
63
74
|
}
|
|
75
|
+
function update(guard, file, fn) {
|
|
76
|
+
return withFileLock(file, () => fn(loadPool(guard, file)));
|
|
77
|
+
}
|
|
64
78
|
var activeSeeds = (db) => db.seeds.filter((s) => s.status === "active");
|
|
65
79
|
var archivedSeeds = (db) => db.seeds.filter((s) => s.status === "archived");
|
|
66
80
|
function evictionScore(seed, policy, now) {
|
|
@@ -95,7 +109,14 @@ function archiveSeed(seed, reason, now) {
|
|
|
95
109
|
function retireLimit(s, policy) {
|
|
96
110
|
return normalizeCategory(s.category) === "chat" ? 1 : policy.seeds.retireAfterUsed;
|
|
97
111
|
}
|
|
98
|
-
function
|
|
112
|
+
function isConsumed(s, policy, prevUsedAt, sinceEvidence) {
|
|
113
|
+
if (s.used < retireLimit(s, policy)) return false;
|
|
114
|
+
return !(prevUsedAt > 0 && sinceEvidence > prevUsedAt);
|
|
115
|
+
}
|
|
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) {
|
|
99
120
|
const db = loadPool(guard, file);
|
|
100
121
|
const text = input.text.trim();
|
|
101
122
|
if (!text) throw new Error("seed text must not be empty");
|
|
@@ -124,16 +145,20 @@ function addSeed(guard, file, policy, input, now = Date.now()) {
|
|
|
124
145
|
extra.status = "archived";
|
|
125
146
|
extra.retireReason = "completed";
|
|
126
147
|
extra.retiredAt = nowIso;
|
|
148
|
+
emit(audit, { event: "seed_retired", id: extra.id, reason: "completed", via: "merge", into: kept.id });
|
|
127
149
|
}
|
|
128
150
|
seed = kept;
|
|
129
151
|
if (activeSeeds(db).length > policy.seeds.maxActive) {
|
|
130
152
|
const victim = pickEvictionVictim(db, policy, now, normalizeCategory(kept.category));
|
|
131
153
|
if (victim && victim.id !== kept.id) {
|
|
132
154
|
archiveSeed(victim, "pool_cap", now);
|
|
155
|
+
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 });
|
|
156
|
+
emit(audit, { event: "seed_retired", id: victim.id, reason: "pool_cap", via: "merge", into: kept.id });
|
|
133
157
|
savePool(guard, file, db);
|
|
134
158
|
return { kind: "merged", seed: kept, evicted: victim };
|
|
135
159
|
}
|
|
136
160
|
}
|
|
161
|
+
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 });
|
|
137
162
|
savePool(guard, file, db);
|
|
138
163
|
return { kind: "merged", seed: kept };
|
|
139
164
|
}
|
|
@@ -171,73 +196,103 @@ function addSeed(guard, file, policy, input, now = Date.now()) {
|
|
|
171
196
|
status: "active"
|
|
172
197
|
};
|
|
173
198
|
db.seeds.push(seed);
|
|
199
|
+
emit(audit, { event: "seed_added", id: seed.id, kind: "added", tag: seed.tag, source: seed.source, category: seed.category, confidence: seed.confidence });
|
|
200
|
+
if (evicted) emit(audit, { event: "seed_retired", id: evicted.id, reason: "pool_cap", via: "add", into: seed.id });
|
|
174
201
|
savePool(guard, file, db);
|
|
175
202
|
return { kind: "added", seed, evicted };
|
|
176
203
|
}
|
|
177
|
-
function gcPool(guard, file, policy, now = Date.now()) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
204
|
+
function gcPool(guard, file, policy, now = Date.now(), audit = NO_AUDIT) {
|
|
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
|
+
}
|
|
193
224
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
+
});
|
|
198
237
|
}
|
|
199
|
-
function surfaceSeed(guard, file, policy, id, now = Date.now()) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
238
|
+
function surfaceSeed(guard, file, policy, id, now = Date.now(), audit = NO_AUDIT) {
|
|
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
|
+
});
|
|
210
252
|
}
|
|
211
|
-
function archiveSeedById(guard, file, id, reason = "completed", now = Date.now()) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
253
|
+
function archiveSeedById(guard, file, id, reason = "completed", now = Date.now(), audit = NO_AUDIT) {
|
|
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
|
+
});
|
|
218
262
|
}
|
|
219
|
-
function restoreSeed(guard, file, policy, id, now = Date.now()) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
263
|
+
function restoreSeed(guard, file, policy, id, now = Date.now(), audit = NO_AUDIT) {
|
|
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
|
+
});
|
|
233
286
|
}
|
|
234
|
-
function deleteSeed(guard, file, id) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
287
|
+
function deleteSeed(guard, file, id, audit = NO_AUDIT) {
|
|
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
|
+
});
|
|
241
296
|
}
|
|
242
297
|
|
|
243
298
|
export {
|
|
@@ -259,4 +314,4 @@ export {
|
|
|
259
314
|
restoreSeed,
|
|
260
315
|
deleteSeed
|
|
261
316
|
};
|
|
262
|
-
//# 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":[]}
|