zam-core 0.3.7 → 0.3.11

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/dist/index.js CHANGED
@@ -1,76 +1,84 @@
1
1
  // src/kernel/analytics/stats.ts
2
- function q(db, sql, ...params) {
3
- return db.prepare(sql).get(...params);
2
+ async function q(db, sql, ...params) {
3
+ return await db.prepare(sql).get(...params);
4
4
  }
5
- function getUserStats(db, userId) {
5
+ async function count(db, sql, ...params) {
6
+ return (await q(db, sql, ...params)).n;
7
+ }
8
+ async function getUserStats(db, userId) {
9
+ const avgRow = await q(
10
+ db,
11
+ "SELECT AVG(stability) as v FROM cards WHERE user_id = ? AND reps > 0",
12
+ userId
13
+ );
14
+ const lastSessionRow = await db.prepare(
15
+ "SELECT started_at FROM sessions WHERE user_id = ? ORDER BY started_at DESC LIMIT 1"
16
+ ).get(userId);
6
17
  return {
7
18
  userId,
8
- totalTokens: q(db, "SELECT COUNT(*) as n FROM tokens").n,
9
- cardsInDeck: q(db, "SELECT COUNT(*) as n FROM cards WHERE user_id = ?", userId).n,
10
- dueToday: q(
19
+ totalTokens: await count(db, "SELECT COUNT(*) as n FROM tokens"),
20
+ cardsInDeck: await count(
21
+ db,
22
+ "SELECT COUNT(*) as n FROM cards WHERE user_id = ?",
23
+ userId
24
+ ),
25
+ dueToday: await count(
11
26
  db,
12
27
  "SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND blocked = 0 AND due_at <= datetime('now')",
13
28
  userId
14
- ).n,
15
- blocked: q(
29
+ ),
30
+ blocked: await count(
16
31
  db,
17
32
  "SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND blocked = 1",
18
33
  userId
19
- ).n,
20
- mature: q(
34
+ ),
35
+ mature: await count(
21
36
  db,
22
37
  "SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND reps >= 3 AND stability >= 21",
23
38
  userId
24
- ).n,
25
- avgStability: (() => {
26
- const v = q(
27
- db,
28
- "SELECT AVG(stability) as v FROM cards WHERE user_id = ? AND reps > 0",
29
- userId
30
- );
31
- return v.v ? Math.round(v.v * 100) / 100 : null;
32
- })(),
33
- totalSessions: q(db, "SELECT COUNT(*) as n FROM sessions WHERE user_id = ?", userId).n,
34
- lastSession: (() => {
35
- const r = db.prepare(
36
- "SELECT started_at FROM sessions WHERE user_id = ? ORDER BY started_at DESC LIMIT 1"
37
- ).get(userId);
38
- return r?.started_at ?? null;
39
- })()
39
+ ),
40
+ avgStability: avgRow.v ? Math.round(avgRow.v * 100) / 100 : null,
41
+ totalSessions: await count(
42
+ db,
43
+ "SELECT COUNT(*) as n FROM sessions WHERE user_id = ?",
44
+ userId
45
+ ),
46
+ lastSession: lastSessionRow?.started_at ?? null
40
47
  };
41
48
  }
42
- function getDomainCompetence(db, userId) {
43
- const domains = db.prepare(
49
+ async function getDomainCompetence(db, userId) {
50
+ const domains = await db.prepare(
44
51
  `SELECT DISTINCT t.domain FROM cards c
45
52
  JOIN tokens t ON t.id = c.token_id
46
53
  WHERE c.user_id = ? AND t.domain != ''`
47
54
  ).all(userId);
48
- return domains.map((d) => {
49
- const total = q(
55
+ const competences = [];
56
+ for (const d of domains) {
57
+ const total = await count(
50
58
  db,
51
59
  `SELECT COUNT(*) as n FROM cards c
52
60
  JOIN tokens t ON t.id = c.token_id
53
61
  WHERE c.user_id = ? AND t.domain = ?`,
54
62
  userId,
55
63
  d.domain
56
- ).n;
57
- const mature = q(
64
+ );
65
+ const mature = await count(
58
66
  db,
59
67
  `SELECT COUNT(*) as n FROM cards c
60
68
  JOIN tokens t ON t.id = c.token_id
61
69
  WHERE c.user_id = ? AND t.domain = ? AND c.reps >= 3 AND c.stability >= 21`,
62
70
  userId,
63
71
  d.domain
64
- ).n;
65
- const avgStab = q(
72
+ );
73
+ const avgStab = (await q(
66
74
  db,
67
75
  `SELECT AVG(c.stability) as v FROM cards c
68
76
  JOIN tokens t ON t.id = c.token_id
69
77
  WHERE c.user_id = ? AND t.domain = ? AND c.reps > 0`,
70
78
  userId,
71
79
  d.domain
72
- ).v ?? 0;
73
- const reviews = q(
80
+ )).v ?? 0;
81
+ const reviews = await q(
74
82
  db,
75
83
  `SELECT COUNT(*) as total,
76
84
  SUM(CASE WHEN rating >= 2 THEN 1 ELSE 0 END) as passed
@@ -88,19 +96,26 @@ function getDomainCompetence(db, userId) {
88
96
  } else {
89
97
  suggestedMode = "shadowing";
90
98
  }
91
- return {
99
+ competences.push({
92
100
  domain: d.domain,
93
101
  totalCards: total,
94
102
  matureCards: mature,
95
103
  avgStability: Math.round(avgStab * 100) / 100,
96
104
  retentionRate: Math.round(retentionRate * 1e3) / 1e3,
97
105
  suggestedMode
98
- };
99
- });
106
+ });
107
+ }
108
+ return competences;
100
109
  }
101
110
 
102
111
  // src/kernel/credentials.ts
103
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
112
+ import {
113
+ chmodSync,
114
+ existsSync,
115
+ mkdirSync,
116
+ readFileSync,
117
+ writeFileSync
118
+ } from "fs";
104
119
  import { homedir } from "os";
105
120
  import { dirname, join } from "path";
106
121
  var DEFAULT_CREDENTIALS_PATH = join(homedir(), ".zam", "credentials.json");
@@ -116,22 +131,37 @@ function loadCredentials(path) {
116
131
  function saveCredentials(creds, path) {
117
132
  const p = path ?? DEFAULT_CREDENTIALS_PATH;
118
133
  const dir = dirname(p);
134
+ let createdDirectory = false;
119
135
  if (!existsSync(dir)) {
120
- mkdirSync(dir, { recursive: true });
136
+ mkdirSync(dir, { recursive: true, mode: 448 });
137
+ createdDirectory = true;
138
+ }
139
+ if (process.platform !== "win32" && (path === void 0 || createdDirectory)) {
140
+ chmodSync(dir, 448);
121
141
  }
122
142
  writeFileSync(p, `${JSON.stringify(creds, null, 2)}
123
- `, "utf-8");
143
+ `, {
144
+ encoding: "utf-8",
145
+ mode: 384
146
+ });
147
+ if (process.platform !== "win32") {
148
+ chmodSync(p, 384);
149
+ }
124
150
  }
125
151
  function getTursoCredentials(path) {
126
152
  const creds = loadCredentials(path);
127
153
  if (creds.turso?.url && creds.turso?.token) {
128
- return { url: creds.turso.url, token: creds.turso.token };
154
+ return {
155
+ url: creds.turso.url,
156
+ token: creds.turso.token,
157
+ ...creds.turso.mode ? { mode: creds.turso.mode } : {}
158
+ };
129
159
  }
130
160
  return null;
131
161
  }
132
- function setTursoCredentials(url, token, path) {
162
+ function setTursoCredentials(url, token, path, mode) {
133
163
  const creds = loadCredentials(path);
134
- creds.turso = { url, token };
164
+ creds.turso = { url, token, ...mode ? { mode } : {} };
135
165
  saveCredentials(creds, path);
136
166
  }
137
167
  function clearTursoCredentials(path) {
@@ -222,7 +252,255 @@ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as rea
222
252
  import { createRequire } from "module";
223
253
  import { homedir as homedir2 } from "os";
224
254
  import { dirname as dirname2, join as join2 } from "path";
225
- import BetterSqlite3 from "better-sqlite3";
255
+
256
+ // src/kernel/db/remote/hrana.ts
257
+ var DEFAULT_TIMEOUT_MS = 15e3;
258
+ var DEFAULT_MAX_ATTEMPTS = 2;
259
+ function toHttpUrl(url) {
260
+ return url.replace(/^libsql:\/\//i, "https://").replace(/^wss:\/\//i, "https://").replace(/^ws:\/\//i, "http://").replace(/\/+$/, "");
261
+ }
262
+ function encodeValue(param) {
263
+ if (param === null) return { type: "null" };
264
+ if (typeof param === "string") return { type: "text", value: param };
265
+ if (typeof param === "bigint") {
266
+ return { type: "integer", value: param.toString() };
267
+ }
268
+ if (typeof param === "number") {
269
+ if (Number.isSafeInteger(param)) {
270
+ return { type: "integer", value: param.toString() };
271
+ }
272
+ return { type: "float", value: param };
273
+ }
274
+ if (param instanceof Uint8Array) {
275
+ return { type: "blob", base64: Buffer.from(param).toString("base64") };
276
+ }
277
+ throw new TypeError(
278
+ `Cannot bind a value of type ${typeof param} to a SQL parameter`
279
+ );
280
+ }
281
+ function decodeValue(value) {
282
+ switch (value.type) {
283
+ case "null":
284
+ return null;
285
+ case "integer":
286
+ return Number(value.value);
287
+ case "float":
288
+ return value.value;
289
+ case "text":
290
+ return value.value;
291
+ case "blob":
292
+ return new Uint8Array(Buffer.from(value.base64, "base64"));
293
+ }
294
+ }
295
+ function rowsToObjects(result) {
296
+ return result.rows.map((row) => {
297
+ const obj = {};
298
+ row.forEach((value, i) => {
299
+ obj[result.cols[i]?.name ?? `col${i}`] = decodeValue(value);
300
+ });
301
+ return obj;
302
+ });
303
+ }
304
+ function isRetryableTransportError(err) {
305
+ if (!(err instanceof Error) || err.name === "HranaResponseError") {
306
+ return false;
307
+ }
308
+ const code = err.cause?.code;
309
+ return code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "EAI_AGAIN";
310
+ }
311
+ var HranaResponseError = class extends Error {
312
+ name = "HranaResponseError";
313
+ };
314
+ var HranaTransport = class {
315
+ pipelineUrl;
316
+ authToken;
317
+ timeoutMs;
318
+ maxAttempts;
319
+ constructor(options) {
320
+ this.pipelineUrl = `${toHttpUrl(options.url)}/v3/pipeline`;
321
+ this.authToken = options.authToken;
322
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
323
+ this.maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
324
+ }
325
+ /**
326
+ * POST one pipeline of requests. Returns the server results plus the baton
327
+ * for continuing an open stream. Retries transport-level failures only for
328
+ * stateless pipelines (no baton involved on either side).
329
+ */
330
+ async pipeline(requests, baton, baseUrl) {
331
+ const url = baseUrl ? `${toHttpUrl(baseUrl)}/v3/pipeline` : this.pipelineUrl;
332
+ const keepsState = baton != null || !requests.some((r) => r.type === "close");
333
+ const attempts = keepsState ? 1 : this.maxAttempts;
334
+ let lastError;
335
+ for (let attempt = 1; attempt <= attempts; attempt++) {
336
+ try {
337
+ return await this.post(url, { baton: baton ?? null, requests });
338
+ } catch (err) {
339
+ lastError = err;
340
+ if (!isRetryableTransportError(err) || attempt === attempts) {
341
+ throw this.offline(err);
342
+ }
343
+ }
344
+ }
345
+ throw this.offline(lastError);
346
+ }
347
+ async post(url, body) {
348
+ const controller = new AbortController();
349
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
350
+ let response;
351
+ try {
352
+ response = await fetch(url, {
353
+ method: "POST",
354
+ headers: {
355
+ "content-type": "application/json",
356
+ ...this.authToken ? { authorization: `Bearer ${this.authToken}` } : {}
357
+ },
358
+ body: JSON.stringify(body),
359
+ signal: controller.signal
360
+ });
361
+ } finally {
362
+ clearTimeout(timer);
363
+ }
364
+ if (response.status === 401 || response.status === 403) {
365
+ throw new HranaResponseError(
366
+ `Turso rejected the configured credentials (HTTP ${response.status}). Refresh the token with: zam connector setup turso`
367
+ );
368
+ }
369
+ if (!response.ok) {
370
+ const detail = await response.text().catch(() => "");
371
+ throw new HranaResponseError(
372
+ `Turso request failed with HTTP ${response.status}${detail ? `: ${detail.slice(0, 200)}` : ""}`
373
+ );
374
+ }
375
+ return await response.json();
376
+ }
377
+ offline(err) {
378
+ if (err instanceof HranaResponseError) return err;
379
+ const cause = err instanceof Error ? err.cause?.message ?? err.message : String(err);
380
+ return new HranaResponseError(
381
+ `Cannot reach the Turso database at ${this.pipelineUrl}: ${cause}. Check your network connection, or switch to the local provider (ZAM_DB_PROVIDER=local).`
382
+ );
383
+ }
384
+ };
385
+ function unwrapResult(response, index) {
386
+ const entry = response.results[index];
387
+ if (!entry) {
388
+ throw new HranaResponseError(`Turso response is missing result #${index}`);
389
+ }
390
+ if (entry.type === "error") {
391
+ throw new Error(entry.error.message);
392
+ }
393
+ return entry.response.result;
394
+ }
395
+
396
+ // src/kernel/db/remote/provider.ts
397
+ var TABLE_INFO_PRAGMA = /^\s*table_info\s*\(\s*['"]?(\w+)['"]?\s*\)\s*$/i;
398
+ function toRunResult(result) {
399
+ return {
400
+ changes: result?.affected_row_count ?? 0,
401
+ lastInsertRowid: result?.last_insert_rowid != null ? Number(result.last_insert_rowid) : 0
402
+ };
403
+ }
404
+ function makeStatement(sql, run) {
405
+ const execute = async (params, wantRows) => {
406
+ const { results } = await run([
407
+ {
408
+ type: "execute",
409
+ stmt: { sql, args: params.map(encodeValue), want_rows: wantRows }
410
+ }
411
+ ]);
412
+ return results[0];
413
+ };
414
+ return {
415
+ async run(...params) {
416
+ return toRunResult(await execute(params, false));
417
+ },
418
+ async get(...params) {
419
+ const result = await execute(params, true);
420
+ return result ? rowsToObjects(result)[0] : void 0;
421
+ },
422
+ async all(...params) {
423
+ const result = await execute(params, true);
424
+ return result ? rowsToObjects(result) : [];
425
+ }
426
+ };
427
+ }
428
+ function makeDatabase(run, transport) {
429
+ let txTail = Promise.resolve();
430
+ const db = {
431
+ prepare(sql) {
432
+ return makeStatement(sql, run);
433
+ },
434
+ async exec(sql) {
435
+ await run([{ type: "sequence", sql }]);
436
+ },
437
+ async pragma(source) {
438
+ const tableInfo = TABLE_INFO_PRAGMA.exec(source);
439
+ const sql = tableInfo ? `SELECT * FROM pragma_table_info('${tableInfo[1]}')` : `PRAGMA ${source}`;
440
+ return db.prepare(sql).all();
441
+ },
442
+ transaction(fn) {
443
+ const next = txTail.then(async () => {
444
+ const stream = openStream(transport);
445
+ try {
446
+ await stream.run([
447
+ {
448
+ type: "execute",
449
+ stmt: { sql: "BEGIN IMMEDIATE", want_rows: false }
450
+ }
451
+ ]);
452
+ const result = await fn(makeDatabase(stream.run, transport));
453
+ await stream.run(
454
+ [{ type: "execute", stmt: { sql: "COMMIT", want_rows: false } }],
455
+ true
456
+ );
457
+ return result;
458
+ } catch (err) {
459
+ await stream.run(
460
+ [
461
+ {
462
+ type: "execute",
463
+ stmt: { sql: "ROLLBACK", want_rows: false }
464
+ }
465
+ ],
466
+ true
467
+ ).catch(() => {
468
+ });
469
+ throw err;
470
+ }
471
+ });
472
+ txTail = next.catch(() => {
473
+ });
474
+ return next;
475
+ },
476
+ async close() {
477
+ }
478
+ };
479
+ return db;
480
+ }
481
+ function openStream(transport) {
482
+ let baton = null;
483
+ let baseUrl = null;
484
+ return {
485
+ async run(requests, close = false) {
486
+ const sent = close ? [...requests, { type: "close" }] : requests;
487
+ const response = await transport.pipeline(sent, baton, baseUrl);
488
+ baton = response.baton;
489
+ baseUrl = response.base_url ?? baseUrl;
490
+ return {
491
+ results: requests.map((_, i) => unwrapResult(response, i))
492
+ };
493
+ }
494
+ };
495
+ }
496
+ function openRemoteDatabase(options) {
497
+ const transport = new HranaTransport(options);
498
+ const statelessRun = async (requests) => {
499
+ const response = await transport.pipeline([...requests, { type: "close" }]);
500
+ return { results: requests.map((_, i) => unwrapResult(response, i)) };
501
+ };
502
+ return makeDatabase(statelessRun, transport);
503
+ }
226
504
 
227
505
  // src/kernel/db/schema.ts
228
506
  var SCHEMA = `
@@ -303,6 +581,22 @@ CREATE TABLE IF NOT EXISTS session_steps (
303
581
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
304
582
  );
305
583
 
584
+ -- Confirmed ratings synthesized from monitor evidence.
585
+ -- The composite primary key makes repeated synthesis idempotent per token.
586
+ CREATE TABLE IF NOT EXISTS session_syntheses (
587
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
588
+ token_id TEXT NOT NULL REFERENCES tokens(id) ON DELETE CASCADE,
589
+ card_id TEXT NOT NULL REFERENCES cards(id) ON DELETE CASCADE,
590
+ inferred_rating INTEGER NOT NULL CHECK (inferred_rating BETWEEN 1 AND 4),
591
+ confirmed_rating INTEGER NOT NULL CHECK (confirmed_rating BETWEEN 1 AND 4),
592
+ confidence TEXT NOT NULL CHECK (confidence IN ('medium', 'high')),
593
+ evidence TEXT NOT NULL DEFAULT '{}',
594
+ review_log_id TEXT NOT NULL REFERENCES review_logs(id) ON DELETE CASCADE,
595
+ session_step_id TEXT NOT NULL REFERENCES session_steps(id) ON DELETE CASCADE,
596
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
597
+ PRIMARY KEY (session_id, token_id)
598
+ );
599
+
306
600
  -- User configuration
307
601
  CREATE TABLE IF NOT EXISTS user_config (
308
602
  key TEXT PRIMARY KEY,
@@ -335,14 +629,70 @@ CREATE INDEX IF NOT EXISTS idx_review_logs_user ON review_logs(user_id, reviewed
335
629
  CREATE INDEX IF NOT EXISTS idx_session_steps_session ON session_steps(session_id);
336
630
  `;
337
631
 
632
+ // src/kernel/db/sync-adapter.ts
633
+ function wrapSyncDatabase(driver) {
634
+ let txTail = Promise.resolve();
635
+ const db = {
636
+ prepare(sql) {
637
+ return {
638
+ async run(...params) {
639
+ return driver.prepare(sql).run(...params);
640
+ },
641
+ async get(...params) {
642
+ return driver.prepare(sql).get(...params);
643
+ },
644
+ async all(...params) {
645
+ return driver.prepare(sql).all(...params);
646
+ }
647
+ };
648
+ },
649
+ async exec(sql) {
650
+ driver.exec(sql);
651
+ },
652
+ async pragma(source) {
653
+ return driver.pragma(source);
654
+ },
655
+ transaction(fn) {
656
+ const run = txTail.then(async () => {
657
+ driver.exec("BEGIN IMMEDIATE");
658
+ try {
659
+ const result = await fn(db);
660
+ driver.exec("COMMIT");
661
+ return result;
662
+ } catch (err) {
663
+ driver.exec("ROLLBACK");
664
+ throw err;
665
+ }
666
+ });
667
+ txTail = run.catch(() => {
668
+ });
669
+ return run;
670
+ },
671
+ ...driver.sync ? {
672
+ async sync() {
673
+ driver.sync?.();
674
+ }
675
+ } : {},
676
+ async close() {
677
+ driver.close();
678
+ }
679
+ };
680
+ return db;
681
+ }
682
+
338
683
  // src/kernel/db/connection.ts
339
684
  var DEFAULT_DB_DIR = join2(homedir2(), ".zam");
340
685
  var DEFAULT_DB_PATH = join2(DEFAULT_DB_DIR, "zam.db");
341
686
  var require2 = createRequire(import.meta.url);
342
687
  function isRemoteDatabasePath(dbPath) {
343
- return /^(libsql|https?):\/\//i.test(dbPath);
688
+ return /^(libsql|https?|wss?):\/\//i.test(dbPath);
689
+ }
690
+ function isDatabaseProvider(value) {
691
+ return value === "local" || value === "native" || value === "remote";
344
692
  }
345
693
  function openLocalSqlite(dbPath) {
694
+ const mod = require2("better-sqlite3");
695
+ const BetterSqlite3 = "default" in mod ? mod.default : mod;
346
696
  return new BetterSqlite3(dbPath);
347
697
  }
348
698
  function loadLibsql() {
@@ -352,11 +702,11 @@ function loadLibsql() {
352
702
  } catch (err) {
353
703
  const detail = err instanceof Error ? ` ${err.message}` : "";
354
704
  throw new Error(
355
- `Turso sync requires the optional native libsql backend, which is not available for ${process.platform}/${process.arch}.${detail}`
705
+ `Turso sync requires the optional native libsql backend, which is not available for ${process.platform}/${process.arch}. Switch to the HTTP provider instead: zam connector setup turso --mode remote (or set ZAM_DB_PROVIDER=remote).${detail}`
356
706
  );
357
707
  }
358
708
  }
359
- function openDatabase(options = {}) {
709
+ async function openDatabase(options = {}) {
360
710
  const configuredCloud = options.useConfiguredCloud !== false && !options.dbPath && !options.syncUrl ? getTursoCredentials() : null;
361
711
  let requiresTurso = false;
362
712
  try {
@@ -377,7 +727,26 @@ function openDatabase(options = {}) {
377
727
  const dbPath = configuredCloud?.url ?? options.dbPath ?? DEFAULT_DB_PATH;
378
728
  const isRemote = isRemoteDatabasePath(dbPath);
379
729
  const isEmbeddedReplica = Boolean(options.syncUrl);
380
- if (options.initialize && !isRemote) {
730
+ const provider = resolveProvider(options, configuredCloud?.mode, isRemote);
731
+ const shouldInitialize = options.initialize === true || !isRemote && !isEmbeddedReplica && !existsSync2(dbPath);
732
+ if (provider === "remote") {
733
+ const url = isRemote ? dbPath : options.syncUrl;
734
+ if (!url) {
735
+ throw new Error(
736
+ "The remote database provider is selected but no Turso URL is configured. Run: zam connector setup turso"
737
+ );
738
+ }
739
+ const db2 = openRemoteDatabase({
740
+ url,
741
+ authToken: configuredCloud?.token ?? options.authToken
742
+ });
743
+ if (options.initialize) {
744
+ await db2.exec(SCHEMA);
745
+ }
746
+ await runMigrations(db2);
747
+ return db2;
748
+ }
749
+ if (shouldInitialize && !isRemote) {
381
750
  const dir = dirname2(dbPath);
382
751
  if (!existsSync2(dir)) {
383
752
  mkdirSync2(dir, { recursive: true });
@@ -402,66 +771,93 @@ function openDatabase(options = {}) {
402
771
  if (authToken) {
403
772
  dbOpts.authToken = authToken;
404
773
  }
405
- let db;
774
+ let driver;
406
775
  if (isRemote || isEmbeddedReplica) {
407
- const LibsqlDatabase = loadLibsql();
408
776
  try {
409
- db = new LibsqlDatabase(dbPath, dbOpts);
410
- } catch (err) {
411
- const msg = err.message;
412
- if (msg.includes("InvalidLocalState") && options.syncUrl) {
413
- const metaPath = `${dbPath}.meta`;
414
- const infoPath = `${dbPath}-info`;
415
- if (existsSync2(metaPath)) rmSync(metaPath);
416
- if (existsSync2(infoPath)) rmSync(infoPath);
417
- db = new LibsqlDatabase(dbPath, dbOpts);
418
- } else {
419
- throw err;
777
+ const LibsqlDatabase = loadLibsql();
778
+ try {
779
+ driver = new LibsqlDatabase(dbPath, dbOpts);
780
+ } catch (err) {
781
+ const msg = err.message;
782
+ if (msg.includes("InvalidLocalState") && options.syncUrl) {
783
+ const metaPath = `${dbPath}.meta`;
784
+ const infoPath = `${dbPath}-info`;
785
+ if (existsSync2(metaPath)) rmSync(metaPath);
786
+ if (existsSync2(infoPath)) rmSync(infoPath);
787
+ driver = new LibsqlDatabase(dbPath, dbOpts);
788
+ } else {
789
+ throw err;
790
+ }
791
+ }
792
+ } catch (nativeErr) {
793
+ const fallbackUrl = isRemote ? dbPath : options.syncUrl;
794
+ if (isRemote && !isEmbeddedReplica && fallbackUrl) {
795
+ const db2 = openRemoteDatabase({
796
+ url: fallbackUrl,
797
+ authToken: configuredCloud?.token ?? options.authToken
798
+ });
799
+ if (options.initialize) {
800
+ await db2.exec(SCHEMA);
801
+ }
802
+ await runMigrations(db2);
803
+ return db2;
420
804
  }
805
+ throw nativeErr;
421
806
  }
422
807
  } else {
423
- db = openLocalSqlite(dbPath);
808
+ driver = openLocalSqlite(dbPath);
424
809
  }
425
810
  if (!isRemote && !isEmbeddedReplica) {
426
- db.pragma("journal_mode = WAL");
811
+ driver.pragma("journal_mode = WAL");
427
812
  }
428
- db.pragma("foreign_keys = ON");
813
+ driver.pragma("foreign_keys = ON");
429
814
  if (!isRemote) {
430
- db.pragma("busy_timeout = 5000");
815
+ driver.pragma("busy_timeout = 5000");
431
816
  }
817
+ const db = wrapSyncDatabase(driver);
432
818
  if (isEmbeddedReplica) {
433
- db.sync?.();
819
+ await db.sync?.();
434
820
  }
435
- if (options.initialize) {
436
- db.exec(SCHEMA);
821
+ if (shouldInitialize) {
822
+ await db.exec(SCHEMA);
437
823
  }
438
- runMigrations(db);
824
+ await runMigrations(db);
439
825
  return db;
440
826
  }
441
- function openDatabaseWithSync(options = {}) {
827
+ function resolveProvider(options, credentialsMode, isRemote) {
828
+ if (options.provider) return options.provider;
829
+ const env = process.env.ZAM_DB_PROVIDER;
830
+ if (isDatabaseProvider(env)) return env;
831
+ if (isDatabaseProvider(credentialsMode) && (isRemote || options.syncUrl)) {
832
+ return credentialsMode;
833
+ }
834
+ if (isRemote || options.syncUrl) return "native";
835
+ return "local";
836
+ }
837
+ async function openDatabaseWithSync(options = {}) {
442
838
  return openDatabase(options);
443
839
  }
444
840
  function getDefaultDbPath() {
445
841
  return DEFAULT_DB_PATH;
446
842
  }
447
- function runMigrations(db) {
448
- const sessionCols = db.pragma("table_info(sessions)");
843
+ async function runMigrations(db) {
844
+ const sessionCols = await db.pragma("table_info(sessions)");
449
845
  if (sessionCols.length > 0 && !sessionCols.some((c) => c.name === "execution_context")) {
450
- db.exec(
846
+ await db.exec(
451
847
  `ALTER TABLE sessions ADD COLUMN execution_context TEXT NOT NULL DEFAULT 'shell'`
452
848
  );
453
849
  }
454
- const tokenCols = db.pragma("table_info(tokens)");
850
+ const tokenCols = await db.pragma("table_info(tokens)");
455
851
  if (tokenCols.length > 0 && !tokenCols.some((c) => c.name === "deprecated_at")) {
456
- db.exec(`ALTER TABLE tokens ADD COLUMN deprecated_at TEXT`);
852
+ await db.exec(`ALTER TABLE tokens ADD COLUMN deprecated_at TEXT`);
457
853
  }
458
854
  if (tokenCols.length > 0 && !tokenCols.some((c) => c.name === "source_link")) {
459
- db.exec(`ALTER TABLE tokens ADD COLUMN source_link TEXT`);
855
+ await db.exec(`ALTER TABLE tokens ADD COLUMN source_link TEXT`);
460
856
  }
461
857
  if (tokenCols.length > 0 && !tokenCols.some((c) => c.name === "question")) {
462
- db.exec(`ALTER TABLE tokens ADD COLUMN question TEXT`);
858
+ await db.exec(`ALTER TABLE tokens ADD COLUMN question TEXT`);
463
859
  }
464
- db.exec(`
860
+ await db.exec(`
465
861
  CREATE TABLE IF NOT EXISTS agent_skills (
466
862
  id TEXT PRIMARY KEY,
467
863
  slug TEXT NOT NULL UNIQUE,
@@ -473,6 +869,165 @@ function runMigrations(db) {
473
869
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
474
870
  )
475
871
  `);
872
+ await db.exec(`
873
+ CREATE TABLE IF NOT EXISTS session_syntheses (
874
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
875
+ token_id TEXT NOT NULL REFERENCES tokens(id) ON DELETE CASCADE,
876
+ card_id TEXT NOT NULL REFERENCES cards(id) ON DELETE CASCADE,
877
+ inferred_rating INTEGER NOT NULL CHECK (inferred_rating BETWEEN 1 AND 4),
878
+ confirmed_rating INTEGER NOT NULL CHECK (confirmed_rating BETWEEN 1 AND 4),
879
+ confidence TEXT NOT NULL CHECK (confidence IN ('medium', 'high')),
880
+ evidence TEXT NOT NULL DEFAULT '{}',
881
+ review_log_id TEXT NOT NULL REFERENCES review_logs(id) ON DELETE CASCADE,
882
+ session_step_id TEXT NOT NULL REFERENCES session_steps(id) ON DELETE CASCADE,
883
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
884
+ PRIMARY KEY (session_id, token_id)
885
+ )
886
+ `);
887
+ }
888
+
889
+ // src/kernel/db/snapshot.ts
890
+ import { createHash } from "crypto";
891
+ var SNAPSHOT_FORMAT = "zam-snapshot";
892
+ var SNAPSHOT_VERSION = 1;
893
+ var MANIFEST_PREFIX = "-- zam-snapshot: ";
894
+ var SNAPSHOT_TABLES = [
895
+ "tokens",
896
+ "sessions",
897
+ "cards",
898
+ "prerequisites",
899
+ "session_steps",
900
+ "review_logs",
901
+ "session_syntheses",
902
+ "user_config",
903
+ "agent_skills"
904
+ ];
905
+ function quoteValue(value) {
906
+ if (value === null || value === void 0) return "NULL";
907
+ if (typeof value === "number") {
908
+ return Number.isFinite(value) ? String(value) : "NULL";
909
+ }
910
+ if (typeof value === "bigint") return value.toString();
911
+ if (typeof value === "string") return `'${value.replace(/'/g, "''")}'`;
912
+ if (value instanceof Uint8Array) {
913
+ let hex = "";
914
+ for (const byte of value) hex += byte.toString(16).padStart(2, "0");
915
+ return `X'${hex}'`;
916
+ }
917
+ throw new Error(`Cannot serialize value of type ${typeof value} to SQL`);
918
+ }
919
+ async function getColumns(db, table) {
920
+ const cols = await db.pragma(`table_info(${table})`);
921
+ return cols.map((c) => c.name);
922
+ }
923
+ async function countRows(db, table) {
924
+ const row = await db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get();
925
+ return Number(row.n);
926
+ }
927
+ async function exportSnapshot(db, options = {}) {
928
+ const createdAt = options.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
929
+ const tables = {};
930
+ const sections = [];
931
+ for (const table of SNAPSHOT_TABLES) {
932
+ const columns = await getColumns(db, table);
933
+ if (columns.length === 0) {
934
+ tables[table] = 0;
935
+ continue;
936
+ }
937
+ const colList = columns.join(", ");
938
+ const rows = await db.prepare(`SELECT ${colList} FROM ${table}`).all();
939
+ tables[table] = rows.length;
940
+ if (rows.length === 0) continue;
941
+ const lines = [`-- ${table} (${rows.length})`];
942
+ for (const row of rows) {
943
+ const values = columns.map((c) => quoteValue(row[c])).join(", ");
944
+ lines.push(`INSERT INTO ${table} (${colList}) VALUES (${values});`);
945
+ }
946
+ sections.push(lines.join("\n"));
947
+ }
948
+ const body = sections.length > 0 ? `${sections.join("\n\n")}
949
+ ` : "";
950
+ const checksum = createHash("sha256").update(body).digest("hex");
951
+ const manifest = {
952
+ format: SNAPSHOT_FORMAT,
953
+ version: SNAPSHOT_VERSION,
954
+ createdAt,
955
+ tables,
956
+ checksum
957
+ };
958
+ return `${MANIFEST_PREFIX}${JSON.stringify(manifest)}
959
+ ${body}`;
960
+ }
961
+ function parseSnapshot(snapshot) {
962
+ const newline = snapshot.indexOf("\n");
963
+ const header = (newline === -1 ? snapshot : snapshot.slice(0, newline)).trim();
964
+ const body = newline === -1 ? "" : snapshot.slice(newline + 1);
965
+ if (!header.startsWith(MANIFEST_PREFIX)) {
966
+ throw new Error("Not a ZAM snapshot: missing manifest header.");
967
+ }
968
+ let manifest;
969
+ try {
970
+ manifest = JSON.parse(header.slice(MANIFEST_PREFIX.length));
971
+ } catch {
972
+ throw new Error("Snapshot manifest is not valid JSON.");
973
+ }
974
+ if (manifest.format !== SNAPSHOT_FORMAT) {
975
+ throw new Error(`Unsupported snapshot format: ${manifest.format}`);
976
+ }
977
+ if (manifest.version > SNAPSHOT_VERSION) {
978
+ throw new Error(
979
+ `Snapshot version ${manifest.version} is newer than supported (${SNAPSHOT_VERSION}). Upgrade ZAM to import it.`
980
+ );
981
+ }
982
+ return { manifest, body };
983
+ }
984
+ function verifySnapshot(snapshot) {
985
+ const { manifest, body } = parseSnapshot(snapshot);
986
+ const actual = createHash("sha256").update(body).digest("hex");
987
+ if (actual !== manifest.checksum) {
988
+ throw new Error("Snapshot is corrupted: checksum mismatch.");
989
+ }
990
+ return manifest;
991
+ }
992
+ async function importSnapshot(db, snapshot, options = {}) {
993
+ const { manifest, body } = parseSnapshot(snapshot);
994
+ const actual = createHash("sha256").update(body).digest("hex");
995
+ if (actual !== manifest.checksum) {
996
+ throw new Error("Snapshot is corrupted: checksum mismatch.");
997
+ }
998
+ return db.transaction(async (tx) => {
999
+ let existing = 0;
1000
+ for (const table of SNAPSHOT_TABLES) {
1001
+ existing += await countRows(tx, table);
1002
+ }
1003
+ if (existing > 0 && !options.force) {
1004
+ throw new Error(
1005
+ `Target database already holds ${existing} row(s). Pass force to overwrite it.`
1006
+ );
1007
+ }
1008
+ if (options.force) {
1009
+ for (const table of [...SNAPSHOT_TABLES].reverse()) {
1010
+ await tx.exec(`DELETE FROM ${table};`);
1011
+ }
1012
+ }
1013
+ if (body.trim().length > 0) {
1014
+ await tx.exec(body);
1015
+ }
1016
+ const tables = {};
1017
+ let total = 0;
1018
+ for (const table of SNAPSHOT_TABLES) {
1019
+ const count2 = await countRows(tx, table);
1020
+ tables[table] = count2;
1021
+ total += count2;
1022
+ const expected = manifest.tables[table] ?? 0;
1023
+ if (count2 !== expected) {
1024
+ throw new Error(
1025
+ `Restore verification failed for ${table}: expected ${expected} row(s), found ${count2}.`
1026
+ );
1027
+ }
1028
+ }
1029
+ return { tables, total };
1030
+ });
476
1031
  }
477
1032
 
478
1033
  // src/kernel/goals/engine.ts
@@ -670,14 +1225,14 @@ function parseRow(row) {
670
1225
  token_slugs: JSON.parse(row.token_slugs)
671
1226
  };
672
1227
  }
673
- function createAgentSkill(db, input) {
674
- const existing = db.prepare("SELECT * FROM agent_skills WHERE slug = ?").get(input.slug);
1228
+ async function createAgentSkill(db, input) {
1229
+ const existing = await db.prepare("SELECT * FROM agent_skills WHERE slug = ?").get(input.slug);
675
1230
  if (existing) {
676
1231
  throw new Error(`Agent skill already exists: ${input.slug}`);
677
1232
  }
678
1233
  const id = ulid();
679
1234
  const now = (/* @__PURE__ */ new Date()).toISOString();
680
- db.prepare(
1235
+ await db.prepare(
681
1236
  `INSERT INTO agent_skills (id, slug, description, steps, token_slugs, source, created_at, updated_at)
682
1237
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
683
1238
  ).run(
@@ -691,38 +1246,38 @@ function createAgentSkill(db, input) {
691
1246
  now
692
1247
  );
693
1248
  return parseRow(
694
- db.prepare("SELECT * FROM agent_skills WHERE id = ?").get(id)
1249
+ await db.prepare("SELECT * FROM agent_skills WHERE id = ?").get(id)
695
1250
  );
696
1251
  }
697
- function getAgentSkill(db, slug) {
698
- const row = db.prepare("SELECT * FROM agent_skills WHERE slug = ?").get(slug);
1252
+ async function getAgentSkill(db, slug) {
1253
+ const row = await db.prepare("SELECT * FROM agent_skills WHERE slug = ?").get(slug);
699
1254
  return row ? parseRow(row) : void 0;
700
1255
  }
701
- function listAgentSkills(db) {
702
- const rows = db.prepare("SELECT * FROM agent_skills ORDER BY created_at ASC").all();
1256
+ async function listAgentSkills(db) {
1257
+ const rows = await db.prepare("SELECT * FROM agent_skills ORDER BY created_at ASC").all();
703
1258
  return rows.map(parseRow);
704
1259
  }
705
1260
 
706
1261
  // src/kernel/models/card.ts
707
1262
  import { ulid as ulid2 } from "ulid";
708
- function ensureCard(db, tokenId, userId) {
709
- const existing = db.prepare("SELECT * FROM cards WHERE token_id = ? AND user_id = ?").get(tokenId, userId);
1263
+ async function ensureCard(db, tokenId, userId) {
1264
+ const existing = await db.prepare("SELECT * FROM cards WHERE token_id = ? AND user_id = ?").get(tokenId, userId);
710
1265
  if (existing) return existing;
711
1266
  const id = ulid2();
712
1267
  const now = (/* @__PURE__ */ new Date()).toISOString();
713
- db.prepare(
1268
+ await db.prepare(
714
1269
  `INSERT INTO cards (id, token_id, user_id, due_at)
715
1270
  VALUES (?, ?, ?, ?)`
716
1271
  ).run(id, tokenId, userId, now);
717
- return db.prepare("SELECT * FROM cards WHERE id = ?").get(id);
1272
+ return await db.prepare("SELECT * FROM cards WHERE id = ?").get(id);
718
1273
  }
719
- function getCard(db, tokenId, userId) {
720
- return db.prepare("SELECT * FROM cards WHERE token_id = ? AND user_id = ?").get(tokenId, userId);
1274
+ async function getCard(db, tokenId, userId) {
1275
+ return await db.prepare("SELECT * FROM cards WHERE token_id = ? AND user_id = ?").get(tokenId, userId);
721
1276
  }
722
- function getCardById(db, cardId) {
723
- return db.prepare("SELECT * FROM cards WHERE id = ?").get(cardId);
1277
+ async function getCardById(db, cardId) {
1278
+ return await db.prepare("SELECT * FROM cards WHERE id = ?").get(cardId);
724
1279
  }
725
- function updateCard(db, cardId, updates) {
1280
+ async function updateCard(db, cardId, updates) {
726
1281
  const fields = [];
727
1282
  const values = [];
728
1283
  if (updates.stability !== void 0) {
@@ -769,32 +1324,32 @@ function updateCard(db, cardId, updates) {
769
1324
  throw new Error("updateCard called with no fields to update");
770
1325
  }
771
1326
  values.push(cardId);
772
- const result = db.prepare(`UPDATE cards SET ${fields.join(", ")} WHERE id = ?`).run(...values);
1327
+ const result = await db.prepare(`UPDATE cards SET ${fields.join(", ")} WHERE id = ?`).run(...values);
773
1328
  if (result.changes === 0) {
774
1329
  throw new Error(`Card not found: ${cardId}`);
775
1330
  }
776
- return db.prepare("SELECT * FROM cards WHERE id = ?").get(cardId);
1331
+ return await db.prepare("SELECT * FROM cards WHERE id = ?").get(cardId);
777
1332
  }
778
- function getCardDeletionImpact(db, tokenId, userId) {
779
- const card = getCard(db, tokenId, userId);
1333
+ async function getCardDeletionImpact(db, tokenId, userId) {
1334
+ const card = await getCard(db, tokenId, userId);
780
1335
  if (!card) {
781
1336
  throw new Error(`Card not found for token ${tokenId} and user ${userId}`);
782
1337
  }
783
- const reviewLogs = db.prepare("SELECT COUNT(*) AS n FROM review_logs WHERE card_id = ?").get(card.id);
1338
+ const reviewLogs = await db.prepare("SELECT COUNT(*) AS n FROM review_logs WHERE card_id = ?").get(card.id);
784
1339
  return { review_logs: reviewLogs.n };
785
1340
  }
786
- function deleteCardForUser(db, tokenId, userId) {
787
- const card = getCard(db, tokenId, userId);
1341
+ async function deleteCardForUser(db, tokenId, userId) {
1342
+ const card = await getCard(db, tokenId, userId);
788
1343
  if (!card) {
789
1344
  throw new Error(`Card not found for token ${tokenId} and user ${userId}`);
790
1345
  }
791
- const impact = getCardDeletionImpact(db, tokenId, userId);
792
- db.prepare("DELETE FROM cards WHERE id = ?").run(card.id);
1346
+ const impact = await getCardDeletionImpact(db, tokenId, userId);
1347
+ await db.prepare("DELETE FROM cards WHERE id = ?").run(card.id);
793
1348
  return { card, impact };
794
1349
  }
795
- function getDueCards(db, userId, now) {
1350
+ async function getDueCards(db, userId, now) {
796
1351
  const cutoff = now ?? (/* @__PURE__ */ new Date()).toISOString();
797
- return db.prepare(
1352
+ return await db.prepare(
798
1353
  `SELECT c.*, t.slug, t.concept, t.domain, t.bloom_level
799
1354
  FROM cards c
800
1355
  JOIN tokens t ON t.id = c.token_id
@@ -802,8 +1357,8 @@ function getDueCards(db, userId, now) {
802
1357
  ORDER BY t.bloom_level ASC, c.due_at ASC`
803
1358
  ).all(userId, cutoff);
804
1359
  }
805
- function getBlockedCards(db, userId) {
806
- return db.prepare(
1360
+ async function getBlockedCards(db, userId) {
1361
+ return await db.prepare(
807
1362
  `SELECT c.*, t.slug, t.concept, t.domain, t.bloom_level
808
1363
  FROM cards c
809
1364
  JOIN tokens t ON t.id = c.token_id
@@ -812,253 +1367,43 @@ function getBlockedCards(db, userId) {
812
1367
  ).all(userId);
813
1368
  }
814
1369
 
815
- // src/kernel/models/prerequisite.ts
816
- function buildAncestorMap(db) {
817
- const rows = db.prepare("SELECT token_id, requires_id FROM prerequisites").all();
818
- const map = /* @__PURE__ */ new Map();
819
- for (const row of rows) {
820
- let ancestors = map.get(row.token_id);
821
- if (!ancestors) {
822
- ancestors = /* @__PURE__ */ new Set();
823
- map.set(row.token_id, ancestors);
824
- }
825
- ancestors.add(row.requires_id);
826
- }
827
- return map;
828
- }
829
- function wouldCreateCycle(db, tokenId, requiresId) {
830
- if (tokenId === requiresId) return true;
831
- const ancestors = buildAncestorMap(db);
832
- const visited = /* @__PURE__ */ new Set();
833
- const queue = [requiresId];
834
- while (queue.length > 0) {
835
- const current = queue.shift();
836
- if (current === tokenId) return true;
837
- if (visited.has(current)) continue;
838
- visited.add(current);
839
- const parents = ancestors.get(current);
840
- if (parents) {
841
- for (const parent of parents) {
842
- if (!visited.has(parent)) queue.push(parent);
843
- }
844
- }
845
- }
846
- return false;
847
- }
848
- function addPrerequisite(db, tokenId, requiresId) {
849
- if (tokenId === requiresId) {
850
- throw new Error("A token cannot be a prerequisite of itself");
851
- }
852
- if (wouldCreateCycle(db, tokenId, requiresId)) {
853
- throw new Error(
854
- `Cannot add prerequisite: would create a cycle. ${requiresId} already depends on ${tokenId} (directly or transitively).`
855
- );
856
- }
857
- db.prepare(
858
- "INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
859
- ).run(tokenId, requiresId);
860
- }
861
- function getPrerequisites(db, tokenId) {
862
- return db.prepare(
863
- `SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
864
- FROM prerequisites p
865
- JOIN tokens t ON t.id = p.requires_id
866
- WHERE p.token_id = ?`
867
- ).all(tokenId);
868
- }
869
- function getDependents(db, tokenId) {
870
- return db.prepare(
871
- `SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
872
- FROM prerequisites p
873
- JOIN tokens t ON t.id = p.token_id
874
- WHERE p.requires_id = ?`
875
- ).all(tokenId);
876
- }
877
-
878
- // src/kernel/models/review.ts
1370
+ // src/kernel/models/token.ts
879
1371
  import { ulid as ulid3 } from "ulid";
880
- function logReview(db, input) {
881
- if (input.rating < 1 || input.rating > 4) {
882
- throw new Error(`Rating must be between 1 and 4, got ${input.rating}`);
883
- }
1372
+ async function createToken(db, input) {
884
1373
  const id = ulid3();
885
1374
  const now = (/* @__PURE__ */ new Date()).toISOString();
886
- db.prepare(
887
- `INSERT INTO review_logs (id, card_id, token_id, user_id, rating, response_time_ms, reviewed_at, scheduled_at, session_id)
888
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
889
- ).run(
1375
+ const bloom = input.bloom_level ?? 1;
1376
+ if (bloom < 1 || bloom > 5) {
1377
+ throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
1378
+ }
1379
+ await db.prepare(`
1380
+ INSERT INTO tokens (id, slug, concept, domain, bloom_level, context, symbiosis_mode, source_link, question, created_at, updated_at)
1381
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1382
+ `).run(
890
1383
  id,
891
- input.card_id,
892
- input.token_id,
893
- input.user_id,
894
- input.rating,
895
- input.response_time_ms ?? null,
1384
+ input.slug,
1385
+ input.concept,
1386
+ input.domain ?? "",
1387
+ bloom,
1388
+ input.context ?? "",
1389
+ input.symbiosis_mode ?? null,
1390
+ input.source_link ?? null,
1391
+ input.question ?? null,
896
1392
  now,
897
- input.scheduled_at,
898
- input.session_id ?? null
1393
+ now
899
1394
  );
900
- return db.prepare("SELECT * FROM review_logs WHERE id = ?").get(id);
1395
+ return await getTokenById(db, id);
901
1396
  }
902
- function getReviewsForCard(db, cardId) {
903
- return db.prepare(
904
- "SELECT * FROM review_logs WHERE card_id = ? ORDER BY reviewed_at ASC"
905
- ).all(cardId);
1397
+ async function getTokenBySlug(db, slug) {
1398
+ return await db.prepare("SELECT * FROM tokens WHERE slug = ?").get(slug);
906
1399
  }
907
- function getReviewsForUser(db, userId, options) {
908
- const conditions = ["user_id = ?"];
909
- const params = [userId];
910
- if (options?.after) {
911
- conditions.push("reviewed_at > ?");
912
- params.push(options.after);
913
- }
914
- if (options?.before) {
915
- conditions.push("reviewed_at < ?");
916
- params.push(options.before);
917
- }
918
- let sql = `SELECT * FROM review_logs WHERE ${conditions.join(" AND ")} ORDER BY reviewed_at DESC`;
919
- if (options?.limit) {
920
- sql += " LIMIT ?";
921
- params.push(options.limit);
922
- }
923
- return db.prepare(sql).all(...params);
924
- }
925
-
926
- // src/kernel/models/session.ts
927
- import { ulid as ulid4 } from "ulid";
928
- function startSession(db, input) {
929
- const id = ulid4();
930
- const now = (/* @__PURE__ */ new Date()).toISOString();
931
- const ctx = input.execution_context ?? "shell";
932
- db.prepare(
933
- `INSERT INTO sessions (id, user_id, task, execution_context, started_at)
934
- VALUES (?, ?, ?, ?, ?)`
935
- ).run(id, input.user_id, input.task, ctx, now);
936
- return db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
937
- }
938
- function endSession(db, sessionId) {
939
- const session = db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
940
- if (!session) {
941
- throw new Error(`Session not found: ${sessionId}`);
942
- }
943
- if (session.completed_at) {
944
- throw new Error(`Session already completed: ${sessionId}`);
945
- }
946
- const now = (/* @__PURE__ */ new Date()).toISOString();
947
- db.prepare("UPDATE sessions SET completed_at = ? WHERE id = ?").run(
948
- now,
949
- sessionId
950
- );
951
- return db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
952
- }
953
- function logStep(db, input) {
954
- if (input.done_by !== "user" && input.done_by !== "agent") {
955
- throw new Error(
956
- `done_by must be 'user' or 'agent', got '${input.done_by}'`
957
- );
958
- }
959
- if (input.rating != null && (input.rating < 1 || input.rating > 4)) {
960
- throw new Error(`Rating must be between 1 and 4, got ${input.rating}`);
961
- }
962
- const session = db.prepare("SELECT id FROM sessions WHERE id = ?").get(input.session_id);
963
- if (!session) {
964
- throw new Error(`Session not found: ${input.session_id}`);
965
- }
966
- const id = ulid4();
967
- const now = (/* @__PURE__ */ new Date()).toISOString();
968
- db.prepare(
969
- `INSERT INTO session_steps (id, session_id, token_id, done_by, rating, notes, created_at)
970
- VALUES (?, ?, ?, ?, ?, ?, ?)`
971
- ).run(
972
- id,
973
- input.session_id,
974
- input.token_id,
975
- input.done_by,
976
- input.rating ?? null,
977
- input.notes ?? null,
978
- now
979
- );
980
- return db.prepare("SELECT * FROM session_steps WHERE id = ?").get(id);
981
- }
982
- function getSessionSummary(db, sessionId) {
983
- const session = db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
984
- if (!session) {
985
- throw new Error(`Session not found: ${sessionId}`);
986
- }
987
- const steps = db.prepare(
988
- `SELECT ss.*, t.slug, t.concept, t.domain, t.bloom_level
989
- FROM session_steps ss
990
- JOIN tokens t ON t.id = ss.token_id
991
- WHERE ss.session_id = ?
992
- ORDER BY ss.created_at ASC`
993
- ).all(sessionId);
994
- return { session, steps };
995
- }
996
-
997
- // src/kernel/models/settings.ts
998
- function getSetting(db, key) {
999
- const row = db.prepare("SELECT value FROM user_config WHERE key = ?").get(key);
1000
- return row?.value;
1001
- }
1002
- function getAllSettings(db) {
1003
- const rows = db.prepare("SELECT key, value FROM user_config ORDER BY key").all();
1004
- const map = {};
1005
- for (const row of rows) {
1006
- map[row.key] = row.value;
1007
- }
1008
- return map;
1009
- }
1010
- function getAllSettingsDetailed(db) {
1011
- return db.prepare("SELECT key, value, updated_at FROM user_config ORDER BY key").all();
1012
- }
1013
- function setSetting(db, key, value) {
1014
- db.prepare(
1015
- `INSERT INTO user_config (key, value, updated_at)
1016
- VALUES (?, ?, datetime('now'))
1017
- ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
1018
- ).run(key, value);
1019
- }
1020
- function deleteSetting(db, key) {
1021
- const result = db.prepare("DELETE FROM user_config WHERE key = ?").run(key);
1022
- return result.changes > 0;
1023
- }
1024
-
1025
- // src/kernel/models/token.ts
1026
- import { ulid as ulid5 } from "ulid";
1027
- function createToken(db, input) {
1028
- const id = ulid5();
1029
- const now = (/* @__PURE__ */ new Date()).toISOString();
1030
- const bloom = input.bloom_level ?? 1;
1031
- if (bloom < 1 || bloom > 5) {
1032
- throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
1033
- }
1034
- db.prepare(`
1035
- INSERT INTO tokens (id, slug, concept, domain, bloom_level, context, symbiosis_mode, source_link, question, created_at, updated_at)
1036
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1037
- `).run(
1038
- id,
1039
- input.slug,
1040
- input.concept,
1041
- input.domain ?? "",
1042
- bloom,
1043
- input.context ?? "",
1044
- input.symbiosis_mode ?? null,
1045
- input.source_link ?? null,
1046
- input.question ?? null,
1047
- now,
1048
- now
1049
- );
1050
- return getTokenById(db, id);
1051
- }
1052
- function getTokenBySlug(db, slug) {
1053
- return db.prepare("SELECT * FROM tokens WHERE slug = ?").get(slug);
1054
- }
1055
- function getTokenById(db, id) {
1056
- return db.prepare("SELECT * FROM tokens WHERE id = ?").get(id);
1057
- }
1058
- function updateToken(db, slug, updates) {
1059
- const token = getTokenBySlug(db, slug);
1060
- if (!token) {
1061
- throw new Error(`Token not found: ${slug}`);
1400
+ async function getTokenById(db, id) {
1401
+ return await db.prepare("SELECT * FROM tokens WHERE id = ?").get(id);
1402
+ }
1403
+ async function updateToken(db, slug, updates) {
1404
+ const token = await getTokenBySlug(db, slug);
1405
+ if (!token) {
1406
+ throw new Error(`Token not found: ${slug}`);
1062
1407
  }
1063
1408
  const fields = [];
1064
1409
  const values = [];
@@ -1105,13 +1450,11 @@ function updateToken(db, slug, updates) {
1105
1450
  fields.push("updated_at = ?");
1106
1451
  values.push((/* @__PURE__ */ new Date()).toISOString());
1107
1452
  values.push(slug);
1108
- db.prepare(`UPDATE tokens SET ${fields.join(", ")} WHERE slug = ?`).run(
1109
- ...values
1110
- );
1111
- return getTokenBySlug(db, slug);
1453
+ await db.prepare(`UPDATE tokens SET ${fields.join(", ")} WHERE slug = ?`).run(...values);
1454
+ return await getTokenBySlug(db, slug);
1112
1455
  }
1113
- function deprecateToken(db, slug) {
1114
- const token = getTokenBySlug(db, slug);
1456
+ async function deprecateToken(db, slug) {
1457
+ const token = await getTokenBySlug(db, slug);
1115
1458
  if (!token) {
1116
1459
  throw new Error(`Token not found: ${slug}`);
1117
1460
  }
@@ -1119,25 +1462,25 @@ function deprecateToken(db, slug) {
1119
1462
  throw new Error(`Token already deprecated: ${slug}`);
1120
1463
  }
1121
1464
  const now = (/* @__PURE__ */ new Date()).toISOString();
1122
- db.prepare(
1465
+ await db.prepare(
1123
1466
  "UPDATE tokens SET deprecated_at = ?, updated_at = ? WHERE slug = ?"
1124
1467
  ).run(now, now, slug);
1125
- return getTokenBySlug(db, slug);
1468
+ return await getTokenBySlug(db, slug);
1126
1469
  }
1127
- function getTokenDeleteImpact(db, slug) {
1128
- const token = getTokenBySlug(db, slug);
1470
+ async function getTokenDeleteImpact(db, slug) {
1471
+ const token = await getTokenBySlug(db, slug);
1129
1472
  if (!token) {
1130
1473
  throw new Error(`Token not found: ${slug}`);
1131
1474
  }
1132
- const cards = db.prepare("SELECT COUNT(*) AS n FROM cards WHERE token_id = ?").get(token.id);
1133
- const reviewLogs = db.prepare("SELECT COUNT(*) AS n FROM review_logs WHERE token_id = ?").get(token.id);
1134
- const prereqsFrom = db.prepare("SELECT COUNT(*) AS n FROM prerequisites WHERE token_id = ?").get(token.id);
1135
- const prereqsTo = db.prepare("SELECT COUNT(*) AS n FROM prerequisites WHERE requires_id = ?").get(token.id);
1136
- const sessionSteps = db.prepare("SELECT COUNT(*) AS n FROM session_steps WHERE token_id = ?").get(token.id);
1137
- const sessionsTouched = db.prepare(
1475
+ const cards = await db.prepare("SELECT COUNT(*) AS n FROM cards WHERE token_id = ?").get(token.id);
1476
+ const reviewLogs = await db.prepare("SELECT COUNT(*) AS n FROM review_logs WHERE token_id = ?").get(token.id);
1477
+ const prereqsFrom = await db.prepare("SELECT COUNT(*) AS n FROM prerequisites WHERE token_id = ?").get(token.id);
1478
+ const prereqsTo = await db.prepare("SELECT COUNT(*) AS n FROM prerequisites WHERE requires_id = ?").get(token.id);
1479
+ const sessionSteps = await db.prepare("SELECT COUNT(*) AS n FROM session_steps WHERE token_id = ?").get(token.id);
1480
+ const sessionsTouched = await db.prepare(
1138
1481
  "SELECT COUNT(DISTINCT session_id) AS n FROM session_steps WHERE token_id = ?"
1139
1482
  ).get(token.id);
1140
- const skillRows = db.prepare("SELECT token_slugs FROM agent_skills").all();
1483
+ const skillRows = await db.prepare("SELECT token_slugs FROM agent_skills").all();
1141
1484
  const agentSkills = skillRows.filter((row) => {
1142
1485
  const tokenSlugs = JSON.parse(row.token_slugs);
1143
1486
  return tokenSlugs.includes(slug);
@@ -1152,34 +1495,29 @@ function getTokenDeleteImpact(db, slug) {
1152
1495
  agent_skills: agentSkills
1153
1496
  };
1154
1497
  }
1155
- function deleteToken(db, slug) {
1156
- const token = getTokenBySlug(db, slug);
1498
+ async function deleteToken(db, slug) {
1499
+ const token = await getTokenBySlug(db, slug);
1157
1500
  if (!token) {
1158
1501
  throw new Error(`Token not found: ${slug}`);
1159
1502
  }
1160
- const impact = getTokenDeleteImpact(db, slug);
1161
- db.exec("BEGIN");
1162
- try {
1503
+ const impact = await getTokenDeleteImpact(db, slug);
1504
+ await db.transaction(async (tx) => {
1163
1505
  const now = (/* @__PURE__ */ new Date()).toISOString();
1164
- const skillRows = db.prepare("SELECT id, token_slugs FROM agent_skills").all();
1506
+ const skillRows = await tx.prepare("SELECT id, token_slugs FROM agent_skills").all();
1165
1507
  for (const row of skillRows) {
1166
1508
  const tokenSlugs = JSON.parse(row.token_slugs);
1167
1509
  const filtered = tokenSlugs.filter((tokenSlug) => tokenSlug !== slug);
1168
1510
  if (filtered.length !== tokenSlugs.length) {
1169
- db.prepare(
1511
+ await tx.prepare(
1170
1512
  "UPDATE agent_skills SET token_slugs = ?, updated_at = ? WHERE id = ?"
1171
1513
  ).run(JSON.stringify(filtered), now, row.id);
1172
1514
  }
1173
1515
  }
1174
- db.prepare("DELETE FROM tokens WHERE id = ?").run(token.id);
1175
- db.exec("COMMIT");
1176
- } catch (err) {
1177
- db.exec("ROLLBACK");
1178
- throw err;
1179
- }
1516
+ await tx.prepare("DELETE FROM tokens WHERE id = ?").run(token.id);
1517
+ });
1180
1518
  return { token, impact };
1181
1519
  }
1182
- function findTokens(db, query) {
1520
+ async function findTokens(db, query) {
1183
1521
  const normalised = query.toLowerCase();
1184
1522
  const searchTokens = normalised.split(/[\s,.\-_/\\:;!?()[\]{}]+/).filter((t2) => t2.length > 0);
1185
1523
  if (searchTokens.length === 0) return [];
@@ -1189,7 +1527,7 @@ function findTokens(db, query) {
1189
1527
  const likeSQL = `SELECT * FROM tokens WHERE deprecated_at IS NULL AND (lower(slug) LIKE ? OR lower(concept) LIKE ? OR lower(domain) LIKE ?)`;
1190
1528
  for (const term of longTerms) {
1191
1529
  const pattern = `%${term}%`;
1192
- const rows = db.prepare(likeSQL).all(pattern, pattern, pattern);
1530
+ const rows = await db.prepare(likeSQL).all(pattern, pattern, pattern);
1193
1531
  for (const row of rows) {
1194
1532
  const entry = scoreMap.get(row.id);
1195
1533
  if (entry) {
@@ -1200,7 +1538,7 @@ function findTokens(db, query) {
1200
1538
  }
1201
1539
  }
1202
1540
  if (shortTerms.length > 0 || longTerms.length === 0) {
1203
- const allTokens = db.prepare("SELECT * FROM tokens WHERE deprecated_at IS NULL").all();
1541
+ const allTokens = await db.prepare("SELECT * FROM tokens WHERE deprecated_at IS NULL").all();
1204
1542
  for (const token of allTokens) {
1205
1543
  const words = `${token.slug} ${token.concept} ${token.domain}`.toLowerCase().split(/[\s,.\-_/\\:;!?()[\]{}]+/).filter(Boolean);
1206
1544
  let matchCount = 0;
@@ -1230,96 +1568,368 @@ function findTokens(db, query) {
1230
1568
  scored.sort((a, b) => b.score - a.score);
1231
1569
  return scored;
1232
1570
  }
1233
- function listTokens(db, options) {
1571
+ async function listTokens(db, options) {
1234
1572
  if (options?.domain) {
1235
- return db.prepare(
1573
+ return await db.prepare(
1236
1574
  "SELECT * FROM tokens WHERE domain = ? AND deprecated_at IS NULL ORDER BY bloom_level, slug"
1237
1575
  ).all(options.domain);
1238
1576
  }
1239
- return db.prepare(
1577
+ return await db.prepare(
1240
1578
  "SELECT * FROM tokens WHERE deprecated_at IS NULL ORDER BY bloom_level, domain, slug"
1241
1579
  ).all();
1242
1580
  }
1243
1581
 
1244
- // src/kernel/observation/analyzer.ts
1245
- function parseMonitorLog(jsonl) {
1246
- const events = [];
1247
- for (const line of jsonl.split("\n")) {
1248
- const trimmed = line.trim();
1249
- if (!trimmed) continue;
1250
- try {
1251
- events.push(JSON.parse(trimmed));
1252
- } catch {
1582
+ // src/kernel/models/prerequisite.ts
1583
+ async function buildAncestorMap(db) {
1584
+ const rows = await db.prepare("SELECT token_id, requires_id FROM prerequisites").all();
1585
+ const map = /* @__PURE__ */ new Map();
1586
+ for (const row of rows) {
1587
+ let ancestors = map.get(row.token_id);
1588
+ if (!ancestors) {
1589
+ ancestors = /* @__PURE__ */ new Set();
1590
+ map.set(row.token_id, ancestors);
1253
1591
  }
1592
+ ancestors.add(row.requires_id);
1254
1593
  }
1255
- return events;
1594
+ return map;
1256
1595
  }
1257
- function pairCommands(events) {
1258
- const starts = /* @__PURE__ */ new Map();
1259
- const records = [];
1260
- for (const e of events) {
1261
- if (e.type === "command_start" && e.seq != null) {
1262
- const key = `${e.pid ?? 0}:${e.seq}`;
1263
- starts.set(key, e);
1264
- } else if (e.type === "command_end" && e.seq != null) {
1265
- const key = `${e.pid ?? 0}:${e.seq}`;
1266
- const start = starts.get(key);
1267
- if (start) {
1268
- const startMs = new Date(start.ts).getTime();
1269
- const endMs = new Date(e.ts).getTime();
1270
- records.push({
1271
- seq: e.seq,
1272
- pid: e.pid ?? 0,
1273
- command: start.command ?? "",
1274
- cwd: start.cwd ?? "",
1275
- startedAt: start.ts,
1276
- endedAt: e.ts,
1277
- durationMs: endMs - startMs,
1278
- exitCode: e.exit_code ?? null
1279
- });
1280
- starts.delete(key);
1596
+ async function wouldCreateCycle(db, tokenId, requiresId) {
1597
+ if (tokenId === requiresId) return true;
1598
+ const ancestors = await buildAncestorMap(db);
1599
+ const visited = /* @__PURE__ */ new Set();
1600
+ const queue = [requiresId];
1601
+ while (queue.length > 0) {
1602
+ const current = queue.shift();
1603
+ if (current === tokenId) return true;
1604
+ if (visited.has(current)) continue;
1605
+ visited.add(current);
1606
+ const parents = ancestors.get(current);
1607
+ if (parents) {
1608
+ for (const parent of parents) {
1609
+ if (!visited.has(parent)) queue.push(parent);
1281
1610
  }
1282
1611
  }
1283
1612
  }
1284
- for (const [, start] of starts) {
1285
- records.push({
1286
- seq: start.seq ?? 0,
1287
- pid: start.pid ?? 0,
1288
- command: start.command ?? "",
1289
- cwd: start.cwd ?? "",
1290
- startedAt: start.ts,
1291
- endedAt: null,
1292
- durationMs: null,
1293
- exitCode: null
1294
- });
1613
+ return false;
1614
+ }
1615
+ async function addPrerequisite(db, tokenId, requiresId) {
1616
+ if (tokenId === requiresId) {
1617
+ throw new Error("A token cannot be a prerequisite of itself");
1295
1618
  }
1296
- records.sort(
1297
- (a, b) => new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime()
1298
- );
1299
- return records;
1619
+ if (await wouldCreateCycle(db, tokenId, requiresId)) {
1620
+ throw new Error(
1621
+ `Cannot add prerequisite: would create a cycle. ${requiresId} already depends on ${tokenId} (directly or transitively).`
1622
+ );
1623
+ }
1624
+ await db.prepare(
1625
+ "INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
1626
+ ).run(tokenId, requiresId);
1300
1627
  }
1301
- var HELP_PATTERNS = ["--help", "man ", "tldr ", "help "];
1302
- var HELP_WINDOW_MS = 6e4;
1303
- function matchesToken(command, patterns) {
1304
- const lower = command.toLowerCase();
1305
- return patterns.some((p) => lower.includes(p.toLowerCase()));
1628
+ async function getPrerequisites(db, tokenId) {
1629
+ return await db.prepare(
1630
+ `SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
1631
+ FROM prerequisites p
1632
+ JOIN tokens t ON t.id = p.requires_id
1633
+ WHERE p.token_id = ?`
1634
+ ).all(tokenId);
1306
1635
  }
1307
- function isHelpCommand(command) {
1308
- const lower = command.toLowerCase();
1309
- return HELP_PATTERNS.some((p) => lower.includes(p));
1636
+ async function getDependents(db, tokenId) {
1637
+ return await db.prepare(
1638
+ `SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
1639
+ FROM prerequisites p
1640
+ JOIN tokens t ON t.id = p.token_id
1641
+ WHERE p.requires_id = ?`
1642
+ ).all(tokenId);
1310
1643
  }
1311
- function commandPrefix(command) {
1312
- return command.split(/\s+/).slice(0, 2).join(" ").toLowerCase();
1644
+ async function getTokenNeighborhood(db, tokenId, userId) {
1645
+ const token = await getTokenById(db, tokenId);
1646
+ if (!token) {
1647
+ throw new Error(`Token not found: ${tokenId}`);
1648
+ }
1649
+ const centerCard = userId ? await getCard(db, tokenId, userId) : void 0;
1650
+ const prereqRows = await getPrerequisites(db, tokenId);
1651
+ const depRows = await getDependents(db, tokenId);
1652
+ const relatedTokenIds = /* @__PURE__ */ new Set();
1653
+ for (const p of prereqRows) relatedTokenIds.add(p.requires_id);
1654
+ for (const d of depRows) relatedTokenIds.add(d.token_id);
1655
+ const cardMap = /* @__PURE__ */ new Map();
1656
+ if (userId && relatedTokenIds.size > 0) {
1657
+ const ids = Array.from(relatedTokenIds);
1658
+ const placeholders = ids.map(() => "?").join(",");
1659
+ const rows = await db.prepare(
1660
+ `SELECT * FROM cards WHERE token_id IN (${placeholders}) AND user_id = ?`
1661
+ ).all(...ids, userId);
1662
+ for (const row of rows) {
1663
+ cardMap.set(row.token_id, row);
1664
+ }
1665
+ }
1666
+ const toNode = (t2, card) => ({
1667
+ id: t2.id,
1668
+ slug: t2.slug,
1669
+ concept: t2.concept,
1670
+ domain: t2.domain,
1671
+ bloom_level: t2.bloom_level,
1672
+ card: card ? {
1673
+ state: card.state,
1674
+ reps: card.reps,
1675
+ stability: card.stability,
1676
+ difficulty: card.difficulty,
1677
+ blocked: card.blocked === 1,
1678
+ due_at: card.due_at,
1679
+ last_review_at: card.last_review_at
1680
+ } : null
1681
+ });
1682
+ const center = toNode(token, centerCard);
1683
+ const prerequisites = prereqRows.map(
1684
+ (p) => toNode(
1685
+ {
1686
+ id: p.requires_id,
1687
+ slug: p.slug,
1688
+ concept: p.concept,
1689
+ domain: p.domain,
1690
+ bloom_level: p.bloom_level
1691
+ },
1692
+ cardMap.get(p.requires_id)
1693
+ )
1694
+ );
1695
+ const dependents = depRows.map(
1696
+ (d) => toNode(
1697
+ {
1698
+ id: d.token_id,
1699
+ slug: d.slug,
1700
+ concept: d.concept,
1701
+ domain: d.domain,
1702
+ bloom_level: d.bloom_level
1703
+ },
1704
+ cardMap.get(d.token_id)
1705
+ )
1706
+ );
1707
+ return { center, prerequisites, dependents };
1313
1708
  }
1314
- function computeMedian(values) {
1315
- if (values.length === 0) return null;
1316
- const sorted = [...values].sort((a, b) => a - b);
1317
- const mid = Math.floor(sorted.length / 2);
1318
- return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
1709
+
1710
+ // src/kernel/models/review.ts
1711
+ import { ulid as ulid4 } from "ulid";
1712
+ async function logReview(db, input) {
1713
+ if (input.rating < 1 || input.rating > 4) {
1714
+ throw new Error(`Rating must be between 1 and 4, got ${input.rating}`);
1715
+ }
1716
+ const id = ulid4();
1717
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1718
+ await db.prepare(
1719
+ `INSERT INTO review_logs (id, card_id, token_id, user_id, rating, response_time_ms, reviewed_at, scheduled_at, session_id)
1720
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
1721
+ ).run(
1722
+ id,
1723
+ input.card_id,
1724
+ input.token_id,
1725
+ input.user_id,
1726
+ input.rating,
1727
+ input.response_time_ms ?? null,
1728
+ now,
1729
+ input.scheduled_at,
1730
+ input.session_id ?? null
1731
+ );
1732
+ return await db.prepare("SELECT * FROM review_logs WHERE id = ?").get(id);
1319
1733
  }
1320
- function analyzeObservation(commands, tokenPatterns) {
1321
- const matchedSet = /* @__PURE__ */ new Set();
1322
- const ratings = [];
1734
+ async function getReviewsForCard(db, cardId) {
1735
+ return await db.prepare(
1736
+ "SELECT * FROM review_logs WHERE card_id = ? ORDER BY reviewed_at ASC"
1737
+ ).all(cardId);
1738
+ }
1739
+ async function getReviewsForUser(db, userId, options) {
1740
+ const conditions = ["user_id = ?"];
1741
+ const params = [userId];
1742
+ if (options?.after) {
1743
+ conditions.push("reviewed_at > ?");
1744
+ params.push(options.after);
1745
+ }
1746
+ if (options?.before) {
1747
+ conditions.push("reviewed_at < ?");
1748
+ params.push(options.before);
1749
+ }
1750
+ let sql = `SELECT * FROM review_logs WHERE ${conditions.join(" AND ")} ORDER BY reviewed_at DESC`;
1751
+ if (options?.limit) {
1752
+ sql += " LIMIT ?";
1753
+ params.push(options.limit);
1754
+ }
1755
+ return await db.prepare(sql).all(...params);
1756
+ }
1757
+
1758
+ // src/kernel/models/session.ts
1759
+ import { ulid as ulid5 } from "ulid";
1760
+ async function startSession(db, input) {
1761
+ const id = ulid5();
1762
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1763
+ const ctx = input.execution_context ?? "shell";
1764
+ await db.prepare(
1765
+ `INSERT INTO sessions (id, user_id, task, execution_context, started_at)
1766
+ VALUES (?, ?, ?, ?, ?)`
1767
+ ).run(id, input.user_id, input.task, ctx, now);
1768
+ return await db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
1769
+ }
1770
+ async function endSession(db, sessionId) {
1771
+ const session = await db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
1772
+ if (!session) {
1773
+ throw new Error(`Session not found: ${sessionId}`);
1774
+ }
1775
+ if (session.completed_at) {
1776
+ throw new Error(`Session already completed: ${sessionId}`);
1777
+ }
1778
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1779
+ await db.prepare("UPDATE sessions SET completed_at = ? WHERE id = ?").run(now, sessionId);
1780
+ return await db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
1781
+ }
1782
+ async function logStep(db, input) {
1783
+ if (input.done_by !== "user" && input.done_by !== "agent") {
1784
+ throw new Error(
1785
+ `done_by must be 'user' or 'agent', got '${input.done_by}'`
1786
+ );
1787
+ }
1788
+ if (input.rating != null && (input.rating < 1 || input.rating > 4)) {
1789
+ throw new Error(`Rating must be between 1 and 4, got ${input.rating}`);
1790
+ }
1791
+ const session = await db.prepare("SELECT id FROM sessions WHERE id = ?").get(input.session_id);
1792
+ if (!session) {
1793
+ throw new Error(`Session not found: ${input.session_id}`);
1794
+ }
1795
+ const id = ulid5();
1796
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1797
+ await db.prepare(
1798
+ `INSERT INTO session_steps (id, session_id, token_id, done_by, rating, notes, created_at)
1799
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
1800
+ ).run(
1801
+ id,
1802
+ input.session_id,
1803
+ input.token_id,
1804
+ input.done_by,
1805
+ input.rating ?? null,
1806
+ input.notes ?? null,
1807
+ now
1808
+ );
1809
+ return await db.prepare("SELECT * FROM session_steps WHERE id = ?").get(id);
1810
+ }
1811
+ async function getSessionSummary(db, sessionId) {
1812
+ const session = await db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
1813
+ if (!session) {
1814
+ throw new Error(`Session not found: ${sessionId}`);
1815
+ }
1816
+ const steps = await db.prepare(
1817
+ `SELECT ss.*, t.slug, t.concept, t.domain, t.bloom_level
1818
+ FROM session_steps ss
1819
+ JOIN tokens t ON t.id = ss.token_id
1820
+ WHERE ss.session_id = ?
1821
+ ORDER BY ss.created_at ASC`
1822
+ ).all(sessionId);
1823
+ return { session, steps };
1824
+ }
1825
+
1826
+ // src/kernel/models/settings.ts
1827
+ async function getSetting(db, key) {
1828
+ const row = await db.prepare("SELECT value FROM user_config WHERE key = ?").get(key);
1829
+ return row?.value;
1830
+ }
1831
+ async function getAllSettings(db) {
1832
+ const rows = await db.prepare("SELECT key, value FROM user_config ORDER BY key").all();
1833
+ const map = {};
1834
+ for (const row of rows) {
1835
+ map[row.key] = row.value;
1836
+ }
1837
+ return map;
1838
+ }
1839
+ async function getAllSettingsDetailed(db) {
1840
+ return await db.prepare("SELECT key, value, updated_at FROM user_config ORDER BY key").all();
1841
+ }
1842
+ async function setSetting(db, key, value) {
1843
+ await db.prepare(
1844
+ `INSERT INTO user_config (key, value, updated_at)
1845
+ VALUES (?, ?, datetime('now'))
1846
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
1847
+ ).run(key, value);
1848
+ }
1849
+ async function deleteSetting(db, key) {
1850
+ const result = await db.prepare("DELETE FROM user_config WHERE key = ?").run(key);
1851
+ return result.changes > 0;
1852
+ }
1853
+
1854
+ // src/kernel/observation/analyzer.ts
1855
+ function parseMonitorLog(jsonl) {
1856
+ const events = [];
1857
+ for (const line of jsonl.split("\n")) {
1858
+ const trimmed = line.trim();
1859
+ if (!trimmed) continue;
1860
+ try {
1861
+ events.push(JSON.parse(trimmed));
1862
+ } catch {
1863
+ }
1864
+ }
1865
+ return events;
1866
+ }
1867
+ function pairCommands(events) {
1868
+ const starts = /* @__PURE__ */ new Map();
1869
+ const records = [];
1870
+ for (const e of events) {
1871
+ if (e.type === "command_start" && e.seq != null) {
1872
+ const key = `${e.pid ?? 0}:${e.seq}`;
1873
+ starts.set(key, e);
1874
+ } else if (e.type === "command_end" && e.seq != null) {
1875
+ const key = `${e.pid ?? 0}:${e.seq}`;
1876
+ const start = starts.get(key);
1877
+ if (start) {
1878
+ const startMs = new Date(start.ts).getTime();
1879
+ const endMs = new Date(e.ts).getTime();
1880
+ records.push({
1881
+ seq: e.seq,
1882
+ pid: e.pid ?? 0,
1883
+ command: start.command ?? "",
1884
+ cwd: start.cwd ?? "",
1885
+ startedAt: start.ts,
1886
+ endedAt: e.ts,
1887
+ durationMs: endMs - startMs,
1888
+ exitCode: e.exit_code ?? null
1889
+ });
1890
+ starts.delete(key);
1891
+ }
1892
+ }
1893
+ }
1894
+ for (const [, start] of starts) {
1895
+ records.push({
1896
+ seq: start.seq ?? 0,
1897
+ pid: start.pid ?? 0,
1898
+ command: start.command ?? "",
1899
+ cwd: start.cwd ?? "",
1900
+ startedAt: start.ts,
1901
+ endedAt: null,
1902
+ durationMs: null,
1903
+ exitCode: null
1904
+ });
1905
+ }
1906
+ records.sort(
1907
+ (a, b) => new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime()
1908
+ );
1909
+ return records;
1910
+ }
1911
+ var HELP_PATTERNS = ["--help", "man ", "tldr ", "help "];
1912
+ var HELP_WINDOW_MS = 6e4;
1913
+ function matchesToken(command, patterns) {
1914
+ const lower = command.toLowerCase();
1915
+ return patterns.some((p) => lower.includes(p.toLowerCase()));
1916
+ }
1917
+ function isHelpCommand(command) {
1918
+ const lower = command.toLowerCase();
1919
+ return HELP_PATTERNS.some((p) => lower.includes(p));
1920
+ }
1921
+ function commandPrefix(command) {
1922
+ return command.split(/\s+/).slice(0, 2).join(" ").toLowerCase();
1923
+ }
1924
+ function computeMedian(values) {
1925
+ if (values.length === 0) return null;
1926
+ const sorted = [...values].sort((a, b) => a - b);
1927
+ const mid = Math.floor(sorted.length / 2);
1928
+ return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
1929
+ }
1930
+ function analyzeObservation(commands, tokenPatterns) {
1931
+ const matchedSet = /* @__PURE__ */ new Set();
1932
+ const ratings = [];
1323
1933
  for (const tp of tokenPatterns) {
1324
1934
  const matchIndices = [];
1325
1935
  const matchedTexts = [];
@@ -1374,8 +1984,8 @@ function analyzeObservation(commands, tokenPatterns) {
1374
1984
  const prefix = commandPrefix(commands[mi].command);
1375
1985
  prefixGroups.set(prefix, (prefixGroups.get(prefix) ?? 0) + 1);
1376
1986
  }
1377
- for (const count of prefixGroups.values()) {
1378
- if (count > 1) selfCorrections += count - 1;
1987
+ for (const count2 of prefixGroups.values()) {
1988
+ if (count2 > 1) selfCorrections += count2 - 1;
1379
1989
  }
1380
1990
  const gaps = [];
1381
1991
  for (let k = 1; k < matchIndices.length; k++) {
@@ -1461,54 +2071,504 @@ function inferRating(signals) {
1461
2071
  return 4;
1462
2072
  }
1463
2073
 
1464
- // src/kernel/observation/monitor-io.ts
1465
- import {
1466
- appendFileSync,
1467
- existsSync as existsSync4,
1468
- mkdirSync as mkdirSync3,
1469
- readFileSync as readFileSync4,
1470
- statSync
1471
- } from "fs";
1472
- import { homedir as homedir3 } from "os";
1473
- import { join as join4 } from "path";
1474
- var MONITOR_DIR = join4(homedir3(), ".zam", "monitor");
1475
- function getMonitorDir() {
1476
- return MONITOR_DIR;
1477
- }
1478
- function getMonitorPath(sessionId) {
1479
- return join4(MONITOR_DIR, `${sessionId}.jsonl`);
2074
+ // src/kernel/observation/monitor-io.ts
2075
+ import {
2076
+ appendFileSync,
2077
+ existsSync as existsSync4,
2078
+ mkdirSync as mkdirSync3,
2079
+ readFileSync as readFileSync4,
2080
+ statSync
2081
+ } from "fs";
2082
+ import { homedir as homedir3 } from "os";
2083
+ import { join as join4 } from "path";
2084
+ var MONITOR_DIR = join4(homedir3(), ".zam", "monitor");
2085
+ function getMonitorDir() {
2086
+ return MONITOR_DIR;
2087
+ }
2088
+ function getMonitorPath(sessionId) {
2089
+ return join4(MONITOR_DIR, `${sessionId}.jsonl`);
2090
+ }
2091
+ function ensureMonitorDir() {
2092
+ if (!existsSync4(MONITOR_DIR)) {
2093
+ mkdirSync3(MONITOR_DIR, { recursive: true, mode: 448 });
2094
+ }
2095
+ }
2096
+ function writeMonitorEvent(sessionId, event) {
2097
+ ensureMonitorDir();
2098
+ const path = getMonitorPath(sessionId);
2099
+ appendFileSync(path, `${JSON.stringify(event)}
2100
+ `);
2101
+ }
2102
+ function readMonitorLog(sessionId) {
2103
+ const path = getMonitorPath(sessionId);
2104
+ if (!existsSync4(path)) {
2105
+ return [];
2106
+ }
2107
+ const content = readFileSync4(path, "utf-8");
2108
+ return parseMonitorLog(content);
2109
+ }
2110
+ function monitorLogExists(sessionId) {
2111
+ return existsSync4(getMonitorPath(sessionId));
2112
+ }
2113
+ function getMonitorLogStats(sessionId) {
2114
+ const path = getMonitorPath(sessionId);
2115
+ if (!existsSync4(path)) {
2116
+ return { exists: false, sizeBytes: 0, lineCount: 0 };
2117
+ }
2118
+ const stat = statSync(path);
2119
+ const content = readFileSync4(path, "utf-8");
2120
+ const lineCount = content.split("\n").filter((l) => l.trim()).length;
2121
+ return { exists: true, sizeBytes: stat.size, lineCount };
2122
+ }
2123
+
2124
+ // src/kernel/observation/session-synthesis.ts
2125
+ import { ulid as ulid7 } from "ulid";
2126
+
2127
+ // src/kernel/recall/evaluator.ts
2128
+ import { ulid as ulid6 } from "ulid";
2129
+
2130
+ // src/kernel/scheduler/fsrs.ts
2131
+ var DEFAULT_W = [
2132
+ 0.4072,
2133
+ 1.1829,
2134
+ 3.1262,
2135
+ 15.4722,
2136
+ // w0–w3: initial stability per rating
2137
+ 7.2102,
2138
+ 0.5316,
2139
+ 1.0651,
2140
+ // w4–w6: difficulty
2141
+ 92e-4,
2142
+ 1.5988,
2143
+ 0.1176,
2144
+ 1.0014,
2145
+ // w7–w10: stability after forgetting
2146
+ 2.0032,
2147
+ 0.0266,
2148
+ 0.3077,
2149
+ 0.15,
2150
+ // w11–w14: stability increase
2151
+ 0,
2152
+ 2.7849,
2153
+ 0.3477,
2154
+ 0.6831
2155
+ // w15–w18: additional parameters
2156
+ ];
2157
+ var DEFAULT_REQUEST_RETENTION = 0.9;
2158
+ function clamp(value, lo, hi) {
2159
+ return Math.min(hi, Math.max(lo, value));
2160
+ }
2161
+ function daysBetween(a, b) {
2162
+ return (b.getTime() - a.getTime()) / (1e3 * 60 * 60 * 24);
2163
+ }
2164
+ function initialStability(w, rating) {
2165
+ return w[rating - 1];
2166
+ }
2167
+ function initialDifficulty(w, rating) {
2168
+ return clamp(w[4] - Math.exp(w[5] * (rating - 1)) + 1, 1, 10);
2169
+ }
2170
+ function nextDifficulty(w, d, rating) {
2171
+ const d0ForGood = initialDifficulty(w, 3);
2172
+ const updated = w[7] * d0ForGood + (1 - w[7]) * (d - w[6] * (rating - 3));
2173
+ return clamp(updated, 1, 10);
2174
+ }
2175
+ function retrievability(elapsed, stability) {
2176
+ if (stability <= 0) return 0;
2177
+ return (1 + elapsed / (9 * stability)) ** -1;
2178
+ }
2179
+ function stabilityAfterSuccess(w, s, d, r, rating) {
2180
+ const hardPenalty = rating === 2 ? w[15] : 1;
2181
+ const easyBonus = rating === 4 ? w[16] : 1;
2182
+ const inner = Math.exp(w[8]) * (11 - d) * s ** -w[9] * (Math.exp(w[10] * (1 - r)) - 1) * hardPenalty * easyBonus;
2183
+ return s * (inner + 1);
2184
+ }
2185
+ function stabilityAfterForgetting(w, s, d, r) {
2186
+ return w[11] * d ** -w[12] * ((s + 1) ** w[13] - 1) * Math.exp(w[14] * (1 - r));
2187
+ }
2188
+ function nextInterval(stability, requestRetention) {
2189
+ const interval = 9 * stability * (1 / requestRetention - 1);
2190
+ return Math.max(1, Math.round(interval));
2191
+ }
2192
+ function createFSRS(params) {
2193
+ const resolvedParams = {
2194
+ w: params?.w ?? [...DEFAULT_W],
2195
+ requestRetention: params?.requestRetention ?? DEFAULT_REQUEST_RETENTION
2196
+ };
2197
+ function schedule(card, rating, now) {
2198
+ const reviewTime = now ?? /* @__PURE__ */ new Date();
2199
+ const w = resolvedParams.w;
2200
+ const elapsed = card.lastReviewAt !== null ? Math.max(0, daysBetween(card.lastReviewAt, reviewTime)) : 0;
2201
+ if (card.state === "new") {
2202
+ const s = initialStability(w, rating);
2203
+ const d = initialDifficulty(w, rating);
2204
+ const interval2 = nextInterval(s, resolvedParams.requestRetention);
2205
+ const dueAt2 = new Date(reviewTime);
2206
+ dueAt2.setDate(dueAt2.getDate() + interval2);
2207
+ return {
2208
+ stability: s,
2209
+ difficulty: d,
2210
+ elapsedDays: 0,
2211
+ scheduledDays: interval2,
2212
+ reps: rating >= 2 ? 1 : 0,
2213
+ lapses: rating === 1 ? 1 : 0,
2214
+ state: "learning",
2215
+ dueAt: dueAt2,
2216
+ lastReviewAt: reviewTime
2217
+ };
2218
+ }
2219
+ const r = retrievability(elapsed, card.stability);
2220
+ let newStability;
2221
+ let newDifficulty;
2222
+ let newReps;
2223
+ let newLapses;
2224
+ let newState;
2225
+ if (rating === 1) {
2226
+ newStability = stabilityAfterForgetting(
2227
+ w,
2228
+ card.stability,
2229
+ card.difficulty,
2230
+ r
2231
+ );
2232
+ newDifficulty = nextDifficulty(w, card.difficulty, rating);
2233
+ newReps = 0;
2234
+ newLapses = card.lapses + 1;
2235
+ newState = "relearning";
2236
+ } else {
2237
+ newStability = stabilityAfterSuccess(
2238
+ w,
2239
+ card.stability,
2240
+ card.difficulty,
2241
+ r,
2242
+ rating
2243
+ );
2244
+ newDifficulty = nextDifficulty(w, card.difficulty, rating);
2245
+ newReps = card.reps + 1;
2246
+ newLapses = card.lapses;
2247
+ newState = "review";
2248
+ }
2249
+ const interval = nextInterval(
2250
+ newStability,
2251
+ resolvedParams.requestRetention
2252
+ );
2253
+ const dueAt = new Date(reviewTime);
2254
+ dueAt.setDate(dueAt.getDate() + interval);
2255
+ return {
2256
+ stability: newStability,
2257
+ difficulty: newDifficulty,
2258
+ elapsedDays: elapsed,
2259
+ scheduledDays: interval,
2260
+ reps: newReps,
2261
+ lapses: newLapses,
2262
+ state: newState,
2263
+ dueAt,
2264
+ lastReviewAt: reviewTime
2265
+ };
2266
+ }
2267
+ return {
2268
+ schedule,
2269
+ params: Object.freeze(resolvedParams)
2270
+ };
2271
+ }
2272
+
2273
+ // src/kernel/recall/evaluator.ts
2274
+ async function evaluateRating(db, input) {
2275
+ return db.transaction((tx) => evaluateRatingWithinTransaction(tx, input));
2276
+ }
2277
+ async function evaluateRatingWithinTransaction(db, input) {
2278
+ const card = await db.prepare("SELECT * FROM cards WHERE id = ?").get(input.cardId);
2279
+ if (!card) {
2280
+ throw new Error(`Card not found: ${input.cardId}`);
2281
+ }
2282
+ const now = /* @__PURE__ */ new Date();
2283
+ const fsrs = createFSRS();
2284
+ const schedulingCard = {
2285
+ stability: card.stability,
2286
+ difficulty: card.difficulty,
2287
+ elapsedDays: card.elapsed_days,
2288
+ scheduledDays: card.scheduled_days,
2289
+ reps: card.reps,
2290
+ lapses: card.lapses,
2291
+ state: card.state,
2292
+ dueAt: new Date(card.due_at),
2293
+ lastReviewAt: card.last_review_at ? new Date(card.last_review_at) : null
2294
+ };
2295
+ const updated = fsrs.schedule(schedulingCard, input.rating, now);
2296
+ await updateCard(db, input.cardId, {
2297
+ stability: updated.stability,
2298
+ difficulty: updated.difficulty,
2299
+ elapsed_days: updated.elapsedDays,
2300
+ scheduled_days: updated.scheduledDays,
2301
+ reps: updated.reps,
2302
+ lapses: updated.lapses,
2303
+ state: updated.state,
2304
+ due_at: updated.dueAt.toISOString(),
2305
+ last_review_at: now.toISOString()
2306
+ });
2307
+ const reviewLogId = input.reviewLogId ?? ulid6();
2308
+ await db.prepare(
2309
+ `INSERT INTO review_logs (id, card_id, token_id, user_id, rating, response_time_ms, reviewed_at, scheduled_at, session_id)
2310
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
2311
+ ).run(
2312
+ reviewLogId,
2313
+ input.cardId,
2314
+ input.tokenId,
2315
+ input.userId,
2316
+ input.rating,
2317
+ input.responseTimeMs ?? null,
2318
+ now.toISOString(),
2319
+ card.due_at,
2320
+ input.sessionId ?? null
2321
+ );
2322
+ return {
2323
+ nextDueAt: updated.dueAt.toISOString(),
2324
+ stability: updated.stability,
2325
+ difficulty: updated.difficulty,
2326
+ state: updated.state,
2327
+ scheduledDays: updated.scheduledDays,
2328
+ reps: updated.reps,
2329
+ lapses: updated.lapses
2330
+ };
2331
+ }
2332
+
2333
+ // src/kernel/scheduler/blocker.ts
2334
+ async function cascadeBlock(db, userId, tokenSlug) {
2335
+ const token = await getTokenBySlug(db, tokenSlug);
2336
+ if (!token) {
2337
+ throw new Error(`Unknown token slug: ${tokenSlug}`);
2338
+ }
2339
+ const prereqs = await getPrerequisites(db, token.id);
2340
+ if (prereqs.length === 0) {
2341
+ throw new Error(`Cannot block ${tokenSlug}: token has no prerequisites`);
2342
+ }
2343
+ await ensureCard(db, token.id, userId);
2344
+ await db.prepare("UPDATE cards SET blocked = 1 WHERE token_id = ? AND user_id = ?").run(token.id, userId);
2345
+ const surfaced = [];
2346
+ for (const prereq of prereqs) {
2347
+ const card = await ensureCard(db, prereq.requires_id, userId);
2348
+ if (card.blocked === 1) {
2349
+ const prereqOfPrereq = await db.prepare("SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?").get(prereq.requires_id);
2350
+ if (prereqOfPrereq.n === 0) {
2351
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2352
+ await db.prepare(
2353
+ "UPDATE cards SET blocked = 0, due_at = ? WHERE token_id = ? AND user_id = ?"
2354
+ ).run(now, prereq.requires_id, userId);
2355
+ }
2356
+ }
2357
+ surfaced.push({
2358
+ slug: prereq.slug,
2359
+ concept: prereq.concept,
2360
+ bloomLevel: prereq.bloom_level
2361
+ });
2362
+ }
2363
+ return {
2364
+ blockedSlug: tokenSlug,
2365
+ prerequisites: surfaced
2366
+ };
2367
+ }
2368
+ async function unblockReady(db, userId) {
2369
+ const blockedCards = await db.prepare(
2370
+ `SELECT c.token_id, t.slug, t.concept
2371
+ FROM cards c
2372
+ JOIN tokens t ON t.id = c.token_id
2373
+ WHERE c.user_id = ? AND c.blocked = 1`
2374
+ ).all(userId);
2375
+ const unblocked = [];
2376
+ for (const card of blockedCards) {
2377
+ const totalPrereqs = await db.prepare("SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?").get(card.token_id);
2378
+ const metPrereqs = await db.prepare(
2379
+ `SELECT COUNT(*) as n FROM cards c
2380
+ JOIN prerequisites p ON p.requires_id = c.token_id
2381
+ WHERE p.token_id = ? AND c.user_id = ? AND c.reps >= 1 AND c.blocked = 0`
2382
+ ).get(card.token_id, userId);
2383
+ if (totalPrereqs.n === 0 || metPrereqs.n === totalPrereqs.n) {
2384
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2385
+ await db.prepare(
2386
+ "UPDATE cards SET blocked = 0, due_at = ? WHERE token_id = ? AND user_id = ?"
2387
+ ).run(now, card.token_id, userId);
2388
+ unblocked.push({ slug: card.slug, concept: card.concept });
2389
+ }
2390
+ }
2391
+ return { unblocked };
2392
+ }
2393
+
2394
+ // src/kernel/observation/session-synthesis.ts
2395
+ function parseSynthesisRow(row) {
2396
+ return {
2397
+ ...row,
2398
+ evidence: JSON.parse(row.evidence)
2399
+ };
1480
2400
  }
1481
- function ensureMonitorDir() {
1482
- if (!existsSync4(MONITOR_DIR)) {
1483
- mkdirSync3(MONITOR_DIR, { recursive: true, mode: 448 });
2401
+ function confidenceRank(confidence) {
2402
+ return confidence === "high" ? 2 : confidence === "medium" ? 1 : 0;
2403
+ }
2404
+ function normalizeSkillStep(step) {
2405
+ const codeSpans = [...step.matchAll(/`([^`]+)`/g)].map((match) => match[1].trim()).filter(Boolean);
2406
+ if (codeSpans.length > 0) return codeSpans;
2407
+ const normalized = step.trim().replace(/^(?:[-*]|\d+[.)])\s+/, "").replace(/^(?:run|execute)\s+/i, "").replace(/^`|`$/g, "").trim();
2408
+ return normalized ? [normalized] : [];
2409
+ }
2410
+ async function buildSkillPatterns(db) {
2411
+ const byToken = /* @__PURE__ */ new Map();
2412
+ for (const skill of await listAgentSkills(db)) {
2413
+ if (skill.token_slugs.length !== 1) continue;
2414
+ const patterns = skill.steps.flatMap(normalizeSkillStep);
2415
+ for (const slug of skill.token_slugs) {
2416
+ const tokenPatterns = byToken.get(slug) ?? /* @__PURE__ */ new Set();
2417
+ for (const pattern of patterns) tokenPatterns.add(pattern);
2418
+ byToken.set(slug, tokenPatterns);
2419
+ }
1484
2420
  }
2421
+ return [...byToken.entries()].map(([slug, patterns]) => ({
2422
+ slug,
2423
+ patterns: [...patterns]
2424
+ }));
1485
2425
  }
1486
- function writeMonitorEvent(sessionId, event) {
1487
- ensureMonitorDir();
1488
- const path = getMonitorPath(sessionId);
1489
- appendFileSync(path, `${JSON.stringify(event)}
1490
- `);
1491
- }
1492
- function readMonitorLog(sessionId) {
1493
- const path = getMonitorPath(sessionId);
1494
- if (!existsSync4(path)) {
1495
- return [];
2426
+ function mergePatterns(automatic, explicit) {
2427
+ const merged = /* @__PURE__ */ new Map();
2428
+ for (const entry of [...automatic, ...explicit]) {
2429
+ const patterns = merged.get(entry.slug) ?? /* @__PURE__ */ new Set();
2430
+ for (const pattern of entry.patterns) {
2431
+ const trimmed = pattern.trim();
2432
+ if (trimmed) patterns.add(trimmed);
2433
+ }
2434
+ merged.set(entry.slug, patterns);
1496
2435
  }
1497
- const content = readFileSync4(path, "utf-8");
1498
- return parseMonitorLog(content);
2436
+ return [...merged.entries()].filter(([, patterns]) => patterns.size > 0).map(([slug, patterns]) => ({ slug, patterns: [...patterns] }));
1499
2437
  }
1500
- function monitorLogExists(sessionId) {
1501
- return existsSync4(getMonitorPath(sessionId));
2438
+ async function getSession(db, sessionId) {
2439
+ const session = await db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
2440
+ if (!session) throw new Error(`Session not found: ${sessionId}`);
2441
+ return session;
1502
2442
  }
1503
- function getMonitorLogStats(sessionId) {
1504
- const path = getMonitorPath(sessionId);
1505
- if (!existsSync4(path)) {
1506
- return { exists: false, sizeBytes: 0, lineCount: 0 };
2443
+ async function getSessionSynthesisRecords(db, sessionId) {
2444
+ const rows = await db.prepare(
2445
+ "SELECT * FROM session_syntheses WHERE session_id = ? ORDER BY created_at"
2446
+ ).all(sessionId);
2447
+ return rows.map(parseSynthesisRow);
2448
+ }
2449
+ async function prepareSessionSynthesis(db, input) {
2450
+ const session = await getSession(db, input.sessionId);
2451
+ const patterns = mergePatterns(
2452
+ await buildSkillPatterns(db),
2453
+ input.explicitPatterns ?? []
2454
+ );
2455
+ const validPatterns = [];
2456
+ const tokens = /* @__PURE__ */ new Map();
2457
+ for (const pattern of patterns) {
2458
+ const token = await getTokenBySlug(db, pattern.slug);
2459
+ if (!token || token.deprecated_at) continue;
2460
+ validPatterns.push(pattern);
2461
+ tokens.set(pattern.slug, token);
2462
+ }
2463
+ const commands = input.commands ?? pairCommands(readMonitorLog(input.sessionId));
2464
+ const analysis = analyzeObservation(commands, validPatterns);
2465
+ const applied = new Set(
2466
+ (await getSessionSynthesisRecords(db, input.sessionId)).map(
2467
+ (record) => record.token_id
2468
+ )
2469
+ );
2470
+ const minRank = confidenceRank(input.minConfidence ?? "medium");
2471
+ let skippedLowConfidence = 0;
2472
+ const candidates = [];
2473
+ for (const rating of analysis.ratings) {
2474
+ const token = tokens.get(rating.tokenSlug);
2475
+ if (!token || rating.rating == null || applied.has(token.id)) continue;
2476
+ if (confidenceRank(rating.confidence) < minRank) {
2477
+ skippedLowConfidence++;
2478
+ continue;
2479
+ }
2480
+ candidates.push({
2481
+ tokenId: token.id,
2482
+ tokenSlug: token.slug,
2483
+ concept: token.concept,
2484
+ domain: token.domain,
2485
+ inferredRating: rating.rating,
2486
+ confidence: rating.confidence,
2487
+ evidence: rating.evidence,
2488
+ matchedCommandTexts: rating.matchedCommandTexts
2489
+ });
1507
2490
  }
1508
- const stat = statSync(path);
1509
- const content = readFileSync4(path, "utf-8");
1510
- const lineCount = content.split("\n").filter((l) => l.trim()).length;
1511
- return { exists: true, sizeBytes: stat.size, lineCount };
2491
+ return {
2492
+ sessionId: session.id,
2493
+ userId: session.user_id,
2494
+ patternCount: validPatterns.length,
2495
+ commandCount: commands.length,
2496
+ alreadyApplied: applied.size,
2497
+ skippedLowConfidence,
2498
+ candidates,
2499
+ unmatchedCommands: analysis.unmatchedCommands,
2500
+ timeSpan: analysis.timeSpan
2501
+ };
2502
+ }
2503
+ async function applySessionSynthesis(db, input) {
2504
+ return db.transaction(async (tx) => {
2505
+ const session = await getSession(tx, input.sessionId);
2506
+ const token = await getTokenBySlug(tx, input.tokenSlug);
2507
+ if (!token || token.deprecated_at) {
2508
+ throw new Error(`Active token not found: ${input.tokenSlug}`);
2509
+ }
2510
+ const existing = await tx.prepare(
2511
+ "SELECT * FROM session_syntheses WHERE session_id = ? AND token_id = ?"
2512
+ ).get(session.id, token.id);
2513
+ if (existing) {
2514
+ return { applied: false, record: parseSynthesisRow(existing) };
2515
+ }
2516
+ const card = await ensureCard(tx, token.id, session.user_id);
2517
+ const reviewLogId = ulid7();
2518
+ await evaluateRatingWithinTransaction(tx, {
2519
+ cardId: card.id,
2520
+ tokenId: token.id,
2521
+ userId: session.user_id,
2522
+ rating: input.confirmedRating,
2523
+ sessionId: session.id,
2524
+ reviewLogId
2525
+ });
2526
+ let blocked;
2527
+ if (input.confirmedRating === 1) {
2528
+ const prerequisites = await getPrerequisites(tx, token.id);
2529
+ if (prerequisites.length > 0) {
2530
+ blocked = await cascadeBlock(tx, session.user_id, token.slug);
2531
+ }
2532
+ }
2533
+ const notes = `Observation synthesis (${input.confidence}, inferred ${input.inferredRating}): ${input.matchedCommandTexts.slice(0, 3).join(" | ")}`;
2534
+ const step = await logStep(tx, {
2535
+ session_id: session.id,
2536
+ token_id: token.id,
2537
+ done_by: "user",
2538
+ rating: input.confirmedRating,
2539
+ notes
2540
+ });
2541
+ const evidence = {
2542
+ signals: input.evidence,
2543
+ matchedCommandTexts: input.matchedCommandTexts
2544
+ };
2545
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2546
+ await tx.prepare(
2547
+ `INSERT INTO session_syntheses (
2548
+ session_id, token_id, card_id, inferred_rating, confirmed_rating,
2549
+ confidence, evidence, review_log_id, session_step_id, created_at
2550
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
2551
+ ).run(
2552
+ session.id,
2553
+ token.id,
2554
+ card.id,
2555
+ input.inferredRating,
2556
+ input.confirmedRating,
2557
+ input.confidence,
2558
+ JSON.stringify(evidence),
2559
+ reviewLogId,
2560
+ step.id,
2561
+ now
2562
+ );
2563
+ const record = await tx.prepare(
2564
+ "SELECT * FROM session_syntheses WHERE session_id = ? AND token_id = ?"
2565
+ ).get(session.id, token.id);
2566
+ return {
2567
+ applied: true,
2568
+ record: parseSynthesisRow(record),
2569
+ blocked
2570
+ };
2571
+ });
1512
2572
  }
1513
2573
 
1514
2574
  // src/kernel/observation/shell-hooks.ts
@@ -1810,379 +2870,121 @@ function normalizeCommand(command) {
1810
2870
  }
1811
2871
  return parts.slice(0, Math.min(2, parts.length)).join(" ").toLowerCase();
1812
2872
  }
1813
- function extractSequences(commands, minLen, maxLen) {
1814
- const filtered = commands.filter((c) => {
1815
- const lower = c.command.toLowerCase().trim();
1816
- return lower.length > 0 && !lower.startsWith("cd ") && lower !== "cd" && lower !== "ls" && lower !== "pwd" && lower !== "clear" && lower !== "exit" && !lower.startsWith("echo ");
1817
- });
1818
- const normalized = filtered.map((c) => normalizeCommand(c.command));
1819
- const sequences = [];
1820
- for (let len = minLen; len <= maxLen; len++) {
1821
- for (let i = 0; i <= normalized.length - len; i++) {
1822
- const seq = normalized.slice(i, i + len);
1823
- if (new Set(seq).size >= 2) {
1824
- sequences.push(seq);
1825
- }
1826
- }
1827
- }
1828
- return sequences;
1829
- }
1830
- function findExamplesForSequence(commands, steps) {
1831
- const normalized = commands.map((c) => ({
1832
- norm: normalizeCommand(c.command),
1833
- full: c.command
1834
- }));
1835
- for (let i = 0; i <= normalized.length - steps.length; i++) {
1836
- let match = true;
1837
- for (let j = 0; j < steps.length; j++) {
1838
- if (normalized[i + j].norm !== steps[j]) {
1839
- match = false;
1840
- break;
1841
- }
1842
- }
1843
- if (match) {
1844
- return normalized.slice(i, i + steps.length).map((n) => n.full);
1845
- }
1846
- }
1847
- return [];
1848
- }
1849
- function removeSubsequences(candidates) {
1850
- const sorted = [...candidates].sort(
1851
- (a, b) => b.steps.length - a.steps.length
1852
- );
1853
- const result = [];
1854
- for (const candidate of sorted) {
1855
- const key = candidate.steps.join(" \u2192 ");
1856
- const isSubsequence = result.some((longer) => {
1857
- const longerKey = longer.steps.join(" \u2192 ");
1858
- return longerKey.includes(key) && longerKey !== key;
1859
- });
1860
- if (!isSubsequence) {
1861
- result.push(candidate);
1862
- }
1863
- }
1864
- return result;
1865
- }
1866
- function generateSlug(steps) {
1867
- return steps.map((s) => {
1868
- const parts = s.split(/\s+/);
1869
- return parts[parts.length - 1];
1870
- }).join("-");
1871
- }
1872
- function describeSequence(steps) {
1873
- return `Recurring pattern: ${steps.join(" \u2192 ")}`;
1874
- }
1875
-
1876
- // src/kernel/scheduler/blocker.ts
1877
- function cascadeBlock(db, userId, tokenSlug) {
1878
- const token = getTokenBySlug(db, tokenSlug);
1879
- if (!token) {
1880
- throw new Error(`Unknown token slug: ${tokenSlug}`);
1881
- }
1882
- ensureCard(db, token.id, userId);
1883
- db.prepare(
1884
- "UPDATE cards SET blocked = 1 WHERE token_id = ? AND user_id = ?"
1885
- ).run(token.id, userId);
1886
- const prereqs = getPrerequisites(db, token.id);
1887
- const surfaced = [];
1888
- for (const prereq of prereqs) {
1889
- const card = ensureCard(db, prereq.requires_id, userId);
1890
- if (card.blocked === 1) {
1891
- const prereqOfPrereq = db.prepare("SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?").get(prereq.requires_id);
1892
- if (prereqOfPrereq.n === 0) {
1893
- const now = (/* @__PURE__ */ new Date()).toISOString();
1894
- db.prepare(
1895
- "UPDATE cards SET blocked = 0, due_at = ? WHERE token_id = ? AND user_id = ?"
1896
- ).run(now, prereq.requires_id, userId);
1897
- }
1898
- }
1899
- surfaced.push({
1900
- slug: prereq.slug,
1901
- concept: prereq.concept,
1902
- bloomLevel: prereq.bloom_level
1903
- });
1904
- }
1905
- return {
1906
- blockedSlug: tokenSlug,
1907
- prerequisites: surfaced
1908
- };
1909
- }
1910
- function unblockReady(db, userId) {
1911
- const blockedCards = db.prepare(
1912
- `SELECT c.token_id, t.slug, t.concept
1913
- FROM cards c
1914
- JOIN tokens t ON t.id = c.token_id
1915
- WHERE c.user_id = ? AND c.blocked = 1`
1916
- ).all(userId);
1917
- const unblocked = [];
1918
- for (const card of blockedCards) {
1919
- const totalPrereqs = db.prepare("SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?").get(card.token_id);
1920
- const metPrereqs = db.prepare(
1921
- `SELECT COUNT(*) as n FROM cards c
1922
- JOIN prerequisites p ON p.requires_id = c.token_id
1923
- WHERE p.token_id = ? AND c.user_id = ? AND c.reps >= 1 AND c.blocked = 0`
1924
- ).get(card.token_id, userId);
1925
- if (totalPrereqs.n === 0 || metPrereqs.n === totalPrereqs.n) {
1926
- const now = (/* @__PURE__ */ new Date()).toISOString();
1927
- db.prepare(
1928
- "UPDATE cards SET blocked = 0, due_at = ? WHERE token_id = ? AND user_id = ?"
1929
- ).run(now, card.token_id, userId);
1930
- unblocked.push({ slug: card.slug, concept: card.concept });
1931
- }
1932
- }
1933
- return { unblocked };
1934
- }
1935
-
1936
- // src/kernel/recall/evaluator.ts
1937
- import { ulid as ulid6 } from "ulid";
1938
-
1939
- // src/kernel/scheduler/fsrs.ts
1940
- var DEFAULT_W = [
1941
- 0.4072,
1942
- 1.1829,
1943
- 3.1262,
1944
- 15.4722,
1945
- // w0–w3: initial stability per rating
1946
- 7.2102,
1947
- 0.5316,
1948
- 1.0651,
1949
- // w4–w6: difficulty
1950
- 92e-4,
1951
- 1.5988,
1952
- 0.1176,
1953
- 1.0014,
1954
- // w7–w10: stability after forgetting
1955
- 2.0032,
1956
- 0.0266,
1957
- 0.3077,
1958
- 0.15,
1959
- // w11–w14: stability increase
1960
- 0,
1961
- 2.7849,
1962
- 0.3477,
1963
- 0.6831
1964
- // w15–w18: additional parameters
1965
- ];
1966
- var DEFAULT_REQUEST_RETENTION = 0.9;
1967
- function clamp(value, lo, hi) {
1968
- return Math.min(hi, Math.max(lo, value));
1969
- }
1970
- function daysBetween(a, b) {
1971
- return (b.getTime() - a.getTime()) / (1e3 * 60 * 60 * 24);
1972
- }
1973
- function initialStability(w, rating) {
1974
- return w[rating - 1];
1975
- }
1976
- function initialDifficulty(w, rating) {
1977
- return clamp(w[4] - Math.exp(w[5] * (rating - 1)) + 1, 1, 10);
1978
- }
1979
- function nextDifficulty(w, d, rating) {
1980
- const d0ForGood = initialDifficulty(w, 3);
1981
- const updated = w[7] * d0ForGood + (1 - w[7]) * (d - w[6] * (rating - 3));
1982
- return clamp(updated, 1, 10);
1983
- }
1984
- function retrievability(elapsed, stability) {
1985
- if (stability <= 0) return 0;
1986
- return (1 + elapsed / (9 * stability)) ** -1;
1987
- }
1988
- function stabilityAfterSuccess(w, s, d, r, rating) {
1989
- const hardPenalty = rating === 2 ? w[15] : 1;
1990
- const easyBonus = rating === 4 ? w[16] : 1;
1991
- const inner = Math.exp(w[8]) * (11 - d) * s ** -w[9] * (Math.exp(w[10] * (1 - r)) - 1) * hardPenalty * easyBonus;
1992
- return s * (inner + 1);
1993
- }
1994
- function stabilityAfterForgetting(w, s, d, r) {
1995
- return w[11] * d ** -w[12] * ((s + 1) ** w[13] - 1) * Math.exp(w[14] * (1 - r));
1996
- }
1997
- function nextInterval(stability, requestRetention) {
1998
- const interval = 9 * stability * (1 / requestRetention - 1);
1999
- return Math.max(1, Math.round(interval));
2873
+ function extractSequences(commands, minLen, maxLen) {
2874
+ const filtered = commands.filter((c) => {
2875
+ const lower = c.command.toLowerCase().trim();
2876
+ return lower.length > 0 && !lower.startsWith("cd ") && lower !== "cd" && lower !== "ls" && lower !== "pwd" && lower !== "clear" && lower !== "exit" && !lower.startsWith("echo ");
2877
+ });
2878
+ const normalized = filtered.map((c) => normalizeCommand(c.command));
2879
+ const sequences = [];
2880
+ for (let len = minLen; len <= maxLen; len++) {
2881
+ for (let i = 0; i <= normalized.length - len; i++) {
2882
+ const seq = normalized.slice(i, i + len);
2883
+ if (new Set(seq).size >= 2) {
2884
+ sequences.push(seq);
2885
+ }
2886
+ }
2887
+ }
2888
+ return sequences;
2000
2889
  }
2001
- function createFSRS(params) {
2002
- const resolvedParams = {
2003
- w: params?.w ?? [...DEFAULT_W],
2004
- requestRetention: params?.requestRetention ?? DEFAULT_REQUEST_RETENTION
2005
- };
2006
- function schedule(card, rating, now) {
2007
- const reviewTime = now ?? /* @__PURE__ */ new Date();
2008
- const w = resolvedParams.w;
2009
- const elapsed = card.lastReviewAt !== null ? Math.max(0, daysBetween(card.lastReviewAt, reviewTime)) : 0;
2010
- if (card.state === "new") {
2011
- const s = initialStability(w, rating);
2012
- const d = initialDifficulty(w, rating);
2013
- const interval2 = nextInterval(s, resolvedParams.requestRetention);
2014
- const dueAt2 = new Date(reviewTime);
2015
- dueAt2.setDate(dueAt2.getDate() + interval2);
2016
- return {
2017
- stability: s,
2018
- difficulty: d,
2019
- elapsedDays: 0,
2020
- scheduledDays: interval2,
2021
- reps: rating >= 2 ? 1 : 0,
2022
- lapses: rating === 1 ? 1 : 0,
2023
- state: "learning",
2024
- dueAt: dueAt2,
2025
- lastReviewAt: reviewTime
2026
- };
2890
+ function findExamplesForSequence(commands, steps) {
2891
+ const normalized = commands.map((c) => ({
2892
+ norm: normalizeCommand(c.command),
2893
+ full: c.command
2894
+ }));
2895
+ for (let i = 0; i <= normalized.length - steps.length; i++) {
2896
+ let match = true;
2897
+ for (let j = 0; j < steps.length; j++) {
2898
+ if (normalized[i + j].norm !== steps[j]) {
2899
+ match = false;
2900
+ break;
2901
+ }
2027
2902
  }
2028
- const r = retrievability(elapsed, card.stability);
2029
- let newStability;
2030
- let newDifficulty;
2031
- let newReps;
2032
- let newLapses;
2033
- let newState;
2034
- if (rating === 1) {
2035
- newStability = stabilityAfterForgetting(
2036
- w,
2037
- card.stability,
2038
- card.difficulty,
2039
- r
2040
- );
2041
- newDifficulty = nextDifficulty(w, card.difficulty, rating);
2042
- newReps = 0;
2043
- newLapses = card.lapses + 1;
2044
- newState = "relearning";
2045
- } else {
2046
- newStability = stabilityAfterSuccess(
2047
- w,
2048
- card.stability,
2049
- card.difficulty,
2050
- r,
2051
- rating
2052
- );
2053
- newDifficulty = nextDifficulty(w, card.difficulty, rating);
2054
- newReps = card.reps + 1;
2055
- newLapses = card.lapses;
2056
- newState = "review";
2903
+ if (match) {
2904
+ return normalized.slice(i, i + steps.length).map((n) => n.full);
2057
2905
  }
2058
- const interval = nextInterval(
2059
- newStability,
2060
- resolvedParams.requestRetention
2061
- );
2062
- const dueAt = new Date(reviewTime);
2063
- dueAt.setDate(dueAt.getDate() + interval);
2064
- return {
2065
- stability: newStability,
2066
- difficulty: newDifficulty,
2067
- elapsedDays: elapsed,
2068
- scheduledDays: interval,
2069
- reps: newReps,
2070
- lapses: newLapses,
2071
- state: newState,
2072
- dueAt,
2073
- lastReviewAt: reviewTime
2074
- };
2075
2906
  }
2076
- return {
2077
- schedule,
2078
- params: Object.freeze(resolvedParams)
2079
- };
2907
+ return [];
2080
2908
  }
2081
-
2082
- // src/kernel/recall/evaluator.ts
2083
- function evaluateRating(db, input) {
2084
- const card = db.prepare("SELECT * FROM cards WHERE id = ?").get(input.cardId);
2085
- if (!card) {
2086
- throw new Error(`Card not found: ${input.cardId}`);
2087
- }
2088
- const now = /* @__PURE__ */ new Date();
2089
- const fsrs = createFSRS();
2090
- const schedulingCard = {
2091
- stability: card.stability,
2092
- difficulty: card.difficulty,
2093
- elapsedDays: card.elapsed_days,
2094
- scheduledDays: card.scheduled_days,
2095
- reps: card.reps,
2096
- lapses: card.lapses,
2097
- state: card.state,
2098
- dueAt: new Date(card.due_at),
2099
- lastReviewAt: card.last_review_at ? new Date(card.last_review_at) : null
2100
- };
2101
- const updated = fsrs.schedule(schedulingCard, input.rating, now);
2102
- updateCard(db, input.cardId, {
2103
- stability: updated.stability,
2104
- difficulty: updated.difficulty,
2105
- elapsed_days: updated.elapsedDays,
2106
- scheduled_days: updated.scheduledDays,
2107
- reps: updated.reps,
2108
- lapses: updated.lapses,
2109
- state: updated.state,
2110
- due_at: updated.dueAt.toISOString(),
2111
- last_review_at: now.toISOString()
2112
- });
2113
- db.prepare(
2114
- `INSERT INTO review_logs (id, card_id, token_id, user_id, rating, response_time_ms, reviewed_at, scheduled_at, session_id)
2115
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
2116
- ).run(
2117
- ulid6(),
2118
- input.cardId,
2119
- input.tokenId,
2120
- input.userId,
2121
- input.rating,
2122
- input.responseTimeMs ?? null,
2123
- now.toISOString(),
2124
- card.due_at,
2125
- input.sessionId ?? null
2909
+ function removeSubsequences(candidates) {
2910
+ const sorted = [...candidates].sort(
2911
+ (a, b) => b.steps.length - a.steps.length
2126
2912
  );
2127
- return {
2128
- nextDueAt: updated.dueAt.toISOString(),
2129
- stability: updated.stability,
2130
- difficulty: updated.difficulty,
2131
- state: updated.state,
2132
- scheduledDays: updated.scheduledDays,
2133
- reps: updated.reps,
2134
- lapses: updated.lapses
2135
- };
2913
+ const result = [];
2914
+ for (const candidate of sorted) {
2915
+ const key = candidate.steps.join(" \u2192 ");
2916
+ const isSubsequence = result.some((longer) => {
2917
+ const longerKey = longer.steps.join(" \u2192 ");
2918
+ return longerKey.includes(key) && longerKey !== key;
2919
+ });
2920
+ if (!isSubsequence) {
2921
+ result.push(candidate);
2922
+ }
2923
+ }
2924
+ return result;
2925
+ }
2926
+ function generateSlug(steps) {
2927
+ return steps.map((s) => {
2928
+ const parts = s.split(/\s+/);
2929
+ return parts[parts.length - 1];
2930
+ }).join("-");
2931
+ }
2932
+ function describeSequence(steps) {
2933
+ return `Recurring pattern: ${steps.join(" \u2192 ")}`;
2136
2934
  }
2137
2935
 
2138
2936
  // src/kernel/recall/actions.ts
2139
- function getReviewTarget(db, cardId, userId) {
2140
- const card = getCardById(db, cardId);
2937
+ async function getReviewTarget(db, cardId, userId) {
2938
+ const card = await getCardById(db, cardId);
2141
2939
  if (!card) {
2142
2940
  throw new Error(`Card not found: ${cardId}`);
2143
2941
  }
2144
2942
  if (card.user_id !== userId) {
2145
2943
  throw new Error(`Card ${cardId} does not belong to user ${userId}`);
2146
2944
  }
2147
- const token = getTokenById(db, card.token_id);
2945
+ const token = await getTokenById(db, card.token_id);
2148
2946
  if (!token) {
2149
2947
  throw new Error(`Token not found for card ${cardId}`);
2150
2948
  }
2151
2949
  return { cardId: card.id, token };
2152
2950
  }
2153
- function executeReviewAction(db, input) {
2154
- const target = getReviewTarget(db, input.cardId, input.userId);
2155
- switch (input.action) {
2156
- case "rate": {
2157
- if (input.rating == null) {
2158
- throw new Error("rating is required for action=rate");
2159
- }
2160
- const evaluation = evaluateRating(db, {
2161
- cardId: target.cardId,
2162
- tokenId: target.token.id,
2951
+ async function executeReviewAction(db, input) {
2952
+ if (input.action === "rate") {
2953
+ if (input.rating == null) {
2954
+ throw new Error("rating is required for action=rate");
2955
+ }
2956
+ const rating = input.rating;
2957
+ return db.transaction(async (tx) => {
2958
+ const target2 = await getReviewTarget(tx, input.cardId, input.userId);
2959
+ const evaluation = await evaluateRatingWithinTransaction(tx, {
2960
+ cardId: target2.cardId,
2961
+ tokenId: target2.token.id,
2163
2962
  userId: input.userId,
2164
- rating: input.rating
2963
+ rating
2165
2964
  });
2166
2965
  let blocked;
2167
- if (input.rating === 1) {
2168
- const prereqs = getPrerequisites(db, target.token.id);
2966
+ if (rating === 1) {
2967
+ const prereqs = await getPrerequisites(tx, target2.token.id);
2169
2968
  if (prereqs.length > 0) {
2170
- blocked = cascadeBlock(db, input.userId, target.token.slug);
2969
+ blocked = await cascadeBlock(tx, input.userId, target2.token.slug);
2171
2970
  }
2172
2971
  }
2173
2972
  return {
2174
2973
  action: input.action,
2175
- token: target.token,
2974
+ token: target2.token,
2176
2975
  evaluation,
2177
2976
  blocked
2178
2977
  };
2179
- }
2978
+ });
2979
+ }
2980
+ const target = await getReviewTarget(db, input.cardId, input.userId);
2981
+ switch (input.action) {
2180
2982
  case "skip":
2181
2983
  return { action: input.action, token: target.token, skipped: true };
2182
2984
  case "stop":
2183
2985
  return { action: input.action, token: target.token, stopped: true };
2184
2986
  case "edit-token": {
2185
- const updatedToken = updateToken(
2987
+ const updatedToken = await updateToken(
2186
2988
  db,
2187
2989
  target.token.slug,
2188
2990
  input.tokenUpdates ?? {}
@@ -2194,7 +2996,7 @@ function executeReviewAction(db, input) {
2194
2996
  };
2195
2997
  }
2196
2998
  case "deprecate-token": {
2197
- const updatedToken = deprecateToken(db, target.token.slug);
2999
+ const updatedToken = await deprecateToken(db, target.token.slug);
2198
3000
  return {
2199
3001
  action: input.action,
2200
3002
  token: target.token,
@@ -2202,7 +3004,7 @@ function executeReviewAction(db, input) {
2202
3004
  };
2203
3005
  }
2204
3006
  case "delete-token": {
2205
- const deletedToken = deleteToken(db, target.token.slug);
3007
+ const deletedToken = await deleteToken(db, target.token.slug);
2206
3008
  return {
2207
3009
  action: input.action,
2208
3010
  token: target.token,
@@ -2210,7 +3012,11 @@ function executeReviewAction(db, input) {
2210
3012
  };
2211
3013
  }
2212
3014
  case "delete-card": {
2213
- const deletedCard = deleteCardForUser(db, target.token.id, input.userId);
3015
+ const deletedCard = await deleteCardForUser(
3016
+ db,
3017
+ target.token.id,
3018
+ input.userId
3019
+ );
2214
3020
  return {
2215
3021
  action: input.action,
2216
3022
  token: target.token,
@@ -2514,12 +3320,12 @@ function interleave(items, maxConsecutive = 2) {
2514
3320
  }
2515
3321
 
2516
3322
  // src/kernel/scheduler/queue.ts
2517
- function buildReviewQueue(db, options) {
3323
+ async function buildReviewQueue(db, options) {
2518
3324
  const maxNew = options.maxNew ?? 10;
2519
3325
  const maxReviews = options.maxReviews ?? 50;
2520
3326
  const now = options.now ?? /* @__PURE__ */ new Date();
2521
3327
  const nowISO = now.toISOString();
2522
- const dueRows = db.prepare(
3328
+ const dueRows = await db.prepare(
2523
3329
  `SELECT
2524
3330
  c.id AS card_id,
2525
3331
  c.token_id AS token_id,
@@ -2540,7 +3346,7 @@ function buildReviewQueue(db, options) {
2540
3346
  AND t.deprecated_at IS NULL
2541
3347
  ORDER BY c.due_at ASC`
2542
3348
  ).all(options.userId, nowISO);
2543
- const newRows = db.prepare(
3349
+ const newRows = await db.prepare(
2544
3350
  `SELECT
2545
3351
  c.id AS card_id,
2546
3352
  c.token_id AS token_id,
@@ -3055,11 +3861,61 @@ function t(locale, key, params = {}) {
3055
3861
  return str;
3056
3862
  }
3057
3863
 
3864
+ // src/kernel/system/install-config.ts
3865
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
3866
+ import { homedir as homedir5 } from "os";
3867
+ import { dirname as dirname4, join as join7 } from "path";
3868
+ var DEFAULT_CONFIG_PATH = join7(homedir5(), ".zam", "config.json");
3869
+ function loadInstallConfig(path = DEFAULT_CONFIG_PATH) {
3870
+ if (!existsSync7(path)) return {};
3871
+ try {
3872
+ return JSON.parse(readFileSync7(path, "utf-8"));
3873
+ } catch {
3874
+ return {};
3875
+ }
3876
+ }
3877
+ function saveInstallConfig(config, path = DEFAULT_CONFIG_PATH) {
3878
+ const dir = dirname4(path);
3879
+ if (!existsSync7(dir)) mkdirSync5(dir, { recursive: true });
3880
+ writeFileSync4(path, `${JSON.stringify(config, null, 2)}
3881
+ `, "utf-8");
3882
+ }
3883
+ function getInstallMode(path = DEFAULT_CONFIG_PATH) {
3884
+ return loadInstallConfig(path).mode ?? "developer";
3885
+ }
3886
+ function setInstallMode(mode, path = DEFAULT_CONFIG_PATH) {
3887
+ const config = loadInstallConfig(path);
3888
+ config.mode = mode;
3889
+ saveInstallConfig(config, path);
3890
+ }
3891
+ function getInstallChannel(path = DEFAULT_CONFIG_PATH) {
3892
+ const config = loadInstallConfig(path);
3893
+ if (config.channel) return config.channel;
3894
+ return (config.mode ?? "developer") === "developer" ? "developer" : "direct";
3895
+ }
3896
+ function setInstallChannel(channel, path = DEFAULT_CONFIG_PATH) {
3897
+ const config = loadInstallConfig(path);
3898
+ config.channel = channel;
3899
+ saveInstallConfig(config, path);
3900
+ }
3901
+ function detectSyncProvider(dir) {
3902
+ const p = dir.toLowerCase();
3903
+ if (p.includes("onedrive")) return "OneDrive";
3904
+ if (p.includes("dropbox")) return "Dropbox";
3905
+ if (p.includes("google drive") || p.includes("googledrive") || p.includes("/my drive")) {
3906
+ return "Google Drive";
3907
+ }
3908
+ if (p.includes("icloud") || p.includes("mobile documents")) {
3909
+ return "iCloud Drive";
3910
+ }
3911
+ return null;
3912
+ }
3913
+
3058
3914
  // src/kernel/system/installer.ts
3059
3915
  import { execFileSync, execSync } from "child_process";
3060
- import { existsSync as existsSync7 } from "fs";
3061
- import { homedir as homedir5 } from "os";
3062
- import { join as join7 } from "path";
3916
+ import { existsSync as existsSync8 } from "fs";
3917
+ import { homedir as homedir6 } from "os";
3918
+ import { join as join8 } from "path";
3063
3919
  function hasCommand(cmd) {
3064
3920
  try {
3065
3921
  const checkCmd = process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`;
@@ -3076,7 +3932,7 @@ function installFastFlowLM() {
3076
3932
  message: "FastFlowLM is only supported on Windows."
3077
3933
  };
3078
3934
  }
3079
- const hasFlm = hasCommand("flm") || existsSync7("C:\\Program Files\\flm\\flm.exe");
3935
+ const hasFlm = hasCommand("flm") || existsSync8("C:\\Program Files\\flm\\flm.exe");
3080
3936
  if (hasFlm) {
3081
3937
  return { success: true, message: "FastFlowLM is already installed." };
3082
3938
  }
@@ -3103,8 +3959,8 @@ function installFastFlowLM() {
3103
3959
  function installOllama() {
3104
3960
  const isMac = process.platform === "darwin";
3105
3961
  const isWin = process.platform === "win32";
3106
- const hasOllama = hasCommand("ollama") || isMac && existsSync7("/Applications/Ollama.app") || isWin && existsSync7(
3107
- join7(homedir5(), "AppData", "Local", "Programs", "Ollama", "ollama.exe")
3962
+ const hasOllama = hasCommand("ollama") || isMac && existsSync8("/Applications/Ollama.app") || isWin && existsSync8(
3963
+ join8(homedir6(), "AppData", "Local", "Programs", "Ollama", "ollama.exe")
3108
3964
  );
3109
3965
  if (hasOllama) {
3110
3966
  return { success: true, message: "Ollama is already installed." };
@@ -3164,8 +4020,8 @@ function installOllama() {
3164
4020
  function resolveOllamaCommand() {
3165
4021
  if (hasCommand("ollama")) return "ollama";
3166
4022
  const candidates = process.platform === "win32" ? [
3167
- join7(
3168
- homedir5(),
4023
+ join8(
4024
+ homedir6(),
3169
4025
  "AppData",
3170
4026
  "Local",
3171
4027
  "Programs",
@@ -3173,7 +4029,7 @@ function resolveOllamaCommand() {
3173
4029
  "ollama.exe"
3174
4030
  )
3175
4031
  ] : process.platform === "darwin" ? ["/Applications/Ollama.app/Contents/Resources/ollama"] : [];
3176
- return candidates.find((candidate) => existsSync7(candidate));
4032
+ return candidates.find((candidate) => existsSync8(candidate));
3177
4033
  }
3178
4034
  function prepareLocalModel(runner, model) {
3179
4035
  if (runner === "fastflowlm") {
@@ -3206,6 +4062,63 @@ function prepareLocalModel(runner, model) {
3206
4062
  };
3207
4063
  }
3208
4064
  }
4065
+ function planOpenCodeInstall(env) {
4066
+ if (env.hasNpm) {
4067
+ return { method: "npm", command: "npm install -g opencode-ai" };
4068
+ }
4069
+ if (env.platform === "darwin") {
4070
+ if (env.hasBrew) {
4071
+ return {
4072
+ method: "homebrew",
4073
+ command: "brew install anomalyco/tap/opencode"
4074
+ };
4075
+ }
4076
+ return {
4077
+ method: "script",
4078
+ command: "curl -fsSL https://opencode.ai/install | bash"
4079
+ };
4080
+ }
4081
+ if (env.platform === "win32") {
4082
+ if (env.hasScoop)
4083
+ return { method: "scoop", command: "scoop install opencode" };
4084
+ if (env.hasChoco) {
4085
+ return { method: "chocolatey", command: "choco install opencode" };
4086
+ }
4087
+ return null;
4088
+ }
4089
+ return {
4090
+ method: "script",
4091
+ command: "curl -fsSL https://opencode.ai/install | bash"
4092
+ };
4093
+ }
4094
+ function installOpenCode() {
4095
+ if (hasCommand("opencode")) {
4096
+ return { success: true, message: "opencode is already installed." };
4097
+ }
4098
+ const plan = planOpenCodeInstall({
4099
+ platform: process.platform,
4100
+ hasNpm: hasCommand("npm"),
4101
+ hasBrew: hasCommand("brew"),
4102
+ hasScoop: hasCommand("scoop"),
4103
+ hasChoco: hasCommand("choco")
4104
+ });
4105
+ if (!plan) {
4106
+ return {
4107
+ success: false,
4108
+ message: "Could not find a way to install opencode automatically. Install npm, Scoop, or Chocolatey, or follow https://opencode.ai/docs (native Apple Silicon and Windows on ARM builds are available)."
4109
+ };
4110
+ }
4111
+ console.log(`Installing opencode via ${plan.method}...`);
4112
+ try {
4113
+ execSync(plan.command, { stdio: "inherit" });
4114
+ return { success: true, message: `opencode installed via ${plan.method}.` };
4115
+ } catch (err) {
4116
+ return {
4117
+ success: false,
4118
+ message: `Failed to install opencode: ${err.message}. Try manually: ${plan.command}`
4119
+ };
4120
+ }
4121
+ }
3209
4122
 
3210
4123
  // src/kernel/system/locale.ts
3211
4124
  import { execSync as execSync2 } from "child_process";
@@ -3304,72 +4217,164 @@ function getSystemProfile() {
3304
4217
  }
3305
4218
 
3306
4219
  // src/kernel/system/repos.ts
3307
- import { existsSync as existsSync8 } from "fs";
4220
+ import { existsSync as existsSync9 } from "fs";
3308
4221
  import { resolve as resolve2 } from "path";
3309
- function getRepoPaths(db) {
3310
- const personalSetting = getSetting(db, "repo.personal") || getSetting(db, "personal.workspace_dir");
3311
- const teamSetting = getSetting(db, "repo.team");
3312
- const orgSetting = getSetting(db, "repo.org");
4222
+ async function getRepoPaths(db) {
4223
+ const personalSetting = await getSetting(db, "repo.personal") || await getSetting(db, "personal.workspace_dir");
4224
+ const teamSetting = await getSetting(db, "repo.team");
4225
+ const orgSetting = await getSetting(db, "repo.org");
3313
4226
  return {
3314
4227
  personal: personalSetting ? resolve2(personalSetting) : null,
3315
4228
  team: teamSetting ? resolve2(teamSetting) : null,
3316
4229
  org: orgSetting ? resolve2(orgSetting) : null
3317
4230
  };
3318
4231
  }
3319
- function resolveRepoPath(db, type) {
3320
- const paths = getRepoPaths(db);
4232
+ async function resolveRepoPath(db, type) {
4233
+ const paths = await getRepoPaths(db);
3321
4234
  return paths[type];
3322
4235
  }
3323
- function resolveAllBeliefPaths(db) {
3324
- const paths = getRepoPaths(db);
4236
+ async function resolveAllBeliefPaths(db) {
4237
+ const paths = await getRepoPaths(db);
3325
4238
  const dirs = [];
3326
4239
  if (paths.personal) {
3327
4240
  const personalDir = resolve2(paths.personal, "beliefs");
3328
- if (existsSync8(personalDir)) dirs.push(personalDir);
4241
+ if (existsSync9(personalDir)) dirs.push(personalDir);
3329
4242
  }
3330
4243
  if (paths.team) {
3331
4244
  const teamDir = resolve2(paths.team, "beliefs");
3332
- if (existsSync8(teamDir)) dirs.push(teamDir);
4245
+ if (existsSync9(teamDir)) dirs.push(teamDir);
3333
4246
  }
3334
4247
  if (paths.org) {
3335
4248
  const orgDir = resolve2(paths.org, "beliefs");
3336
- if (existsSync8(orgDir)) dirs.push(orgDir);
4249
+ if (existsSync9(orgDir)) dirs.push(orgDir);
3337
4250
  }
3338
4251
  return dirs;
3339
4252
  }
3340
- function resolveAllGoalPaths(db) {
3341
- const paths = getRepoPaths(db);
4253
+ async function resolveAllGoalPaths(db) {
4254
+ const paths = await getRepoPaths(db);
3342
4255
  const dirs = [];
3343
4256
  if (paths.personal) {
3344
4257
  const personalDir = resolve2(paths.personal, "goals");
3345
- if (existsSync8(personalDir)) dirs.push(personalDir);
4258
+ if (existsSync9(personalDir)) dirs.push(personalDir);
3346
4259
  }
3347
4260
  if (paths.team) {
3348
4261
  const teamDir = resolve2(paths.team, "goals");
3349
- if (existsSync8(teamDir)) dirs.push(teamDir);
4262
+ if (existsSync9(teamDir)) dirs.push(teamDir);
3350
4263
  }
3351
4264
  if (paths.org) {
3352
4265
  const orgDir = resolve2(paths.org, "goals");
3353
- if (existsSync8(orgDir)) dirs.push(orgDir);
4266
+ if (existsSync9(orgDir)) dirs.push(orgDir);
3354
4267
  }
3355
4268
  return dirs;
3356
4269
  }
4270
+
4271
+ // src/kernel/system/update-check.ts
4272
+ var WINGET_PACKAGE_ID = "ZAM.ZAM";
4273
+ var HOMEBREW_CASK = "zam";
4274
+ function parseVersion(version) {
4275
+ const clean = version.trim().replace(/^v/i, "");
4276
+ const [main, pre = ""] = clean.split("-", 2);
4277
+ const core = main.split(".").map((n) => Number.parseInt(n, 10) || 0);
4278
+ while (core.length < 3) core.push(0);
4279
+ return { core, pre: pre ? pre.split(".") : [] };
4280
+ }
4281
+ function compareVersions(a, b) {
4282
+ const pa = parseVersion(a);
4283
+ const pb = parseVersion(b);
4284
+ for (let i = 0; i < 3; i++) {
4285
+ if (pa.core[i] !== pb.core[i]) return pa.core[i] > pb.core[i] ? 1 : -1;
4286
+ }
4287
+ if (pa.pre.length === 0 && pb.pre.length === 0) return 0;
4288
+ if (pa.pre.length === 0) return 1;
4289
+ if (pb.pre.length === 0) return -1;
4290
+ const len = Math.max(pa.pre.length, pb.pre.length);
4291
+ for (let i = 0; i < len; i++) {
4292
+ const x = pa.pre[i];
4293
+ const y = pb.pre[i];
4294
+ if (x === void 0) return -1;
4295
+ if (y === void 0) return 1;
4296
+ const xNum = /^\d+$/.test(x);
4297
+ const yNum = /^\d+$/.test(y);
4298
+ if (xNum && yNum) {
4299
+ const dx = Number(x);
4300
+ const dy = Number(y);
4301
+ if (dx !== dy) return dx > dy ? 1 : -1;
4302
+ } else if (xNum !== yNum) {
4303
+ return xNum ? -1 : 1;
4304
+ } else if (x !== y) {
4305
+ return x > y ? 1 : -1;
4306
+ }
4307
+ }
4308
+ return 0;
4309
+ }
4310
+ function decideUpdate(input) {
4311
+ const { currentVersion, latestVersion, channel } = input;
4312
+ const base = { currentVersion, latestVersion, channel };
4313
+ if (compareVersions(latestVersion, currentVersion) <= 0) {
4314
+ return {
4315
+ ...base,
4316
+ updateAvailable: false,
4317
+ action: "none",
4318
+ reason: "Already on the latest version."
4319
+ };
4320
+ }
4321
+ switch (channel) {
4322
+ case "direct":
4323
+ return {
4324
+ ...base,
4325
+ updateAvailable: true,
4326
+ action: "self-update",
4327
+ reason: "A signed update can be installed in place."
4328
+ };
4329
+ case "winget":
4330
+ return {
4331
+ ...base,
4332
+ updateAvailable: true,
4333
+ action: "run-command",
4334
+ command: `winget upgrade --id ${WINGET_PACKAGE_ID}`,
4335
+ reason: "Update available through winget."
4336
+ };
4337
+ case "homebrew":
4338
+ return {
4339
+ ...base,
4340
+ updateAvailable: true,
4341
+ action: "run-command",
4342
+ command: `brew upgrade --cask ${HOMEBREW_CASK}`,
4343
+ reason: "Update available through Homebrew."
4344
+ };
4345
+ default:
4346
+ return {
4347
+ ...base,
4348
+ updateAvailable: true,
4349
+ action: "inform",
4350
+ command: "git pull && npm install && npm run build",
4351
+ reason: "Developer install \u2014 update from source."
4352
+ };
4353
+ }
4354
+ }
3357
4355
  export {
3358
4356
  DEFAULT_REVIEW_CONTEXT_MAX_CHARS,
4357
+ HOMEBREW_CASK,
4358
+ SNAPSHOT_VERSION,
4359
+ WINGET_PACKAGE_ID,
3359
4360
  addPrerequisite,
3360
4361
  analyzeObservation,
4362
+ applySessionSynthesis,
3361
4363
  buildReviewQueue,
3362
4364
  cascadeBlock,
3363
4365
  clearADOCredentials,
3364
4366
  clearTursoCredentials,
4367
+ compareVersions,
3365
4368
  createAgentSkill,
3366
4369
  createFSRS,
3367
4370
  createGoal,
3368
4371
  createToken,
4372
+ decideUpdate,
3369
4373
  deleteCardForUser,
3370
4374
  deleteSetting,
3371
4375
  deleteToken,
3372
4376
  deprecateToken,
4377
+ detectSyncProvider,
3373
4378
  detectSystemLocale,
3374
4379
  discoverSkills,
3375
4380
  distributeGlobalSkills,
@@ -3378,6 +4383,7 @@ export {
3378
4383
  ensureMonitorDir,
3379
4384
  evaluateRating,
3380
4385
  executeReviewAction,
4386
+ exportSnapshot,
3381
4387
  extractTasks,
3382
4388
  extractTokenRefs,
3383
4389
  fetchActiveWorkItems,
@@ -3404,6 +4410,8 @@ export {
3404
4410
  getDueCards,
3405
4411
  getGoal,
3406
4412
  getGoalTree,
4413
+ getInstallChannel,
4414
+ getInstallMode,
3407
4415
  getMonitorDir,
3408
4416
  getMonitorLogStats,
3409
4417
  getMonitorPath,
@@ -3413,23 +4421,28 @@ export {
3413
4421
  getReviewsForCard,
3414
4422
  getReviewsForUser,
3415
4423
  getSessionSummary,
4424
+ getSessionSynthesisRecords,
3416
4425
  getSetting,
3417
4426
  getSystemProfile,
3418
4427
  getTokenById,
3419
4428
  getTokenBySlug,
3420
4429
  getTokenDeleteImpact,
4430
+ getTokenNeighborhood,
3421
4431
  getTursoCredentials,
3422
4432
  getUserStats,
3423
4433
  hasCommand,
4434
+ importSnapshot,
3424
4435
  injectShellHooks,
3425
4436
  installFastFlowLM,
3426
4437
  installOllama,
4438
+ installOpenCode,
3427
4439
  interleave,
3428
4440
  listAgentSkills,
3429
4441
  listGoals,
3430
4442
  listTokens,
3431
4443
  loadADOConfig,
3432
4444
  loadCredentials,
4445
+ loadInstallConfig,
3433
4446
  logReview,
3434
4447
  logStep,
3435
4448
  matchesFilePath,
@@ -3438,10 +4451,14 @@ export {
3438
4451
  normalizePath,
3439
4452
  openDatabase,
3440
4453
  openDatabaseWithSync,
4454
+ openRemoteDatabase,
3441
4455
  pairCommands,
3442
4456
  parseGoalFile,
3443
4457
  parseMonitorLog,
4458
+ parseSnapshot,
4459
+ planOpenCodeInstall,
3444
4460
  prepareLocalModel,
4461
+ prepareSessionSynthesis,
3445
4462
  readMonitorLog,
3446
4463
  resolveAllBeliefPaths,
3447
4464
  resolveAllGoalPaths,
@@ -3449,8 +4466,11 @@ export {
3449
4466
  resolveRepoPath,
3450
4467
  resolveReviewContext,
3451
4468
  saveCredentials,
4469
+ saveInstallConfig,
3452
4470
  serializeGoal,
3453
4471
  setADOCredentials,
4472
+ setInstallChannel,
4473
+ setInstallMode,
3454
4474
  setSetting,
3455
4475
  setTursoCredentials,
3456
4476
  startSession,
@@ -3459,6 +4479,7 @@ export {
3459
4479
  updateCard,
3460
4480
  updateGoalStatus,
3461
4481
  updateToken,
4482
+ verifySnapshot,
3462
4483
  wouldCreateCycle,
3463
4484
  writeMonitorEvent
3464
4485
  };