@elvisw/dsh-magic-context 0.0.0-stage → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/LICENSE +23 -0
  2. package/NOTICE +93 -0
  3. package/README.md +501 -2
  4. package/README.zh-CN.md +398 -0
  5. package/cordis.patch.yml +20 -0
  6. package/lib/client.js +2753 -0
  7. package/lib/core/compartments.js +424 -0
  8. package/lib/core/corpus-store.js +176 -0
  9. package/lib/core/directive-detect.js +147 -0
  10. package/lib/core/domain-reconcile.js +394 -0
  11. package/lib/core/domains.js +127 -0
  12. package/lib/core/embedding-local.js +247 -0
  13. package/lib/core/global-autostage.js +223 -0
  14. package/lib/core/global-domains.js +302 -0
  15. package/lib/core/journal.js +248 -0
  16. package/lib/core/llm-deadline.js +53 -0
  17. package/lib/core/machine-domain.js +106 -0
  18. package/lib/core/maintenance.js +380 -0
  19. package/lib/core/memory-bridge.js +818 -0
  20. package/lib/core/memory-categories.js +84 -0
  21. package/lib/core/memory-scoring.js +65 -0
  22. package/lib/core/memory-search.js +414 -0
  23. package/lib/core/memory-write.js +1176 -0
  24. package/lib/core/policy.js +111 -0
  25. package/lib/core/profile-confirmation.js +55 -0
  26. package/lib/core/profile-review.js +375 -0
  27. package/lib/core/profile-write.js +283 -0
  28. package/lib/core/reference-seeds.js +63 -0
  29. package/lib/core/route-health.js +106 -0
  30. package/lib/core/stores.js +1021 -0
  31. package/lib/core/tagger.js +7 -0
  32. package/lib/core/types.js +15 -0
  33. package/lib/core/vector-backfill.js +140 -0
  34. package/lib/core/vector-lane.js +184 -0
  35. package/lib/dsh/command-surface.js +376 -0
  36. package/lib/dsh/compaction-engine.js +231 -0
  37. package/lib/dsh/config-switches.js +56 -0
  38. package/lib/dsh/config.js +13 -0
  39. package/lib/dsh/context-pressure.js +36 -0
  40. package/lib/dsh/drop-landing.js +42 -0
  41. package/lib/dsh/emergency.js +89 -0
  42. package/lib/dsh/flush-command.js +148 -0
  43. package/lib/dsh/global-review.js +412 -0
  44. package/lib/dsh/historian.js +907 -0
  45. package/lib/dsh/live-journal.js +81 -0
  46. package/lib/dsh/maintenance-wiring.js +229 -0
  47. package/lib/dsh/management-pass.js +564 -0
  48. package/lib/dsh/memory-lane.js +535 -0
  49. package/lib/dsh/pre-step.js +675 -0
  50. package/lib/dsh/project-drift.js +116 -0
  51. package/lib/dsh/recomp.js +192 -0
  52. package/lib/dsh/reduce-guidance.js +188 -0
  53. package/lib/dsh/seams.js +588 -0
  54. package/lib/dsh/search-hint.js +186 -0
  55. package/lib/dsh/session-notice.js +100 -0
  56. package/lib/dsh/status-command.js +75 -0
  57. package/lib/dsh/telemetry.js +349 -0
  58. package/lib/dsh/tool-bridge.js +1009 -0
  59. package/lib/dsh/volatile.js +89 -0
  60. package/lib/index.js +931 -0
  61. package/package.json +108 -4
  62. package/tools/migrate-profile-package-name.mjs +317 -0
  63. package/vendor/onnx/LICENSE +27 -0
  64. package/vendor/onnx/ort-wasm-simd-threaded.mjs +59 -0
  65. package/vendor/onnx/ort-wasm-simd-threaded.wasm +0 -0
  66. package/vendor/onnx/ort.node.min.mjs +8 -0
@@ -0,0 +1,1021 @@
1
+ /**
2
+ * The adapter's own SQLite stores, on `node:sqlite` (no native addons: the
3
+ * DSH desktop host is an Electron RunAsNode process where ABI-compiled
4
+ * modules are not portable; node:sqlite is built in on Node >= 22.19).
5
+ *
6
+ * Schema discipline mirrors Magic Context's storage layer: a version fence
7
+ * that fails closed when the database is newer than this build knows, and a
8
+ * forward-only migration chain. (The Phase 1+ plan of moving these rows into
9
+ * the shared context.db was superseded 2026-10-13: the corpus became the
10
+ * plugin's own memories.db, and this store stays the adapter's runtime side.)
11
+ */
12
+ import { DatabaseSync } from 'node:sqlite';
13
+ import { mkdirSync } from 'node:fs';
14
+ import { dirname, join } from 'node:path';
15
+ import { homedir } from 'node:os';
16
+ import { parseShareCategories, serializeShareCategories } from "./domains.js";
17
+ export const SCHEMA_VERSION = 11;
18
+ export function defaultDataDir() {
19
+ return join(homedir(), '.local', 'share', 'cortexkit', 'magic-context-dsh-poc');
20
+ }
21
+ const MIGRATIONS = [
22
+ {
23
+ version: 1,
24
+ up: (db) => {
25
+ db.exec(`
26
+ CREATE TABLE meta(
27
+ key TEXT PRIMARY KEY,
28
+ value TEXT NOT NULL
29
+ );
30
+ CREATE TABLE tags(
31
+ session_id TEXT NOT NULL,
32
+ target_kind TEXT NOT NULL,
33
+ target_id TEXT NOT NULL,
34
+ tag_number INTEGER NOT NULL,
35
+ created_at INTEGER NOT NULL,
36
+ PRIMARY KEY(session_id, target_kind, target_id)
37
+ );
38
+ CREATE TABLE tag_counters(
39
+ session_id TEXT PRIMARY KEY,
40
+ next_tag INTEGER NOT NULL
41
+ );
42
+ CREATE TABLE usage_records(
43
+ session_id TEXT NOT NULL,
44
+ seq INTEGER NOT NULL,
45
+ turn INTEGER NOT NULL,
46
+ step INTEGER NOT NULL,
47
+ provider TEXT NOT NULL,
48
+ model TEXT NOT NULL,
49
+ input_tokens INTEGER,
50
+ output_tokens INTEGER,
51
+ cache_read_tokens INTEGER,
52
+ cache_write_tokens INTEGER,
53
+ recorded_at INTEGER NOT NULL,
54
+ PRIMARY KEY(session_id, seq)
55
+ );
56
+ `);
57
+ },
58
+ },
59
+ {
60
+ // v2: the agent-facing drop queue (ctx_reduce marks, the execute band
61
+ // lands) — Magic Context's pending_ops vocabulary on the DSH adapter.
62
+ version: 2,
63
+ up: (db) => {
64
+ db.exec(`
65
+ CREATE TABLE pending_drops(
66
+ session_id TEXT NOT NULL,
67
+ tag_number INTEGER NOT NULL,
68
+ queued_at INTEGER NOT NULL,
69
+ landed_event_seq INTEGER,
70
+ PRIMARY KEY(session_id, tag_number)
71
+ );
72
+ `);
73
+ },
74
+ },
75
+ {
76
+ // v3: the historian's compartments and the m[0]/m[1] baseline identity.
77
+ // A compartment is a durable digest of a covered surface span; it lands
78
+ // as a `[compacted §C<n>§]` placeholder on the next execute band, and its
79
+ // text is injected through the composed baseline node.
80
+ version: 3,
81
+ up: (db) => {
82
+ db.exec(`
83
+ CREATE TABLE compartments(
84
+ session_id TEXT NOT NULL,
85
+ compartment_id INTEGER NOT NULL,
86
+ created_at INTEGER NOT NULL,
87
+ covered_start_seq INTEGER NOT NULL,
88
+ covered_end_seq INTEGER NOT NULL,
89
+ covered_seqs TEXT NOT NULL,
90
+ covered_tokens INTEGER NOT NULL,
91
+ importance INTEGER NOT NULL,
92
+ tier_p1 TEXT NOT NULL,
93
+ tier_p2 TEXT NOT NULL,
94
+ tier_p3 TEXT NOT NULL,
95
+ tier_p4 TEXT NOT NULL,
96
+ facts TEXT NOT NULL,
97
+ events TEXT NOT NULL,
98
+ baseline_epoch INTEGER NOT NULL,
99
+ landed_event_seq INTEGER,
100
+ PRIMARY KEY(session_id, compartment_id)
101
+ );
102
+ CREATE TABLE compartment_counters(
103
+ session_id TEXT PRIMARY KEY,
104
+ next_compartment INTEGER NOT NULL
105
+ );
106
+ CREATE TABLE baseline_state(
107
+ session_id TEXT PRIMARY KEY,
108
+ epoch INTEGER NOT NULL,
109
+ node_seq INTEGER,
110
+ provider TEXT,
111
+ model TEXT,
112
+ frozen_at INTEGER NOT NULL,
113
+ memories_digest TEXT,
114
+ updated_at INTEGER NOT NULL
115
+ );
116
+ `);
117
+ },
118
+ },
119
+ {
120
+ // v4: the activity journal — the validation-period audit trail. The
121
+ // harness logger is not persisted by the Desktop app, so decisions and
122
+ // outcomes get their own append-only table (see core/journal.ts).
123
+ version: 4,
124
+ up: (db) => {
125
+ db.exec(`
126
+ CREATE TABLE journal(
127
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
128
+ at INTEGER NOT NULL,
129
+ level TEXT NOT NULL,
130
+ kind TEXT NOT NULL,
131
+ session_id TEXT,
132
+ summary TEXT NOT NULL,
133
+ data TEXT
134
+ );
135
+ CREATE INDEX journal_at ON journal(at);
136
+ CREATE INDEX journal_kind ON journal(kind);
137
+ CREATE INDEX journal_session ON journal(session_id, at);
138
+ `);
139
+ },
140
+ },
141
+ {
142
+ // v5: the additive-memory watermark (Magic Context's `maxMemoryId`), so a
143
+ // newly written memory surfaces in m[1] `<new-memories>` instead of
144
+ // changing the recall digest and forcing a HARD rebuild of m[0].
145
+ version: 5,
146
+ up: (db) => {
147
+ db.exec('ALTER TABLE baseline_state ADD COLUMN max_memory_id INTEGER');
148
+ },
149
+ },
150
+ {
151
+ // v6: the m[1] *mutation* delta lane. `max_memory_mutation_id` is Magic
152
+ // Context's `cached_m0_max_memory_mutation_id` (watermark into the shared
153
+ // `memory_mutation_log`); `project_epoch_fingerprint` is the
154
+ // `project_state.project_memory_epoch` identity the baseline was built
155
+ // against. Both are additive columns: an existing v5 database is upgraded
156
+ // in place, and rows written before this version simply have NULL there
157
+ // (the pass folds them once, see management-pass.ts).
158
+ version: 6,
159
+ up: (db) => {
160
+ db.exec(`
161
+ ALTER TABLE baseline_state ADD COLUMN max_memory_mutation_id INTEGER;
162
+ ALTER TABLE baseline_state ADD COLUMN project_epoch_fingerprint TEXT;
163
+ `);
164
+ },
165
+ },
166
+ {
167
+ // v7: the fusion additions (borrowed from mon3stera/dsh-magic-context
168
+ // @0.1.0, audited 2026-10-09; see docs/ for the fusion plan).
169
+ // - compartments.landed_reason: a TERMINAL non-publish outcome.
170
+ // `not-shrinkable`: the publish guard proved the digest does not pay
171
+ // for its own baseline render. `superseded-entrants`: substantive
172
+ // nodes entered the positional range after drafting. A reasoned row
173
+ // is never published and never retried (listUnlandedCompartments
174
+ // excludes it), ending the old rejected-draft-retried-forever shape.
175
+ // - compartments.superseded: retires a drifted draft AND RELEASES its
176
+ // covered-seq claims, so the next historian run re-digests the whole
177
+ // span (originals plus the entrants) instead of leaking it.
178
+ // NOTE (2026-10-16): the publish guard that called these two was
179
+ // RETIRED (0 hits in its whole window); the column and both store APIs
180
+ // remain for schema-v7 compatibility, historical rows and tests.
181
+ // - memory_stats: the S(t) scoring sidecar. Hits/injections for rows of
182
+ // the SHARED memory store are tracked HERE, adapter-local — the
183
+ // shared store is never written for scoring (writer-contract
184
+ // discipline: only the bridge's contract actions touch it).
185
+ version: 7,
186
+ up: (db) => {
187
+ db.exec(`
188
+ ALTER TABLE compartments ADD COLUMN landed_reason TEXT;
189
+ ALTER TABLE compartments ADD COLUMN superseded INTEGER NOT NULL DEFAULT 0;
190
+ CREATE TABLE memory_stats(
191
+ project_identity TEXT NOT NULL,
192
+ memory_id INTEGER NOT NULL,
193
+ hits INTEGER NOT NULL DEFAULT 0,
194
+ last_hit_at INTEGER,
195
+ injections INTEGER NOT NULL DEFAULT 0,
196
+ last_injected_at INTEGER,
197
+ local_archived INTEGER NOT NULL DEFAULT 0,
198
+ updated_at INTEGER NOT NULL,
199
+ PRIMARY KEY(project_identity, memory_id)
200
+ );
201
+ CREATE INDEX memory_stats_archived ON memory_stats(project_identity, local_archived);
202
+ `);
203
+ },
204
+ },
205
+ {
206
+ // v8: the global-domain substrate (docs/dsh-plugin-execution-plan-2026-10-09.md,
207
+ // WP1). Magic Context, its dashboard and every other host are being removed,
208
+ // so the adapter becomes the sole owner of: which global domains exist, which
209
+ // project identities activate them, the cross-project candidate pool for
210
+ // org/machine, and the confirmation queue that directive-shaped profile
211
+ // candidates must pass through. All of it lives HERE (the adapter's own
212
+ // store) — the own corpus (memories.db) deliberately carries none of it.
213
+ //
214
+ // - domains: one row per global domain. `cap` is DERIVED from a token
215
+ // target by the caller, so the admission limit and the budget cannot
216
+ // drift apart.
217
+ // - domain_members: the activation rule (the only sharing membership
218
+ // since the 2026-10-13 corpus cutover).
219
+ // - global_candidates / global_candidate_observations: the cross-project
220
+ // recurrence gate — a fact promoted to a global domain must have been
221
+ // observed independently by at least two DIFFERENT project identities.
222
+ // - profile_confirmations: the human gate for directive-shaped content.
223
+ // Unconfirmed entries never enter the injected pool.
224
+ // - baseline_state.profile_watermark / profile_m0_digest: the additive
225
+ // watermark and the m[0] content digest for the profile, so a new
226
+ // profile row rides the m[1] delta while a rewritten or retired one
227
+ // forces a HARD rebuild (deliberately unlike Magic Context, which kept
228
+ // serving a stale m[0] profile block).
229
+ version: 8,
230
+ up: (db) => {
231
+ db.exec(`
232
+ CREATE TABLE domains(
233
+ domain_id TEXT PRIMARY KEY,
234
+ kind TEXT NOT NULL CHECK(kind IN ('user','org','machine')),
235
+ enabled INTEGER NOT NULL DEFAULT 0,
236
+ budget_tokens INTEGER NOT NULL,
237
+ cap INTEGER NOT NULL,
238
+ share_categories TEXT NOT NULL DEFAULT '["CONSTRAINTS"]',
239
+ created_at INTEGER NOT NULL,
240
+ updated_at INTEGER NOT NULL
241
+ );
242
+ CREATE TABLE domain_members(
243
+ domain_id TEXT NOT NULL REFERENCES domains(domain_id) ON DELETE CASCADE,
244
+ identity TEXT NOT NULL,
245
+ added_at INTEGER NOT NULL,
246
+ PRIMARY KEY(domain_id, identity)
247
+ );
248
+ CREATE INDEX domain_members_identity ON domain_members(identity);
249
+ CREATE TABLE global_candidates(
250
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
251
+ domain_id TEXT NOT NULL,
252
+ content TEXT NOT NULL,
253
+ normalized_hash TEXT NOT NULL,
254
+ first_identity TEXT NOT NULL,
255
+ session_id TEXT,
256
+ compartment_start INTEGER,
257
+ compartment_end INTEGER,
258
+ distinct_identities INTEGER NOT NULL DEFAULT 1,
259
+ observation_count INTEGER NOT NULL DEFAULT 1,
260
+ state TEXT NOT NULL DEFAULT 'pending' CHECK(state IN ('pending','approved','rejected','expired')),
261
+ created_at INTEGER NOT NULL,
262
+ updated_at INTEGER NOT NULL
263
+ );
264
+ CREATE UNIQUE INDEX global_candidates_domain_hash ON global_candidates(domain_id, normalized_hash);
265
+ CREATE INDEX global_candidates_state ON global_candidates(domain_id, state);
266
+ CREATE TABLE global_candidate_observations(
267
+ candidate_id INTEGER NOT NULL REFERENCES global_candidates(id) ON DELETE CASCADE,
268
+ identity TEXT NOT NULL,
269
+ session_id TEXT NOT NULL,
270
+ compartment_start INTEGER,
271
+ compartment_end INTEGER,
272
+ observations INTEGER NOT NULL DEFAULT 1,
273
+ first_seen_at INTEGER NOT NULL,
274
+ last_seen_at INTEGER NOT NULL,
275
+ PRIMARY KEY(candidate_id, identity, session_id)
276
+ );
277
+ CREATE INDEX global_observations_identity ON global_candidate_observations(identity);
278
+ CREATE TABLE profile_confirmations(
279
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
280
+ contract_candidate_id INTEGER NOT NULL,
281
+ question TEXT NOT NULL,
282
+ content TEXT NOT NULL,
283
+ state TEXT NOT NULL DEFAULT 'pending' CHECK(state IN ('pending','approved','rejected')),
284
+ created_at INTEGER NOT NULL,
285
+ decided_at INTEGER
286
+ );
287
+ CREATE INDEX profile_confirmations_state ON profile_confirmations(state);
288
+ ALTER TABLE baseline_state ADD COLUMN profile_watermark INTEGER;
289
+ ALTER TABLE baseline_state ADD COLUMN profile_m0_digest TEXT;
290
+ `);
291
+ },
292
+ },
293
+ {
294
+ // v9: what a global candidate will be published AS.
295
+ //
296
+ // v8 stored a candidate's content and hash but not its category, because the
297
+ // only intended source was the historian's recurrence scan. Naming the
298
+ // category up front matters for two reasons: the category decides whether
299
+ // the row will be visible at all once published (per-domain allowlist), and
300
+ // `source_memory_id` keeps the provenance so a reviewer can see which
301
+ // project memory a candidate came from. Both are nullable: a candidate
302
+ // created by the recurrence scan has no single source memory, and a human
303
+ // promotion always has one.
304
+ version: 9,
305
+ up: (db) => {
306
+ db.exec(`
307
+ ALTER TABLE global_candidates ADD COLUMN category TEXT;
308
+ ALTER TABLE global_candidates ADD COLUMN source_memory_id INTEGER;
309
+ `);
310
+ },
311
+ },
312
+ {
313
+ // v10: the LLM domain classification that routes an auto-staged candidate
314
+ // into machine or org — and, when classification fails, leaves the memory
315
+ // at PROJECT level (nothing staged). `suggested_domain` and
316
+ // `suggestion_reason` ride the candidate row so the review card can show
317
+ // the classifier's verdict and the operator can overrule it (the card's
318
+ // "publish to another domain" option). Both nullable: a human promotion or
319
+ // a pre-v10 auto-stage has no suggestion.
320
+ version: 10,
321
+ up: (db) => {
322
+ db.exec(`
323
+ ALTER TABLE global_candidates ADD COLUMN suggested_domain TEXT;
324
+ ALTER TABLE global_candidates ADD COLUMN suggestion_reason TEXT;
325
+ `);
326
+ },
327
+ },
328
+ {
329
+ // v11: the manual `/ctx-flush` marker. It is deliberately on the queue
330
+ // row, not on a tag: only an already-pending drop can be forced, and the
331
+ // marker must survive a process restart until the next preStep.
332
+ version: 11,
333
+ up: (db) => {
334
+ db.exec(`
335
+ ALTER TABLE pending_drops ADD COLUMN flush_requested INTEGER NOT NULL DEFAULT 0;
336
+ `);
337
+ },
338
+ },
339
+ ];
340
+ export class StoreAheadOfAdapterError extends Error {
341
+ // Declared explicitly rather than as constructor parameter properties: the
342
+ // scripts in scripts/ load this module with plain Node (type stripping), and
343
+ // parameter properties are not erasable syntax. Keeping the class erasable
344
+ // lets migration tooling reuse the real store instead of re-implementing its
345
+ // SQL — one source of truth for the adapter schema.
346
+ dbVersion;
347
+ adapterVersion;
348
+ constructor(dbVersion, adapterVersion) {
349
+ super(`magic-context dsh adapter: store schema v${dbVersion} is newer than this build supports (v${adapterVersion}); `
350
+ + 'update the adapter (or roll it back together with its database) before continuing');
351
+ this.name = 'StoreAheadOfAdapterError';
352
+ this.dbVersion = dbVersion;
353
+ this.adapterVersion = adapterVersion;
354
+ }
355
+ }
356
+ function targetKey(target) {
357
+ return target.kind === 'user-message'
358
+ ? { kind: 'user-message', id: target.messageId }
359
+ : { kind: 'tool-result', id: target.callId };
360
+ }
361
+ export function openStores(dataDir) {
362
+ const dbPath = join(dataDir, 'dsh-magic-context.db');
363
+ mkdirSync(dirname(dbPath), { recursive: true });
364
+ const db = new DatabaseSync(dbPath);
365
+ db.exec('PRAGMA journal_mode = WAL');
366
+ let closed = false;
367
+ let persisted = 0;
368
+ const hasMetaTable = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'meta'").get();
369
+ if (hasMetaTable !== undefined) {
370
+ const row = db.prepare('SELECT value FROM meta WHERE key = ?').get('schema_version');
371
+ persisted = row === undefined ? 0 : Number(row.value);
372
+ if (!Number.isInteger(persisted) || persisted < 0) {
373
+ db.close();
374
+ throw new Error(`magic-context dsh adapter: invalid persisted schema version ${String(row?.value)}`);
375
+ }
376
+ }
377
+ if (persisted > SCHEMA_VERSION) {
378
+ db.close();
379
+ throw new StoreAheadOfAdapterError(persisted, SCHEMA_VERSION);
380
+ }
381
+ for (const migration of MIGRATIONS) {
382
+ if (migration.version <= persisted)
383
+ continue;
384
+ db.exec('BEGIN');
385
+ try {
386
+ migration.up(db);
387
+ db.prepare('INSERT INTO meta(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value')
388
+ .run('schema_version', String(migration.version));
389
+ db.exec('COMMIT');
390
+ }
391
+ catch (error) {
392
+ db.exec('ROLLBACK');
393
+ db.close();
394
+ throw error;
395
+ }
396
+ }
397
+ const allocateTag = (sessionId, target, now) => {
398
+ const { kind, id } = targetKey(target);
399
+ const existing = db
400
+ .prepare('SELECT tag_number FROM tags WHERE session_id = ? AND target_kind = ? AND target_id = ?')
401
+ .get(sessionId, kind, id);
402
+ if (existing !== undefined)
403
+ return existing.tag_number;
404
+ const insert = db.prepare('INSERT INTO tags(session_id, target_kind, target_id, tag_number, created_at) VALUES(?, ?, ?, ?, ?)');
405
+ const bump = db.prepare('INSERT INTO tag_counters(session_id, next_tag) VALUES(?, 2) ON CONFLICT(session_id) DO UPDATE SET next_tag = next_tag + 1 RETURNING next_tag - 1');
406
+ db.exec('BEGIN');
407
+ try {
408
+ const allocated = bump.get(sessionId);
409
+ const tagNumber = allocated === undefined ? 1 : allocated['next_tag - 1'];
410
+ insert.run(sessionId, kind, id, tagNumber, now);
411
+ db.exec('COMMIT');
412
+ return tagNumber;
413
+ }
414
+ catch (error) {
415
+ db.exec('ROLLBACK');
416
+ throw error;
417
+ }
418
+ };
419
+ const getTag = (sessionId, target) => {
420
+ const { kind, id } = targetKey(target);
421
+ const row = db
422
+ .prepare('SELECT tag_number FROM tags WHERE session_id = ? AND target_kind = ? AND target_id = ?')
423
+ .get(sessionId, kind, id);
424
+ return row?.tag_number;
425
+ };
426
+ const usageColumns = 'session_id, seq, turn, step, provider, model, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, recorded_at';
427
+ const recordUsage = (record) => {
428
+ db.exec('BEGIN');
429
+ try {
430
+ db.prepare(`INSERT OR IGNORE INTO usage_records(${usageColumns}) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(record.sessionId, record.seq, record.turn, record.step, record.provider, record.model, record.inputTokens ?? null, record.outputTokens ?? null, record.cacheReadTokens ?? null, record.cacheWriteTokens ?? null, record.recordedAt);
431
+ db.exec('COMMIT');
432
+ }
433
+ catch (error) {
434
+ db.exec('ROLLBACK');
435
+ throw error;
436
+ }
437
+ };
438
+ const listUsage = (sessionId) => db.prepare(`SELECT * FROM usage_records WHERE session_id = ? ORDER BY seq`).all(sessionId).map(row => ({
439
+ sessionId: row.session_id,
440
+ seq: row.seq,
441
+ turn: row.turn,
442
+ step: row.step,
443
+ provider: row.provider,
444
+ model: row.model,
445
+ inputTokens: row.input_tokens ?? undefined,
446
+ outputTokens: row.output_tokens ?? undefined,
447
+ cacheReadTokens: row.cache_read_tokens ?? undefined,
448
+ cacheWriteTokens: row.cache_write_tokens ?? undefined,
449
+ recordedAt: row.recorded_at,
450
+ }));
451
+ const listTags = (sessionId) => db.prepare('SELECT * FROM tags WHERE session_id = ? ORDER BY tag_number').all(sessionId).map(row => ({
452
+ sessionId: row.session_id,
453
+ tagNumber: row.tag_number,
454
+ target: row.target_kind === 'user-message'
455
+ ? { kind: 'user-message', messageId: row.target_id }
456
+ : { kind: 'tool-result', callId: row.target_id },
457
+ createdAt: row.created_at,
458
+ }));
459
+ const latestUsage = (sessionId) => {
460
+ const row = db.prepare('SELECT * FROM usage_records WHERE session_id = ? ORDER BY seq DESC LIMIT 1').get(sessionId);
461
+ if (row === undefined)
462
+ return undefined;
463
+ return {
464
+ sessionId: row.session_id,
465
+ seq: row.seq,
466
+ turn: row.turn,
467
+ step: row.step,
468
+ provider: row.provider,
469
+ model: row.model,
470
+ inputTokens: row.input_tokens ?? undefined,
471
+ outputTokens: row.output_tokens ?? undefined,
472
+ cacheReadTokens: row.cache_read_tokens ?? undefined,
473
+ cacheWriteTokens: row.cache_write_tokens ?? undefined,
474
+ recordedAt: row.recorded_at,
475
+ };
476
+ };
477
+ /**
478
+ * Newest history change (compartment stored / baseline re-frozen) for the
479
+ * session. Both stamps are metadata about the SAME injected surface, so a
480
+ * reading older than either one describes bytes the provider will not see
481
+ * again.
482
+ *
483
+ * Deliberately not extended to drop landings: `pending_drops` carries no
484
+ * landing timestamp, and a drop-only mutation is already followed by the same
485
+ * pass's baseline refresh in every path that refreshes one. The compaction
486
+ * case — the one that loses content — is covered.
487
+ */
488
+ const latestHistoryChangeAt = (sessionId) => {
489
+ try {
490
+ const row = db.prepare('SELECT MAX(t) AS t FROM ('
491
+ + 'SELECT MAX(created_at) AS t FROM compartments WHERE session_id = ?'
492
+ + ' UNION ALL SELECT MAX(updated_at) AS t FROM baseline_state WHERE session_id = ?)').get(sessionId, sessionId);
493
+ const value = row?.t;
494
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
495
+ }
496
+ catch {
497
+ // A freshness probe must never fail the turn that asked for it.
498
+ return undefined;
499
+ }
500
+ };
501
+ const queueDrop = (sessionId, tagNumber, now) => {
502
+ db.prepare('INSERT INTO pending_drops(session_id, tag_number, queued_at) VALUES(?, ?, ?) '
503
+ + 'ON CONFLICT(session_id, tag_number) DO UPDATE SET queued_at = excluded.queued_at, flush_requested = 0 '
504
+ + 'WHERE landed_event_seq IS NOT NULL').run(sessionId, tagNumber, now);
505
+ };
506
+ const listPendingDrops = (sessionId) => db.prepare('SELECT tag_number FROM pending_drops WHERE session_id = ? AND landed_event_seq IS NULL ORDER BY queued_at, tag_number').all(sessionId).map(row => row.tag_number);
507
+ const requestFlush = (sessionId) => {
508
+ const result = db.prepare('UPDATE pending_drops SET flush_requested = 1 '
509
+ + 'WHERE session_id = ? AND landed_event_seq IS NULL AND flush_requested = 0').run(sessionId);
510
+ return result.changes ?? 0;
511
+ };
512
+ const listFlushRequestedDrops = (sessionId) => db.prepare('SELECT tag_number FROM pending_drops WHERE session_id = ? AND landed_event_seq IS NULL AND flush_requested = 1 '
513
+ + 'ORDER BY queued_at, tag_number').all(sessionId).map(row => row.tag_number);
514
+ const clearFlushRequests = (sessionId, tags) => {
515
+ if (tags.length === 0)
516
+ return;
517
+ const clear = db.prepare('UPDATE pending_drops SET flush_requested = 0 WHERE session_id = ? AND tag_number = ?');
518
+ db.exec('BEGIN');
519
+ try {
520
+ for (const tag of tags)
521
+ clear.run(sessionId, tag);
522
+ db.exec('COMMIT');
523
+ }
524
+ catch (error) {
525
+ db.exec('ROLLBACK');
526
+ throw error;
527
+ }
528
+ };
529
+ const markDropLanded = (sessionId, tagNumber, landedEventSeq) => {
530
+ db.prepare('UPDATE pending_drops SET landed_event_seq = ?, flush_requested = 0 WHERE session_id = ? AND tag_number = ?').run(landedEventSeq, sessionId, tagNumber);
531
+ };
532
+ const dropState = (sessionId, tagNumber) => {
533
+ const row = db.prepare('SELECT landed_event_seq FROM pending_drops WHERE session_id = ? AND tag_number = ?').get(sessionId, tagNumber);
534
+ if (row === undefined)
535
+ return 'none';
536
+ return row.landed_event_seq === null ? 'pending' : 'landed';
537
+ };
538
+ const tagSessions = (tagNumber) => db.prepare('SELECT DISTINCT session_id FROM tags WHERE tag_number = ? ORDER BY session_id').all(tagNumber).map(row => row.session_id);
539
+ const toCompartment = (row) => ({
540
+ sessionId: row.session_id,
541
+ compartmentId: row.compartment_id,
542
+ createdAt: row.created_at,
543
+ coveredStartSeq: row.covered_start_seq,
544
+ coveredEndSeq: row.covered_end_seq,
545
+ coveredSeqs: JSON.parse(row.covered_seqs),
546
+ coveredTokens: row.covered_tokens,
547
+ importance: row.importance,
548
+ tiers: { p1: row.tier_p1, p2: row.tier_p2, p3: row.tier_p3, p4: row.tier_p4 },
549
+ facts: JSON.parse(row.facts),
550
+ events: JSON.parse(row.events),
551
+ baselineEpoch: row.baseline_epoch,
552
+ ...row.landed_event_seq === null ? {} : { landedEventSeq: row.landed_event_seq },
553
+ ...row.landed_reason === null ? {} : { landedReason: row.landed_reason },
554
+ });
555
+ const allocateCompartment = (sessionId, draft, covered, baselineEpoch, now) => {
556
+ db.exec('BEGIN');
557
+ try {
558
+ const bumped = db.prepare('INSERT INTO compartment_counters(session_id, next_compartment) VALUES(?, 2) '
559
+ + 'ON CONFLICT(session_id) DO UPDATE SET next_compartment = next_compartment + 1 '
560
+ + 'RETURNING next_compartment - 1').get(sessionId);
561
+ const compartmentId = bumped === undefined ? 1 : bumped['next_compartment - 1'];
562
+ db.prepare('INSERT INTO compartments(session_id, compartment_id, created_at, covered_start_seq, covered_end_seq, '
563
+ + 'covered_seqs, covered_tokens, importance, tier_p1, tier_p2, tier_p3, tier_p4, facts, events, baseline_epoch) '
564
+ + 'VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').run(sessionId, compartmentId, now, covered.startSeq, covered.endSeq, JSON.stringify([...covered.seqs]), covered.tokens, draft.importance, draft.tiers.p1, draft.tiers.p2, draft.tiers.p3, draft.tiers.p4, JSON.stringify([...draft.facts]), JSON.stringify([...draft.events]), baselineEpoch);
565
+ db.exec('COMMIT');
566
+ return {
567
+ sessionId,
568
+ compartmentId,
569
+ createdAt: now,
570
+ coveredStartSeq: covered.startSeq,
571
+ coveredEndSeq: covered.endSeq,
572
+ coveredSeqs: [...covered.seqs],
573
+ coveredTokens: covered.tokens,
574
+ importance: draft.importance,
575
+ tiers: draft.tiers,
576
+ facts: [...draft.facts],
577
+ events: [...draft.events],
578
+ baselineEpoch,
579
+ };
580
+ }
581
+ catch (error) {
582
+ db.exec('ROLLBACK');
583
+ throw error;
584
+ }
585
+ };
586
+ const listCompartments = (sessionId) => db.prepare('SELECT * FROM compartments WHERE session_id = ? ORDER BY compartment_id')
587
+ .all(sessionId).map(toCompartment);
588
+ const listUnlandedCompartments = (sessionId) => db.prepare('SELECT * FROM compartments WHERE session_id = ? AND landed_event_seq IS NULL '
589
+ + 'AND superseded = 0 AND landed_reason IS NULL ORDER BY compartment_id')
590
+ .all(sessionId).map(toCompartment);
591
+ const markCompartmentLanded = (sessionId, compartmentId, landedEventSeq) => {
592
+ db.prepare('UPDATE compartments SET landed_event_seq = ? WHERE session_id = ? AND compartment_id = ?').run(landedEventSeq, sessionId, compartmentId);
593
+ };
594
+ const markCompartmentRejected = (sessionId, compartmentId, reason) => {
595
+ db.prepare('UPDATE compartments SET landed_reason = ? WHERE session_id = ? AND compartment_id = ? '
596
+ + 'AND landed_event_seq IS NULL').run(reason, sessionId, compartmentId);
597
+ };
598
+ const supersedeCompartment = (sessionId, compartmentId, reason) => {
599
+ db.prepare('UPDATE compartments SET superseded = 1, landed_reason = ? WHERE session_id = ? AND compartment_id = ?').run(reason, sessionId, compartmentId);
600
+ };
601
+ const replaceCompartmentDraft = (sessionId, compartmentId, draft) => {
602
+ db.prepare('UPDATE compartments SET importance = ?, tier_p1 = ?, tier_p2 = ?, tier_p3 = ?, tier_p4 = ?, facts = ?, events = ? '
603
+ + 'WHERE session_id = ? AND compartment_id = ?').run(draft.importance, draft.tiers.p1, draft.tiers.p2, draft.tiers.p3, draft.tiers.p4, JSON.stringify([...draft.facts]), JSON.stringify([...draft.events]), sessionId, compartmentId);
604
+ };
605
+ const coveredSeqs = (sessionId) => {
606
+ // Superseded drafts release their claims: their span (originals plus the
607
+ // nodes that drifted in) goes back into the historian's eligibility pool.
608
+ const rows = db.prepare('SELECT covered_seqs FROM compartments WHERE session_id = ? AND superseded = 0')
609
+ .all(sessionId);
610
+ const seqs = new Set();
611
+ for (const row of rows) {
612
+ for (const seq of JSON.parse(row.covered_seqs))
613
+ seqs.add(seq);
614
+ }
615
+ return seqs;
616
+ };
617
+ const baselineState = (sessionId) => {
618
+ const row = db.prepare('SELECT * FROM baseline_state WHERE session_id = ?').get(sessionId);
619
+ if (row === undefined)
620
+ return undefined;
621
+ return {
622
+ sessionId: row.session_id,
623
+ epoch: row.epoch,
624
+ ...row.node_seq === null ? {} : { nodeSeq: row.node_seq },
625
+ ...row.provider === null ? {} : { provider: row.provider },
626
+ ...row.model === null ? {} : { model: row.model },
627
+ frozenAt: row.frozen_at,
628
+ ...row.memories_digest === null ? {} : { memoriesDigest: row.memories_digest },
629
+ ...row.max_memory_id === null ? {} : { maxMemoryId: row.max_memory_id },
630
+ ...row.max_memory_mutation_id === null ? {} : { maxMemoryMutationId: row.max_memory_mutation_id },
631
+ ...row.project_epoch_fingerprint === null ? {} : { projectEpochFingerprint: row.project_epoch_fingerprint },
632
+ ...row.profile_watermark === null ? {} : { profileWatermark: row.profile_watermark },
633
+ ...row.profile_m0_digest === null ? {} : { profileM0Digest: row.profile_m0_digest },
634
+ updatedAt: row.updated_at,
635
+ };
636
+ };
637
+ const saveBaselineState = (state) => {
638
+ db.prepare('INSERT INTO baseline_state(session_id, epoch, node_seq, provider, model, frozen_at, memories_digest, '
639
+ + 'max_memory_id, max_memory_mutation_id, project_epoch_fingerprint, profile_watermark, profile_m0_digest, updated_at) '
640
+ + 'VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) '
641
+ + 'ON CONFLICT(session_id) DO UPDATE SET epoch = excluded.epoch, node_seq = excluded.node_seq, '
642
+ + 'provider = excluded.provider, model = excluded.model, frozen_at = excluded.frozen_at, '
643
+ + 'memories_digest = excluded.memories_digest, max_memory_id = excluded.max_memory_id, '
644
+ + 'max_memory_mutation_id = excluded.max_memory_mutation_id, '
645
+ + 'project_epoch_fingerprint = excluded.project_epoch_fingerprint, '
646
+ + 'profile_watermark = excluded.profile_watermark, '
647
+ + 'profile_m0_digest = excluded.profile_m0_digest, '
648
+ + 'updated_at = excluded.updated_at').run(state.sessionId, state.epoch, state.nodeSeq ?? null, state.provider ?? null, state.model ?? null, state.frozenAt, state.memoriesDigest ?? null, state.maxMemoryId ?? null, state.maxMemoryMutationId ?? null, state.projectEpochFingerprint ?? null, state.profileWatermark ?? null, state.profileM0Digest ?? null, state.updatedAt);
649
+ };
650
+ const toDomain = (row) => ({
651
+ domainId: row.domain_id,
652
+ kind: row.kind,
653
+ enabled: row.enabled !== 0,
654
+ budgetTokens: row.budget_tokens,
655
+ cap: row.cap,
656
+ shareCategories: parseShareCategories(row.share_categories),
657
+ createdAt: row.created_at,
658
+ updatedAt: row.updated_at,
659
+ });
660
+ const upsertDomain = (domain, now) => {
661
+ // serializeShareCategories rejects an unknown category here, at the write,
662
+ // rather than letting a row become permanently invisible later.
663
+ const shareCategories = serializeShareCategories(domain.shareCategories);
664
+ db.prepare('INSERT INTO domains(domain_id, kind, enabled, budget_tokens, cap, share_categories, created_at, updated_at) '
665
+ + 'VALUES(?, ?, ?, ?, ?, ?, ?, ?) '
666
+ + 'ON CONFLICT(domain_id) DO UPDATE SET kind = excluded.kind, enabled = excluded.enabled, '
667
+ + 'budget_tokens = excluded.budget_tokens, cap = excluded.cap, '
668
+ + 'share_categories = excluded.share_categories, updated_at = excluded.updated_at').run(domain.domainId, domain.kind, domain.enabled ? 1 : 0, domain.budgetTokens, domain.cap, shareCategories, now, now);
669
+ };
670
+ const listDomains = () => db.prepare('SELECT * FROM domains ORDER BY kind, domain_id').all().map(toDomain);
671
+ const getDomain = (domainId) => {
672
+ const row = db.prepare('SELECT * FROM domains WHERE domain_id = ?').get(domainId);
673
+ return row === undefined ? undefined : toDomain(row);
674
+ };
675
+ const setDomainEnabled = (domainId, enabled, now) => {
676
+ db.prepare('UPDATE domains SET enabled = ?, updated_at = ? WHERE domain_id = ?').run(enabled ? 1 : 0, now, domainId);
677
+ };
678
+ const addDomainMember = (domainId, identity, now) => {
679
+ db.prepare('INSERT INTO domain_members(domain_id, identity, added_at) VALUES(?, ?, ?) '
680
+ + 'ON CONFLICT(domain_id, identity) DO NOTHING').run(domainId, identity, now);
681
+ };
682
+ const removeDomainMember = (domainId, identity) => {
683
+ db.prepare('DELETE FROM domain_members WHERE domain_id = ? AND identity = ?').run(domainId, identity);
684
+ };
685
+ const listDomainMembers = (domainId) => {
686
+ const rows = (domainId === undefined
687
+ ? db.prepare('SELECT * FROM domain_members ORDER BY domain_id, identity').all()
688
+ : db.prepare('SELECT * FROM domain_members WHERE domain_id = ? ORDER BY identity').all(domainId));
689
+ return rows.map(row => ({ domainId: row.domain_id, identity: row.identity, addedAt: row.added_at }));
690
+ };
691
+ const activeDomainIdentities = (identity) => {
692
+ const rows = db.prepare('SELECT d.domain_id AS domain_id, d.kind AS kind FROM domain_members m '
693
+ + 'JOIN domains d ON d.domain_id = m.domain_id '
694
+ + 'WHERE m.identity = ? AND d.enabled = 1 ORDER BY d.kind, d.domain_id').all(identity);
695
+ return rows.map(row => ({ domainId: row.domain_id, kind: row.kind, identity: row.domain_id }));
696
+ };
697
+ const toGlobalCandidate = (row) => ({
698
+ id: row.id,
699
+ domainId: row.domain_id,
700
+ content: row.content,
701
+ normalizedHash: row.normalized_hash,
702
+ firstIdentity: row.first_identity,
703
+ ...row.session_id === null ? {} : { sessionId: row.session_id },
704
+ ...row.compartment_start === null ? {} : { compartmentStart: row.compartment_start },
705
+ ...row.compartment_end === null ? {} : { compartmentEnd: row.compartment_end },
706
+ distinctIdentities: row.distinct_identities,
707
+ observationCount: row.observation_count,
708
+ ...row.category === null ? {} : { category: row.category },
709
+ ...row.source_memory_id === null ? {} : { sourceMemoryId: row.source_memory_id },
710
+ ...row.suggested_domain === null ? {} : { suggestedDomain: row.suggested_domain },
711
+ ...row.suggestion_reason === null ? {} : { suggestionReason: row.suggestion_reason },
712
+ state: row.state,
713
+ createdAt: row.created_at,
714
+ updatedAt: row.updated_at,
715
+ });
716
+ const getGlobalCandidateByHash = (domainId, normalizedHash) => {
717
+ const row = db
718
+ .prepare('SELECT * FROM global_candidates WHERE domain_id = ? AND normalized_hash = ?')
719
+ .get(domainId, normalizedHash);
720
+ return row === undefined ? undefined : toGlobalCandidate(row);
721
+ };
722
+ /** Recompute the recurrence weight from the observation rows (never from a counter the caller passes). */
723
+ const refreshCandidateWeight = (candidateId, now) => {
724
+ db.prepare('UPDATE global_candidates SET '
725
+ + 'distinct_identities = (SELECT COUNT(*) FROM global_candidate_observations WHERE candidate_id = ?), '
726
+ + 'observation_count = (SELECT COALESCE(SUM(observations), 0) FROM global_candidate_observations WHERE candidate_id = ?), '
727
+ + 'updated_at = ? WHERE id = ?').run(candidateId, candidateId, now, candidateId);
728
+ };
729
+ const upsertGlobalCandidate = (candidate, now) => {
730
+ db.prepare('INSERT INTO global_candidates(domain_id, content, normalized_hash, first_identity, session_id, '
731
+ + 'compartment_start, compartment_end, distinct_identities, observation_count, category, source_memory_id, '
732
+ + 'suggested_domain, suggestion_reason, '
733
+ + "state, created_at, updated_at) VALUES(?, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?, ?, ?, 'pending', ?, ?) "
734
+ + 'ON CONFLICT(domain_id, normalized_hash) DO UPDATE SET content = excluded.content, '
735
+ // A later sighting that does not carry the metadata must not erase it.
736
+ + 'category = COALESCE(excluded.category, global_candidates.category), '
737
+ + 'source_memory_id = COALESCE(excluded.source_memory_id, global_candidates.source_memory_id), '
738
+ + 'suggested_domain = COALESCE(excluded.suggested_domain, global_candidates.suggested_domain), '
739
+ + 'suggestion_reason = COALESCE(excluded.suggestion_reason, global_candidates.suggestion_reason), '
740
+ + 'updated_at = excluded.updated_at, '
741
+ + "state = CASE WHEN global_candidates.state = 'expired' THEN 'pending' ELSE global_candidates.state END").run(candidate.domainId, candidate.content, candidate.normalizedHash, candidate.firstIdentity, candidate.sessionId ?? null, candidate.compartmentStart ?? null, candidate.compartmentEnd ?? null, candidate.category ?? null, candidate.sourceMemoryId ?? null, candidate.suggestedDomain ?? null, candidate.suggestionReason ?? null, now, now);
742
+ const row = getGlobalCandidateByHash(candidate.domainId, candidate.normalizedHash);
743
+ if (row === undefined)
744
+ throw new Error('magic-context dsh adapter: global candidate upsert did not land');
745
+ return row;
746
+ };
747
+ const getGlobalCandidate = (id) => {
748
+ const row = db.prepare('SELECT * FROM global_candidates WHERE id = ?').get(id);
749
+ return row === undefined ? undefined : toGlobalCandidate(row);
750
+ };
751
+ const listGlobalCandidates = (filter) => {
752
+ const where = [];
753
+ const params = [];
754
+ if (filter?.domainId !== undefined) {
755
+ where.push('domain_id = ?');
756
+ params.push(filter.domainId);
757
+ }
758
+ if (filter?.state !== undefined) {
759
+ where.push('state = ?');
760
+ params.push(filter.state);
761
+ }
762
+ const clause = where.length === 0 ? '' : ` WHERE ${where.join(' AND ')}`;
763
+ const rows = db
764
+ .prepare(`SELECT * FROM global_candidates${clause} ORDER BY updated_at DESC, id DESC`)
765
+ .all(...params);
766
+ return rows.map(toGlobalCandidate);
767
+ };
768
+ const setGlobalCandidateState = (id, state, now) => {
769
+ db.prepare('UPDATE global_candidates SET state = ?, updated_at = ? WHERE id = ?').run(state, now, id);
770
+ };
771
+ const recordCandidateObservation = (observation, now) => {
772
+ db.exec('BEGIN');
773
+ try {
774
+ db.prepare('INSERT INTO global_candidate_observations(candidate_id, identity, session_id, compartment_start, '
775
+ + 'compartment_end, observations, first_seen_at, last_seen_at) VALUES(?, ?, ?, ?, ?, 1, ?, ?) '
776
+ + 'ON CONFLICT(candidate_id, identity, session_id) DO UPDATE SET observations = observations + 1, '
777
+ + 'last_seen_at = excluded.last_seen_at').run(observation.candidateId, observation.identity, observation.sessionId, observation.compartmentStart ?? null, observation.compartmentEnd ?? null, now, now);
778
+ refreshCandidateWeight(observation.candidateId, now);
779
+ db.exec('COMMIT');
780
+ }
781
+ catch (error) {
782
+ db.exec('ROLLBACK');
783
+ throw error;
784
+ }
785
+ };
786
+ const listCandidateObservations = (candidateId) => db.prepare('SELECT * FROM global_candidate_observations WHERE candidate_id = ? ORDER BY identity, session_id').all(candidateId).map(row => ({
787
+ candidateId: row.candidate_id,
788
+ identity: row.identity,
789
+ sessionId: row.session_id,
790
+ ...row.compartment_start === null ? {} : { compartmentStart: row.compartment_start },
791
+ ...row.compartment_end === null ? {} : { compartmentEnd: row.compartment_end },
792
+ observations: row.observations,
793
+ firstSeenAt: row.first_seen_at,
794
+ lastSeenAt: row.last_seen_at,
795
+ }));
796
+ const toProfileConfirmation = (row) => ({
797
+ id: row.id,
798
+ contractCandidateId: row.contract_candidate_id,
799
+ question: row.question,
800
+ content: row.content,
801
+ state: row.state,
802
+ createdAt: row.created_at,
803
+ ...row.decided_at === null ? {} : { decidedAt: row.decided_at },
804
+ });
805
+ const enqueueProfileConfirmation = (input, now) => {
806
+ // Idempotent per pending candidate: a retried historian pass must not
807
+ // produce a second question for the same observation.
808
+ const existing = db.prepare("SELECT * FROM profile_confirmations WHERE contract_candidate_id = ? AND state = 'pending'").get(input.contractCandidateId);
809
+ if (existing !== undefined)
810
+ return toProfileConfirmation(existing);
811
+ const inserted = db.prepare("INSERT INTO profile_confirmations(contract_candidate_id, question, content, state, created_at) "
812
+ + "VALUES(?, ?, ?, 'pending', ?) RETURNING *").get(input.contractCandidateId, input.question, input.content, now);
813
+ return toProfileConfirmation(inserted);
814
+ };
815
+ const listProfileConfirmations = (state) => {
816
+ const rows = (state === undefined
817
+ ? db.prepare('SELECT * FROM profile_confirmations ORDER BY created_at, id').all()
818
+ : db.prepare('SELECT * FROM profile_confirmations WHERE state = ? ORDER BY created_at, id').all(state));
819
+ return rows.map(toProfileConfirmation);
820
+ };
821
+ const decideProfileConfirmation = (id, state, now) => {
822
+ db.prepare('UPDATE profile_confirmations SET state = ?, decided_at = ? WHERE id = ?').run(state, now, id);
823
+ };
824
+ return {
825
+ dbPath,
826
+ upsertDomain,
827
+ listDomains,
828
+ getDomain,
829
+ setDomainEnabled,
830
+ addDomainMember,
831
+ removeDomainMember,
832
+ listDomainMembers,
833
+ activeDomainIdentities,
834
+ upsertGlobalCandidate,
835
+ getGlobalCandidate,
836
+ listGlobalCandidates,
837
+ setGlobalCandidateState,
838
+ recordCandidateObservation,
839
+ listCandidateObservations,
840
+ enqueueProfileConfirmation,
841
+ listProfileConfirmations,
842
+ decideProfileConfirmation,
843
+ allocateTag,
844
+ getTag,
845
+ listTags,
846
+ recordUsage,
847
+ listUsage,
848
+ latestUsage,
849
+ latestHistoryChangeAt,
850
+ queueDrop,
851
+ listPendingDrops,
852
+ requestFlush,
853
+ listFlushRequestedDrops,
854
+ clearFlushRequests,
855
+ markDropLanded,
856
+ dropState,
857
+ tagSessions,
858
+ allocateCompartment,
859
+ listCompartments,
860
+ listUnlandedCompartments,
861
+ markCompartmentLanded,
862
+ markCompartmentRejected,
863
+ supersedeCompartment,
864
+ replaceCompartmentDraft,
865
+ coveredSeqs,
866
+ baselineState,
867
+ saveBaselineState,
868
+ setMeta: (key, value) => {
869
+ db.prepare('INSERT INTO meta(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value')
870
+ .run(key, value);
871
+ },
872
+ getMeta: (key) => {
873
+ const row = db.prepare('SELECT value FROM meta WHERE key = ?').get(key);
874
+ return row?.value;
875
+ },
876
+ appendJournal: (entry) => {
877
+ db.prepare('INSERT INTO journal(at, level, kind, session_id, summary, data) VALUES(?, ?, ?, ?, ?, ?)')
878
+ .run(entry.at, entry.level, entry.kind, entry.sessionId ?? null, entry.summary, entry.data === undefined ? null : JSON.stringify(entry.data));
879
+ },
880
+ listJournal: (filter = {}) => {
881
+ const clauses = [];
882
+ const parameters = [];
883
+ if (filter.since !== undefined) {
884
+ clauses.push('at >= ?');
885
+ parameters.push(filter.since);
886
+ }
887
+ if (filter.until !== undefined) {
888
+ clauses.push('at <= ?');
889
+ parameters.push(filter.until);
890
+ }
891
+ if (filter.sessionId !== undefined) {
892
+ clauses.push('session_id = ?');
893
+ parameters.push(filter.sessionId);
894
+ }
895
+ if (filter.kinds !== undefined && filter.kinds.length > 0) {
896
+ clauses.push(`kind IN (${filter.kinds.map(() => '?').join(', ')})`);
897
+ parameters.push(...filter.kinds);
898
+ }
899
+ if (filter.levels !== undefined && filter.levels.length > 0) {
900
+ clauses.push(`level IN (${filter.levels.map(() => '?').join(', ')})`);
901
+ parameters.push(...filter.levels);
902
+ }
903
+ const where = clauses.length === 0 ? '' : ` WHERE ${clauses.join(' AND ')}`;
904
+ const limit = filter.limit === undefined ? '' : ` LIMIT ${String(Math.max(1, Math.floor(filter.limit)))}`;
905
+ const rows = db.prepare(`SELECT * FROM journal${where} ORDER BY id DESC${limit}`)
906
+ .all(...parameters);
907
+ return rows.map((row) => {
908
+ let data;
909
+ if (row.data !== null) {
910
+ try {
911
+ data = JSON.parse(row.data);
912
+ }
913
+ catch {
914
+ data = { unparsable: true };
915
+ }
916
+ }
917
+ return {
918
+ at: row.at,
919
+ level: row.level,
920
+ kind: row.kind,
921
+ summary: row.summary,
922
+ ...row.session_id === null ? {} : { sessionId: row.session_id },
923
+ ...data === undefined ? {} : { data },
924
+ };
925
+ });
926
+ },
927
+ journalByKind: (since, sessionId) => {
928
+ const clauses = [];
929
+ const parameters = [];
930
+ if (since !== undefined) {
931
+ clauses.push('at >= ?');
932
+ parameters.push(since);
933
+ }
934
+ if (sessionId !== undefined) {
935
+ clauses.push('session_id = ?');
936
+ parameters.push(sessionId);
937
+ }
938
+ const where = clauses.length === 0 ? '' : ` WHERE ${clauses.join(' AND ')}`;
939
+ return db.prepare(`SELECT kind, level, COUNT(*) AS count FROM journal${where} GROUP BY kind, level ORDER BY count DESC`).all(...parameters);
940
+ },
941
+ pruneJournal: (before) => {
942
+ const result = db.prepare('DELETE FROM journal WHERE at < ?').run(before);
943
+ return Number(result.changes);
944
+ },
945
+ recordMemoryEvent: (projectIdentity, memoryId, kind, now) => {
946
+ // Two statements on purpose: the upsert seeds the row, the bump applies
947
+ // the one counter the event names. ≤ ~46 events per pass (memoryLimit +
948
+ // search rows), so the extra prepare is noise next to correctness.
949
+ db.prepare('INSERT INTO memory_stats(project_identity, memory_id, updated_at) VALUES(?, ?, ?) '
950
+ + 'ON CONFLICT(project_identity, memory_id) DO NOTHING').run(projectIdentity, memoryId, now);
951
+ if (kind === 'hit') {
952
+ db.prepare('UPDATE memory_stats SET hits = hits + 1, last_hit_at = ?, updated_at = ? '
953
+ + 'WHERE project_identity = ? AND memory_id = ?').run(now, now, projectIdentity, memoryId);
954
+ }
955
+ else {
956
+ db.prepare('UPDATE memory_stats SET injections = injections + 1, last_injected_at = ?, updated_at = ? '
957
+ + 'WHERE project_identity = ? AND memory_id = ?').run(now, now, projectIdentity, memoryId);
958
+ }
959
+ },
960
+ listMemoryStats: (projectIdentity, memoryIds) => {
961
+ const toStat = (row) => ({
962
+ projectIdentity: row.project_identity,
963
+ memoryId: row.memory_id,
964
+ hits: row.hits,
965
+ ...row.last_hit_at === null ? {} : { lastHitAt: row.last_hit_at },
966
+ injections: row.injections,
967
+ ...row.last_injected_at === null ? {} : { lastInjectedAt: row.last_injected_at },
968
+ localArchived: row.local_archived !== 0,
969
+ updatedAt: row.updated_at,
970
+ });
971
+ if (memoryIds !== undefined && memoryIds.length === 0)
972
+ return [];
973
+ if (memoryIds === undefined) {
974
+ return db.prepare('SELECT * FROM memory_stats WHERE project_identity = ?')
975
+ .all(projectIdentity).map(toStat);
976
+ }
977
+ const out = [];
978
+ // Chunked IN lists stay under SQLite's bound-parameter ceiling.
979
+ for (let index = 0; index < memoryIds.length; index += 500) {
980
+ const chunk = memoryIds.slice(index, index + 500);
981
+ const placeholders = chunk.map(() => '?').join(', ');
982
+ const rows = db.prepare(`SELECT * FROM memory_stats WHERE project_identity = ? AND memory_id IN (${placeholders})`).all(projectIdentity, ...chunk);
983
+ out.push(...rows.map(toStat));
984
+ }
985
+ return out;
986
+ },
987
+ // NOTE: no setter for `local_archived` any more — it had zero production
988
+ // callers from birth (2026-10-10 TTL 排查定案), and S(t) retirement is
989
+ // computed live at recall (memory-bridge pick) and live in the report
990
+ // meter. The column + the read in pick() stay as a manual-retire hook.
991
+ pruneMemoryStats: (projectIdentity, keepIds) => {
992
+ // One statement: orphans are rows whose memory_id left the known pool.
993
+ // keepIds is the project's whole visible pool (memoryLimit-bounded
994
+ // rendering aside, pools stay in the hundreds — well under SQLite's
995
+ // bound-parameter ceiling).
996
+ if (keepIds.length === 0) {
997
+ const result = db.prepare('DELETE FROM memory_stats WHERE project_identity = ?').run(projectIdentity);
998
+ return Number(result.changes);
999
+ }
1000
+ const placeholders = keepIds.map(() => '?').join(', ');
1001
+ const result = db.prepare(`DELETE FROM memory_stats WHERE project_identity = ? AND memory_id NOT IN (${placeholders})`).run(projectIdentity, ...keepIds);
1002
+ return Number(result.changes);
1003
+ },
1004
+ journalBounds: () => {
1005
+ const row = db.prepare('SELECT COUNT(*) AS rows, MIN(at) AS oldest, MAX(at) AS newest FROM journal')
1006
+ .get();
1007
+ return {
1008
+ rows: row.rows,
1009
+ ...row.oldest === null ? {} : { oldest: row.oldest },
1010
+ ...row.newest === null ? {} : { newest: row.newest },
1011
+ };
1012
+ },
1013
+ get closed() {
1014
+ return closed;
1015
+ },
1016
+ close: () => {
1017
+ closed = true;
1018
+ db.close();
1019
+ },
1020
+ };
1021
+ }