akm-cli 0.9.0-beta.45 → 0.9.0-beta.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/assets/wiki/ingest-workflow-template.md +17 -10
  2. package/dist/cli/shared.js +28 -0
  3. package/dist/cli.js +1 -2
  4. package/dist/commands/env/env-cli.js +16 -24
  5. package/dist/commands/env/secret-cli.js +12 -20
  6. package/dist/commands/graph/graph-cli.js +5 -13
  7. package/dist/commands/graph/graph.js +3 -3
  8. package/dist/commands/improve/consolidate/chunking.js +141 -0
  9. package/dist/commands/improve/consolidate/eligibility.js +64 -0
  10. package/dist/commands/improve/consolidate/merge.js +145 -0
  11. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  12. package/dist/commands/improve/consolidate/types.js +4 -0
  13. package/dist/commands/improve/consolidate.js +20 -571
  14. package/dist/commands/improve/distill.js +5 -9
  15. package/dist/commands/improve/eligibility.js +434 -0
  16. package/dist/commands/improve/extract-cli.js +9 -1
  17. package/dist/commands/improve/extract.js +5 -19
  18. package/dist/commands/improve/improve-auto-accept.js +4 -8
  19. package/dist/commands/improve/improve-cli.js +35 -60
  20. package/dist/commands/improve/improve-result-file.js +5 -23
  21. package/dist/commands/improve/improve-session.js +58 -0
  22. package/dist/commands/improve/improve.js +107 -3606
  23. package/dist/commands/improve/locks.js +154 -0
  24. package/dist/commands/improve/loop-stages.js +1079 -0
  25. package/dist/commands/improve/preparation.js +1963 -0
  26. package/dist/commands/improve/recombine.js +6 -12
  27. package/dist/commands/improve/reflect.js +29 -34
  28. package/dist/commands/proposal/drain.js +25 -48
  29. package/dist/commands/proposal/proposal-cli.js +21 -31
  30. package/dist/commands/proposal/validators/proposals.js +3 -7
  31. package/dist/commands/read/curate.js +70 -14
  32. package/dist/commands/read/knowledge.js +2 -2
  33. package/dist/commands/sources/self-update.js +2 -2
  34. package/dist/commands/sources/stash-cli.js +9 -37
  35. package/dist/commands/tasks/tasks-cli.js +19 -27
  36. package/dist/commands/wiki-cli.js +21 -35
  37. package/dist/core/config/config.js +18 -2
  38. package/dist/core/events.js +3 -7
  39. package/dist/core/logs-db.js +6 -63
  40. package/dist/core/state/migrations.js +714 -0
  41. package/dist/core/state-db.js +28 -779
  42. package/dist/indexer/db/db.js +82 -216
  43. package/dist/indexer/indexer.js +11 -112
  44. package/dist/indexer/passes/dir-staleness.js +114 -0
  45. package/dist/indexer/search/search-source.js +10 -24
  46. package/dist/indexer/search/semantic-status.js +4 -0
  47. package/dist/integrations/agent/runner-dispatch.js +59 -0
  48. package/dist/llm/client.js +22 -11
  49. package/dist/llm/embedder.js +15 -0
  50. package/dist/llm/embedders/deterministic.js +66 -0
  51. package/dist/llm/graph-extract.js +28 -39
  52. package/dist/llm/memory-infer.js +34 -22
  53. package/dist/llm/metadata-enhance.js +35 -30
  54. package/dist/llm/structured-call.js +49 -0
  55. package/dist/output/shapes/passthrough.js +0 -1
  56. package/dist/registry/providers/skills-sh.js +21 -147
  57. package/dist/registry/providers/static-index.js +15 -157
  58. package/dist/registry/resolve.js +22 -9
  59. package/dist/scripts/migrate-storage.js +892 -1186
  60. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +214 -179
  61. package/dist/setup/setup.js +26 -5
  62. package/dist/sources/providers/filesystem.js +0 -1
  63. package/dist/sources/providers/git-install.js +206 -0
  64. package/dist/sources/providers/git-provider.js +234 -0
  65. package/dist/sources/providers/git-stash.js +248 -0
  66. package/dist/sources/providers/git.js +10 -671
  67. package/dist/sources/providers/npm.js +2 -6
  68. package/dist/sources/providers/sync-from-ref.js +9 -1
  69. package/dist/sources/providers/website.js +2 -3
  70. package/dist/sources/website-ingest.js +51 -9
  71. package/dist/sources/wiki-fetchers/registry.js +53 -0
  72. package/dist/sources/wiki-fetchers/youtube.js +185 -0
  73. package/dist/storage/database.js +45 -10
  74. package/dist/storage/managed-db.js +82 -0
  75. package/dist/storage/repositories/registry-cache.js +92 -0
  76. package/dist/tasks/runner.js +5 -13
  77. package/dist/workflows/runtime/runs.js +1 -117
  78. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  79. package/package.json +5 -5
  80. package/dist/commands/db-cli.js +0 -23
  81. package/dist/indexer/db/db-backup.js +0 -376
@@ -12,12 +12,12 @@
12
12
  *
13
13
  * ## Why a separate database from index.db
14
14
  *
15
- * index.db uses a single DB_VERSION integer: when the version changes it drops
16
- * ALL tables and recreates them. That is acceptable for the search index because
17
- * every entry is fully regenerable from the stash on disk. Events, proposals, and
18
- * task history are NON-REGENERABLE — losing them is data loss. They must live in
19
- * a database whose schema evolves via incremental, additive migrations that never
20
- * drop rows.
15
+ * index.db is a derived cache built by an idempotent baseline schema; it is fully
16
+ * regenerable from the stash on disk, so a corrupt index is recovered by deleting
17
+ * it and re-running `akm index` (no destructive version-bump rebuild). Events,
18
+ * proposals, and task history are NON-REGENERABLE — losing them is data loss. They
19
+ * must live in a database whose schema evolves via incremental, additive migrations
20
+ * that never drop rows.
21
21
  *
22
22
  * ## Migration-safety contract
23
23
  *
@@ -49,11 +49,8 @@
49
49
  *
50
50
  * @module state-db
51
51
  */
52
- import fs from "node:fs";
53
52
  import path from "node:path";
54
- import { openDatabase } from "../storage/database.js";
55
- import { runMigrations as runSqliteMigrations } from "../storage/engines/sqlite-migrations.js";
56
- import { applyStandardPragmas } from "../storage/sqlite-pragmas.js";
53
+ import { openManagedDatabase, withManagedDb, withManagedDbAsync } from "../storage/managed-db.js";
57
54
  import { classifyImproveAction } from "./improve-types.js";
58
55
  import { getDataDir } from "./paths.js";
59
56
  import { error } from "./warn.js";
@@ -95,739 +92,33 @@ export function getStateDbPath() {
95
92
  * narrow when a post-inference reindex overlapped a parallel event write.
96
93
  */
97
94
  export function openStateDatabase(dbPath) {
98
- const resolvedPath = dbPath ?? getStateDbPath();
99
- const dir = path.dirname(resolvedPath);
100
- if (!fs.existsSync(dir)) {
101
- fs.mkdirSync(dir, { recursive: true });
102
- }
103
- const db = openDatabase(resolvedPath);
104
- // PRAGMAs must run before any DDL or DML.
105
- applyStandardPragmas(db, { dataDir: dir });
106
- runMigrations(db);
107
- return db;
95
+ return openManagedDatabase({ path: dbPath ?? getStateDbPath(), init: runMigrations });
108
96
  }
109
- // ── Migration engine ─────────────────────────────────────────────────────────
110
- //
111
- // The runner itself (ensureMigrationsTable + runMigrations) lives in the shared
112
- // engine at src/storage/engines/sqlite-migrations.ts. This module owns only its
113
- // own MIGRATIONS array and delegates application to that shared runner. The
114
- // {@link Migration} interface is imported from there.
115
97
  /**
116
- * All migrations in application order. New migrations are APPENDED to this
117
- * array never inserted in the middle or reordered.
118
- *
119
- * @see Migration
98
+ * Run `fn` against state.db, owning the handle unless one is borrowed. The loan
99
+ * helper for state.db, mirroring `withIndexDb` / `withWorkflowRunsRepo`. Pass
100
+ * `{ borrowed: ctx?.db }` to reuse an already-open run-scoped handle rather than
101
+ * opening + closing a fresh one — this replaces the hand-rolled
102
+ * `ctx?.db ?? open()` + `ownsDb` flag + `finally`/close idiom at call sites.
120
103
  */
121
- const MIGRATIONS = [
122
- // ── Migration 001 initial schema ──────────────────────────────────────────
123
- {
124
- id: "001-initial-schema",
125
- up: `
126
- -- ── events ──────────────────────────────────────────────────────────────
127
- --
128
- -- Replaces events.jsonl. Indexed (query) columns:
129
- -- id INTEGER PK — monotonic rowid; replaces byte-offset cursor.
130
- -- Callers store this as "sinceId" for resume.
131
- -- event_type TEXT — indexed; replaces the type filter in readEvents().
132
- -- ts TEXT — ISO-8601 UTC ms; indexed for range queries.
133
- -- ref TEXT — nullable asset ref; indexed for ref-scoped queries.
134
- --
135
- -- Extensible (metadata_json) columns:
136
- -- metadata_json TEXT — JSON object storing all non-indexed payload
137
- -- fields (tags, any future structured fields).
138
- -- Maps directly to EventEnvelope.metadata.
139
- --
140
- -- schema_version mirrors EventEnvelope.schemaVersion — always 1 for v1
141
- -- rows. Stored as a column (not in the JSON blob) so future schema
142
- -- changes can be detected and migrated row-by-row if ever needed.
143
- --
144
- -- TTL: rows where ts < NOW() - 90 days can be deleted by a maintenance job.
145
- -- No automatic deletion occurs here — callers call purgeOldEvents().
146
- --
147
- -- ADD COLUMN extension points (future migrations):
148
- -- ALTER TABLE events ADD COLUMN stash_dir TEXT DEFAULT NULL;
149
- -- ALTER TABLE events ADD COLUMN correlation_id TEXT DEFAULT NULL;
150
- -- ALTER TABLE events ADD COLUMN schema_version INTEGER NOT NULL DEFAULT 1;
151
- --
152
- CREATE TABLE IF NOT EXISTS events (
153
- id INTEGER PRIMARY KEY AUTOINCREMENT,
154
- event_type TEXT NOT NULL,
155
- ts TEXT NOT NULL,
156
- ref TEXT,
157
- metadata_json TEXT NOT NULL DEFAULT '{}'
158
- );
159
-
160
- -- Query patterns supported by these indexes:
161
- -- SELECT … WHERE event_type = ? → idx_events_type
162
- -- SELECT … WHERE ref = ? → idx_events_ref
163
- -- SELECT … WHERE ts >= ? AND ts <= ? → idx_events_ts
164
- -- SELECT … WHERE event_type = ? AND ref = ? → idx_events_type (prefix scan) + filter
165
- -- SELECT … WHERE id > ? → PK (rowid) — no extra index needed
166
- CREATE INDEX IF NOT EXISTS idx_events_type ON events(event_type);
167
- CREATE INDEX IF NOT EXISTS idx_events_ref ON events(ref);
168
- CREATE INDEX IF NOT EXISTS idx_events_ts ON events(ts);
169
-
170
- -- ── proposals ────────────────────────────────────────────────────────────
171
- --
172
- -- Replaces per-uuid JSON directories under <stashDir>/.akm/proposals/.
173
- --
174
- -- Indexed (query) columns:
175
- -- id TEXT PK — UUID (crypto.randomUUID()); stable directory name.
176
- -- stash_dir TEXT — absolute stash root; multi-stash installs need
177
- -- this to partition proposal lists per stash.
178
- -- ref TEXT — target asset ref (e.g. "lesson:alpha");
179
- -- indexed for ref-scoped queue views.
180
- -- status TEXT — "pending" | "accepted" | "rejected"; indexed
181
- -- so pending-queue queries are fast.
182
- -- source TEXT — human-readable origin tag (e.g. "reflect").
183
- -- created_at TEXT — ISO-8601; used for ORDER BY created_at ASC.
184
- -- updated_at TEXT — ISO-8601; updated on accept/reject.
185
- --
186
- -- Large payload columns (NOT indexed):
187
- -- content TEXT — full markdown text; the proposal payload body.
188
- -- frontmatter_json TEXT — JSON of parsed frontmatter (may be NULL when
189
- -- the content has no frontmatter block).
190
- --
191
- -- Extensible (metadata_json) columns:
192
- -- metadata_json TEXT — JSON object for future proposal fields.
193
- -- Current fields stored here: sourceRun,
194
- -- review, confidence, gateDecision (#577),
195
- -- backupContent, eligibilitySource.
196
- --
197
- -- ADD COLUMN extension points (future migrations):
198
- -- ALTER TABLE proposals ADD COLUMN source_run TEXT DEFAULT NULL;
199
- -- ALTER TABLE proposals ADD COLUMN review_outcome TEXT DEFAULT NULL;
200
- -- ALTER TABLE proposals ADD COLUMN review_reason TEXT DEFAULT NULL;
201
- -- ALTER TABLE proposals ADD COLUMN review_decided_at TEXT DEFAULT NULL;
202
- -- ALTER TABLE proposals ADD COLUMN archived INTEGER NOT NULL DEFAULT 0;
203
- --
204
- CREATE TABLE IF NOT EXISTS proposals (
205
- id TEXT PRIMARY KEY,
206
- stash_dir TEXT NOT NULL,
207
- ref TEXT NOT NULL,
208
- status TEXT NOT NULL DEFAULT 'pending',
209
- source TEXT NOT NULL,
210
- created_at TEXT NOT NULL,
211
- updated_at TEXT NOT NULL,
212
- content TEXT NOT NULL DEFAULT '',
213
- frontmatter_json TEXT,
214
- metadata_json TEXT NOT NULL DEFAULT '{}'
215
- );
216
-
217
- -- Query patterns:
218
- -- SELECT … WHERE stash_dir = ? AND status = ? → idx_proposals_stash_status
219
- -- SELECT … WHERE ref = ? AND status = ? → idx_proposals_ref_status
220
- -- SELECT … WHERE id = ? → PK
221
- CREATE INDEX IF NOT EXISTS idx_proposals_stash_status
222
- ON proposals(stash_dir, status);
223
- CREATE INDEX IF NOT EXISTS idx_proposals_ref_status
224
- ON proposals(ref, status);
225
-
226
- -- ── task_history ─────────────────────────────────────────────────────────
227
- --
228
- -- Replaces per-task JSONL files under <cacheDir>/tasks/history/.
229
- --
230
- -- Indexed (query) columns:
231
- -- task_id TEXT PK — stable task identifier string.
232
- -- status TEXT — terminal status (e.g. "completed", "failed",
233
- -- "cancelled"); indexed for status-scoped queries.
234
- -- started_at TEXT — ISO-8601; indexed for time-range queries.
235
- -- target_kind TEXT — kind of the target entity (e.g. "issue",
236
- -- "workflow", "agent"); indexed for kind-scoped queries.
237
- -- target_ref TEXT — stable ref of the target entity; indexed for
238
- -- per-target history lookups.
239
- --
240
- -- Non-indexed time columns:
241
- -- completed_at TEXT — ISO-8601 or NULL if still running.
242
- -- failed_at TEXT — ISO-8601 or NULL.
243
- --
244
- -- Non-indexed diagnostic columns:
245
- -- log_path TEXT — absolute path to the task log file, if any.
246
- --
247
- -- Extensible (metadata_json) columns:
248
- -- metadata_json TEXT — JSON object for future task fields (exit_code,
249
- -- runner, priority, parent_task_id, …).
250
- --
251
- -- ADD COLUMN extension points (future migrations):
252
- -- ALTER TABLE task_history ADD COLUMN exit_code INTEGER DEFAULT NULL;
253
- -- ALTER TABLE task_history ADD COLUMN runner TEXT DEFAULT NULL;
254
- -- ALTER TABLE task_history ADD COLUMN parent_task_id TEXT DEFAULT NULL;
255
- -- ALTER TABLE task_history ADD COLUMN priority INTEGER NOT NULL DEFAULT 0;
256
- --
257
- CREATE TABLE IF NOT EXISTS task_history (
258
- task_id TEXT PRIMARY KEY,
259
- status TEXT NOT NULL,
260
- started_at TEXT NOT NULL,
261
- completed_at TEXT,
262
- failed_at TEXT,
263
- log_path TEXT,
264
- target_kind TEXT,
265
- target_ref TEXT,
266
- metadata_json TEXT NOT NULL DEFAULT '{}'
267
- );
268
-
269
- -- Query patterns:
270
- -- SELECT … WHERE task_id = ? → PK
271
- -- SELECT … WHERE started_at >= ? AND started_at <= ? → idx_task_history_started
272
- -- SELECT … WHERE target_kind = ? AND target_ref = ? → idx_task_history_target
273
- -- SELECT … WHERE status = ? → idx_task_history_status
274
- CREATE INDEX IF NOT EXISTS idx_task_history_started
275
- ON task_history(started_at);
276
- CREATE INDEX IF NOT EXISTS idx_task_history_target
277
- ON task_history(target_kind, target_ref);
278
- CREATE INDEX IF NOT EXISTS idx_task_history_status
279
- ON task_history(status);
280
- `,
281
- },
282
- // Migration 002 — fix task_history to be a true per-run log.
283
- //
284
- // Migration 001 used task_id as PRIMARY KEY, meaning each task had exactly
285
- // one row and every new run overwrote the previous one. This silently
286
- // discarded all historical runs — the opposite of a history table.
287
- //
288
- // This migration recreates the table with an AUTOINCREMENT id so each run
289
- // appends a new row. The old single-row table is renamed to _old, the new
290
- // table is created, data is copied, and the old table is dropped.
291
- {
292
- id: "002-task-history-per-run",
293
- up: `
294
- ALTER TABLE task_history RENAME TO task_history_v1;
295
-
296
- CREATE TABLE task_history (
297
- id INTEGER PRIMARY KEY AUTOINCREMENT,
298
- task_id TEXT NOT NULL,
299
- status TEXT NOT NULL,
300
- started_at TEXT NOT NULL,
301
- completed_at TEXT,
302
- failed_at TEXT,
303
- log_path TEXT,
304
- target_kind TEXT,
305
- target_ref TEXT,
306
- metadata_json TEXT NOT NULL DEFAULT '{}'
307
- );
308
-
309
- INSERT INTO task_history
310
- (task_id, status, started_at, completed_at, failed_at,
311
- log_path, target_kind, target_ref, metadata_json)
312
- SELECT task_id, status, started_at, completed_at, failed_at,
313
- log_path, target_kind, target_ref, metadata_json
314
- FROM task_history_v1;
315
-
316
- DROP TABLE task_history_v1;
317
-
318
- -- Unique constraint: same task cannot have two runs with the same start time.
319
- CREATE UNIQUE INDEX IF NOT EXISTS idx_task_history_run
320
- ON task_history(task_id, started_at);
321
- CREATE INDEX IF NOT EXISTS idx_task_history_task_id
322
- ON task_history(task_id);
323
- CREATE INDEX IF NOT EXISTS idx_task_history_started
324
- ON task_history(started_at);
325
- CREATE INDEX IF NOT EXISTS idx_task_history_target
326
- ON task_history(target_kind, target_ref);
327
- CREATE INDEX IF NOT EXISTS idx_task_history_status
328
- ON task_history(status);
329
- `,
330
- },
331
- // ── Migration 003 — improve_runs ────────────────────────────────────────────
332
- //
333
- // Records every `akm improve` invocation as a durable row, replacing the
334
- // legacy `<stash>/.akm/runs/<runId>/improve-result.json` artifact files.
335
- //
336
- // The `dry_run` column is FIRST-CLASS and indexed so productivity audits can
337
- // cleanly filter dry-run probes out of real-run analyses without parsing
338
- // `result_json`. The dry-run/real-run artifact-trap (recorded in
339
- // feedback_akm_dryrun_artifact_trap) was the specific motivating bug.
340
- //
341
- // Indexed (query) columns:
342
- // id TEXT PK — runId (`buildImproveRunId()` output).
343
- // started_at TEXT — ISO-8601; indexed for time-range queries.
344
- // stash_dir TEXT — absolute stash root; multi-stash scoping.
345
- // dry_run INTEGER — 0/1; indexed for productivity audits.
346
- // scope_mode TEXT — "all" | "type" | "ref"; indexed via composite
347
- // with stash_dir for stash-scoped scope queries.
348
- //
349
- // Non-indexed payload:
350
- // completed_at TEXT — ISO-8601 or NULL if interrupted.
351
- // profile TEXT — improve profile name (nullable).
352
- // scope_value TEXT — type name or asset ref (nullable).
353
- // guidance TEXT — user-provided guidance text, if any.
354
- // ok INTEGER — 0/1; whether the run produced ok=true.
355
- // result_json TEXT — full AkmImproveResult JSON.
356
- // metrics_json TEXT — aggregate counts extracted from result, cheap
357
- // to query without parsing result_json.
358
- //
359
- // Extensible (metadata_json) columns:
360
- // metadata_json TEXT — JSON object for future improve-run fields.
361
- //
362
- // ADD COLUMN extension points (future migrations):
363
- // ALTER TABLE improve_runs ADD COLUMN duration_ms INTEGER DEFAULT NULL;
364
- // ALTER TABLE improve_runs ADD COLUMN host TEXT DEFAULT NULL;
365
- //
366
- // TTL: rows where started_at < NOW() - 90 days can be deleted by
367
- // `purgeOldImproveRuns()`. No automatic deletion occurs here.
368
- {
369
- id: "003-improve-runs",
370
- up: `
371
- CREATE TABLE IF NOT EXISTS improve_runs (
372
- id TEXT PRIMARY KEY,
373
- started_at TEXT NOT NULL,
374
- completed_at TEXT,
375
- stash_dir TEXT NOT NULL,
376
- dry_run INTEGER NOT NULL DEFAULT 0,
377
- profile TEXT,
378
- scope_mode TEXT NOT NULL,
379
- scope_value TEXT,
380
- guidance TEXT,
381
- ok INTEGER NOT NULL,
382
- result_json TEXT NOT NULL,
383
- metrics_json TEXT,
384
- metadata_json TEXT NOT NULL DEFAULT '{}'
385
- );
386
-
387
- -- Query patterns supported:
388
- -- SELECT … WHERE started_at >= ? AND started_at <= ?
389
- -- → idx_improve_runs_started
390
- -- SELECT … WHERE dry_run = 0
391
- -- → idx_improve_runs_dry_run (productivity audits filter trap)
392
- -- SELECT … WHERE stash_dir = ? AND scope_mode = ?
393
- -- → idx_improve_runs_stash_scope
394
- CREATE INDEX IF NOT EXISTS idx_improve_runs_started
395
- ON improve_runs(started_at);
396
- CREATE INDEX IF NOT EXISTS idx_improve_runs_dry_run
397
- ON improve_runs(dry_run);
398
- CREATE INDEX IF NOT EXISTS idx_improve_runs_stash_scope
399
- ON improve_runs(stash_dir, scope_mode);
400
- `,
401
- },
402
- // ── Migration 004 — extract_sessions_seen ───────────────────────────────────
403
- //
404
- // Tracks which platform sessions the extractor has processed, so the discovery
405
- // pass in `akm extract --since <window>` skips sessions whose content hasn't
406
- // changed since the last successful run. Replaces the akm-plugin
407
- // session-checkpoint hook's implicit "write-once" memory of what's been
408
- // captured — but persistent and queryable.
409
- //
410
- // Indexed (query) columns:
411
- // harness TEXT — harness name (claude-code, opencode, ...).
412
- // session_id TEXT — platform-native session identifier.
413
- // processed_at TEXT — ISO-8601 UTC; when extract last ran on this session.
414
- // session_ended_at TEXT — session.endedAt at processing time. When a
415
- // later listSessions reports a *newer* endedAt
416
- // for the same session_id, the extractor
417
- // re-processes the appended events.
418
- // outcome TEXT — "candidates_queued" | "no_candidates" |
419
- // "skipped" | "failed".
420
- //
421
- // Non-indexed columns:
422
- // candidate_count INTEGER — number of candidates the LLM produced.
423
- // proposal_count INTEGER — number of proposals actually queued
424
- // (candidates may fail downstream validation).
425
- // rationale TEXT — for "no_candidates", the LLM's explanation.
426
- // source_run TEXT — sourceRun id for PROV-DM traceability.
427
- // metadata_json TEXT — future-proofing (pre-filter stats, LLM
428
- // model+version, prompt token count, etc.).
429
- //
430
- // PK: (harness, session_id) — one row per session per harness. A re-extract
431
- // updates the row in place via INSERT OR REPLACE.
432
- //
433
- // TTL: no automatic deletion. Sessions stay tracked as long as the source
434
- // session files exist on disk. Operator can `DELETE FROM extract_sessions_seen
435
- // WHERE processed_at < ?` for cleanup if desired.
436
- {
437
- id: "004-extract-sessions-seen",
438
- up: `
439
- CREATE TABLE IF NOT EXISTS extract_sessions_seen (
440
- harness TEXT NOT NULL,
441
- session_id TEXT NOT NULL,
442
- processed_at TEXT NOT NULL,
443
- session_ended_at TEXT,
444
- outcome TEXT NOT NULL,
445
- candidate_count INTEGER NOT NULL DEFAULT 0,
446
- proposal_count INTEGER NOT NULL DEFAULT 0,
447
- rationale TEXT,
448
- source_run TEXT,
449
- metadata_json TEXT NOT NULL DEFAULT '{}',
450
- PRIMARY KEY (harness, session_id)
451
- );
452
-
453
- -- Query patterns:
454
- -- SELECT … WHERE harness = ? → idx_extract_sessions_harness
455
- -- SELECT … WHERE processed_at >= ? → idx_extract_sessions_processed
456
- -- SELECT … WHERE harness = ? AND session_id = ? → PK
457
- CREATE INDEX IF NOT EXISTS idx_extract_sessions_harness
458
- ON extract_sessions_seen(harness);
459
- CREATE INDEX IF NOT EXISTS idx_extract_sessions_processed
460
- ON extract_sessions_seen(processed_at);
461
- `,
462
- },
463
- // ── Migration 005 — proposal_fs_imports ─────────────────────────────────────
464
- //
465
- // One-shot ledger for the legacy filesystem→SQLite proposal import (#578).
466
- //
467
- // Before 0.9.0 the proposal queue lived as per-uuid JSON directories under
468
- // `<stashDir>/.akm/proposals/` and the `proposals` table (created in 001) was
469
- // dead weight. 0.9.0 makes the table canonical; the first proposal operation
470
- // against a stash imports any legacy `proposal.json` files it finds (INSERT
471
- // OR IGNORE, so re-runs never duplicate) and records the stash here so later
472
- // invocations skip the directory walk entirely.
473
- //
474
- // Indexed (query) columns:
475
- // stash_dir TEXT PK — absolute stash root the import ran against.
476
- //
477
- // Non-indexed columns:
478
- // imported_at TEXT — ISO-8601 UTC; when the import completed.
479
- // imported_count INTEGER — rows actually inserted by the import.
480
- {
481
- id: "005-proposal-fs-imports",
482
- up: `
483
- CREATE TABLE IF NOT EXISTS proposal_fs_imports (
484
- stash_dir TEXT PRIMARY KEY,
485
- imported_at TEXT NOT NULL,
486
- imported_count INTEGER NOT NULL DEFAULT 0
487
- );
488
- `,
489
- },
490
- // ── Migration 006 — pending proposal lookup index ──────────────────────────
491
- //
492
- // Supports the transaction-scoped dedup / queue-mutation hardening added in
493
- // 0.9.x. The queue now acquires an IMMEDIATE write transaction before it
494
- // reads pending proposals, so the hot path is a stash-scoped `status='pending'
495
- // AND ref=?` probe followed by an update/insert. This composite index keeps
496
- // that lookup index-covered under contention.
497
- {
498
- id: "006-proposals-pending-ref-source",
499
- up: `
500
- CREATE INDEX IF NOT EXISTS idx_proposals_stash_status_ref_source
501
- ON proposals(stash_dir, status, ref, source);
502
- `,
503
- },
504
- // ── Migration 007 — consolidation_judged ────────────────────────────────────
505
- //
506
- // Judged-state cache for nightly consolidation (#581). Lets one consolidation
507
- // run cover the FULL memory corpus cheaply by SKIPPING memories already judged
508
- // with unchanged content, instead of narrowing to a recent time-window slice
509
- // (which leaves a near-duplicate backlog the corpus can never clear).
510
- //
511
- // The consolidate LLM judging loop UPSERTs a row for every memory it saw in a
512
- // successfully-judged chunk; the next run hashes each candidate's current
513
- // content and skips it when the hash equals the cached `content_hash`
514
- // (judged-unchanged → no re-judge). A memory whose content changed produces a
515
- // new hash and is re-judged. This converts coverage from O(window) to
516
- // O(changed/new). DEFAULT OFF — gated behind
517
- // `processes.consolidate.judgedCache.enabled`; when the feature is off this
518
- // table is never read or written and behaviour is byte-identical to today.
519
- //
520
- // Indexed (query) columns:
521
- // entry_key TEXT PK — `memory:<name>` ref; one row per judged memory.
522
- //
523
- // Non-indexed columns:
524
- // content_hash TEXT — sha256 of the frontmatter-stripped, trimmed body.
525
- // judged_at TEXT — ISO-8601 UTC; when the memory was last judged.
526
- // outcome TEXT — coarse outcome of the last judge ("actioned" |
527
- // "no_action"); observability only, never gates.
528
- //
529
- // TTL: no automatic deletion. Rows for memories deleted on disk become
530
- // harmless dead entries (their entry_key never recurs); operators can prune
531
- // with `DELETE FROM consolidation_judged WHERE judged_at < ?` if desired.
532
- {
533
- id: "007-consolidation-judged",
534
- up: `
535
- CREATE TABLE IF NOT EXISTS consolidation_judged (
536
- entry_key TEXT PRIMARY KEY,
537
- content_hash TEXT NOT NULL,
538
- judged_at TEXT NOT NULL,
539
- outcome TEXT NOT NULL
540
- );
541
- `,
542
- },
543
- // ── Migration 008 — body_embeddings ─────────────────────────────────────────
544
- //
545
- // cacheHash-keyed body-embedding cache (WS-3a). Stores the embedding of the
546
- // case-preserving stripped body so the dedup pre-pass and the consolidation
547
- // clustering step share one computed vector per unique body, eliminating
548
- // redundant embedding calls across runs.
549
- //
550
- // Design:
551
- // - PK is the `cacheHash` (sha256 of the stripped, case-preserving body).
552
- // - `embedding` is a raw BLOB storing a Float32 array (384 floats × 4 B =
553
- // 1 536 B per entry for the default bge-small-en-v1.5 model; ~20 MB at
554
- // 13 k memories). This matches the native wire format and avoids JSON
555
- // round-trip overhead.
556
- // - `model_id` is MANDATORY. On mismatch (model changed) the entire table
557
- // is dropped and rebuilt — stale vectors from the wrong metric space would
558
- // produce silent cosine errors.
559
- // - `created_at` is an INTEGER Unix ms timestamp for lazy orphan purges.
560
- //
561
- // Writes: one bulk `WHERE content_hash IN (…)` lookup → embed only misses →
562
- // upsert all results in one transaction per run.
563
- //
564
- // TTL: no automatic row deletion. Orphaned rows for bodies no longer in the
565
- // stash stay until an operator prunes them. The table is ~1.5 KB per row
566
- // (~20 MB at 13 k memories — acceptable).
567
- {
568
- id: "008-body-embeddings",
569
- up: `
570
- CREATE TABLE IF NOT EXISTS body_embeddings (
571
- content_hash TEXT PRIMARY KEY,
572
- embedding BLOB NOT NULL,
573
- model_id TEXT NOT NULL,
574
- created_at INTEGER NOT NULL
575
- );
576
- `,
577
- },
578
- // ── Migration 009 — asset_salience (WS-1 salience vector) ───────────────────
579
- //
580
- // Per-asset salience vector persisted in state.db (canonical store).
581
- //
582
- // Three independently-stored, independently-decayable sub-scores:
583
- // encoding_salience — intrinsic importance (Gap 1; v1 = type-weight stub).
584
- // outcome_salience — differential usefulness (WS-2; 0 until that lands).
585
- // retrieval_salience — frequency × recency (the decayable term).
586
- //
587
- // Plus the scalar projection for ranking:
588
- // rank_score = (w_e·encoding + w_o·outcome + w_r·retrieval) × sizePenalty,
589
- // normalized [0,1]. Every selector reads rank_score; individual sub-scores
590
- // are available for telemetry and per-dimension thresholding.
591
- //
592
- // Plasticity column:
593
- // consecutive_no_ops INTEGER — number of consecutive improve cycles where
594
- // this asset produced a no-op (reflect/distill produced no change).
595
- // Dampens CONSOLIDATION-SELECTION only — intentionally NOT applied to
596
- // rank_score (stable assets stay retrievable but skip LLM merge passes).
597
- //
598
- // updated_at is an INTEGER Unix-ms timestamp for recency queries.
599
- //
600
- // The canonical store is state.db, not frontmatter. An optional frontmatter
601
- // mirror of the stable encodingSalience is allowed for portability (#608).
602
- //
603
- // TTL: rows are overwritten on every run; orphaned rows for deleted assets
604
- // accumulate harmlessly until an operator prunes them.
605
- {
606
- id: "009-asset-salience",
607
- up: `
608
- CREATE TABLE IF NOT EXISTS asset_salience (
609
- asset_ref TEXT PRIMARY KEY,
610
- encoding_salience REAL NOT NULL DEFAULT 0.5,
611
- outcome_salience REAL NOT NULL DEFAULT 0.0,
612
- retrieval_salience REAL NOT NULL DEFAULT 0.0,
613
- rank_score REAL NOT NULL DEFAULT 0.0,
614
- consecutive_no_ops INTEGER NOT NULL DEFAULT 0,
615
- updated_at INTEGER NOT NULL DEFAULT 0
616
- );
617
-
618
- -- Hot path: sort / filter by rank_score for selector queries.
619
- CREATE INDEX IF NOT EXISTS idx_asset_salience_rank
620
- ON asset_salience(rank_score DESC);
621
- `,
622
- },
623
- // ── Migration 010 — asset_outcome (WS-2 outcome loop) ───────────────────────
624
- //
625
- // Per-asset outcome loop persisted in state.db (S2 seam, WS-2).
626
- //
627
- // Stores the differential "was this retrieval useful" signal so the salience
628
- // vector's `outcomeSalience` sub-score (WS-1 `W_OUTCOME` term) is non-zero.
629
- //
630
- // Columns:
631
- // asset_ref TEXT PK — `type:name` asset ref (FK to asset_salience).
632
- // last_retrieved_at INTEGER — Unix-ms of the most recent retrieval.
633
- // retrieval_count INTEGER — total retrieval count from the index DB.
634
- // expected_retrieval_rate REAL — EMA-smoothed expected count per cycle.
635
- // negative_feedback_count INTEGER — cumulative negative-feedback events.
636
- // accepted_change_count INTEGER — cumulative accepted proposals.
637
- // review_pressure INTEGER — #613 pressure counter: repeated low-satisfaction
638
- // retrievals increment it; feeds outcomeSalience.
639
- // outcome_score REAL — differential outcome signal (can be negative).
640
- // updated_at INTEGER — Unix-ms timestamp of last update.
641
- //
642
- // Design:
643
- // - outcome_score is differential (prediction-error shaped), NOT a raw count,
644
- // so it rewards assets that are retrieved MORE than their rolling mean AND
645
- // accepted for change when retrieved. See outcome-loop.ts for the formula.
646
- // - review_pressure (#613): repeated negative-feedback retrievals raise it,
647
- // non-negative cycles decay it. Never mutates asset content directly.
648
- // - warm_start: seeded from utility EMA at row creation, clipped to [0, 0.3]
649
- // so the first negative delta does not cause a spurious rank inversion.
650
- // - Orphaned rows (deleted assets) accumulate harmlessly; operators can prune
651
- // with `DELETE FROM asset_outcome WHERE updated_at < ?` if desired.
652
- {
653
- id: "010-asset-outcome",
654
- up: `
655
- CREATE TABLE IF NOT EXISTS asset_outcome (
656
- asset_ref TEXT PRIMARY KEY,
657
- last_retrieved_at INTEGER NOT NULL DEFAULT 0,
658
- retrieval_count INTEGER NOT NULL DEFAULT 0,
659
- expected_retrieval_rate REAL NOT NULL DEFAULT 0.0,
660
- negative_feedback_count INTEGER NOT NULL DEFAULT 0,
661
- accepted_change_count INTEGER NOT NULL DEFAULT 0,
662
- review_pressure INTEGER NOT NULL DEFAULT 0,
663
- outcome_score REAL NOT NULL DEFAULT 0.0,
664
- updated_at INTEGER NOT NULL DEFAULT 0
665
- );
666
-
667
- -- Hot path: sort assets by review_pressure DESC for #613 admission.
668
- CREATE INDEX IF NOT EXISTS idx_asset_outcome_review_pressure
669
- ON asset_outcome(review_pressure DESC);
670
-
671
- -- Secondary: sort by outcome_score DESC for outcomeSalience reads.
672
- CREATE INDEX IF NOT EXISTS idx_asset_outcome_score
673
- ON asset_outcome(outcome_score DESC);
674
- `,
675
- },
676
- // ── Migration 011 — asset_salience: homeostatic_demoted_at column ─────────────
677
- //
678
- // WS-3b step 0a (homeostatic demotion). Records the last time `retrievalSalience`
679
- // was demoted for this asset so:
680
- // (a) Each run can identify assets that have been demoted but not yet
681
- // re-retrieved (they stay in the demoted state until a retrieval
682
- // re-promotes them via `upsertAssetSalience`).
683
- // (b) The homeostatic pass can log "N assets demoted this run".
684
- //
685
- // NULL = never demoted (or was re-promoted after last demotion, since a fresh
686
- // `upsertAssetSalience` call clears the flag by updating retrieval_salience
687
- // from live data rather than the demoted value — the column is informational,
688
- // not the canonical source of the salience value).
689
- {
690
- id: "011-asset-salience-homeostatic-demoted-at",
691
- up: `
692
- ALTER TABLE asset_salience ADD COLUMN homeostatic_demoted_at INTEGER DEFAULT NULL;
693
- `,
694
- },
695
- // ── Migration 012 — improve_gate_thresholds (WS-4 per-phase threshold store) ─
696
- //
697
- // Persists the auto-tuned accept-gate threshold PER PHASE so that each phase
698
- // (reflect, distill, extract, consolidate) maintains its own calibrated
699
- // threshold rather than sharing a single global `options.autoAccept`.
700
- //
701
- // Schema:
702
- // phase TEXT PK — phase label, e.g. "reflect", "distill", "extract",
703
- // "consolidate".
704
- // threshold INTEGER — tuned threshold (0-100), matches the integer
705
- // scale used everywhere else in the gate pipeline.
706
- // updated_at INTEGER — Unix milliseconds of the last update.
707
- //
708
- // `makeGateConfig` reads the stored threshold for its phase (falling back to
709
- // the caller-supplied `globalThreshold` when no row exists yet). WS-4's
710
- // `persistPhaseThreshold` writes it after each auto-tune step.
711
- {
712
- id: "012-improve-gate-thresholds",
713
- up: `
714
- CREATE TABLE IF NOT EXISTS improve_gate_thresholds (
715
- phase TEXT NOT NULL PRIMARY KEY,
716
- threshold INTEGER NOT NULL,
717
- updated_at INTEGER NOT NULL
718
- );
719
- `,
720
- },
721
- // ── Migration 013 — extract_sessions_seen: content_hash column (#602) ────────
722
- //
723
- // Replaces the brittle timestamp-based incrementality (`session_ended_at`,
724
- // compared against the live session metadata) with a content hash. The old
725
- // clock/timestamp logic caused the Jun 11-12 double-extract + over-throttle
726
- // incident (clock skew / out-of-order endedAt both double-processed AND
727
- // over-suppressed sessions). The hash makes the skip decision byte-exact and
728
- // clock-independent.
729
- //
730
- // Additive ADD COLUMN (migration-safe; mirrors migration 011's style). All
731
- // pre-existing rows read back `content_hash = NULL`, which the skip logic
732
- // treats as "seen before content-hash tracking existed → process once to
733
- // backfill", after which the row gets a real hash and becomes hash-stable.
734
- // Never mutate migration 004 (the original table) — this column is appended.
735
- {
736
- id: "013-extract-sessions-content-hash",
737
- up: `
738
- ALTER TABLE extract_sessions_seen ADD COLUMN content_hash TEXT DEFAULT NULL;
739
- `,
740
- },
741
- // ── Migration 014 — recombine_hypotheses (#625 confirmation count) ───────────
742
- //
743
- // Second-pass promotion ledger for the recombine pass. The first pass (#609)
744
- // only ever emits `type: hypothesis` proposals; this table tracks how many
745
- // CONSECUTIVE runs re-induced the SAME generalization (keyed by the
746
- // deterministic `deriveRecombineLessonRef` value — a hash of the sorted
747
- // member entryKeys). Once `consecutive_count >= confirmThreshold`, the run
748
- // promotes the generalization to a `type: lesson` proposal (through the same
749
- // proposal queue + quality gate). A hypothesis NOT re-induced in a run has its
750
- // consecutive streak reset (decay-to-zero), so confirmation is per exact
751
- // member-set and conservative.
752
- //
753
- // Indexed columns:
754
- // hypothesis_ref TEXT PK — the `lesson:recombined/<slug>-<hash>` ref; one
755
- // row per re-inducible generalization. The ref is
756
- // the promotion TARGET (a lesson in both the
757
- // hypothesis and promoted states), so the ref never
758
- // encodes the proposal type.
759
- // last_seen_at TEXT (idx) — for forensic / pruning queries.
760
- //
761
- // Non-indexed columns:
762
- // signature TEXT — the cluster's shared relatedness signal (tag /
763
- // entity) at induction time; forensics only.
764
- // member_key TEXT — sorted member entryKeys joined; the membership
765
- // fingerprint behind the ref hash. Stored so a
766
- // membership change (which yields a DIFFERENT ref)
767
- // is auditable.
768
- // consecutive_count INTEGER — current confirmation streak (reset on decay
769
- // and on promotion).
770
- // first_seen_at TEXT — ISO-8601 UTC of the first induction.
771
- // last_run TEXT — sourceRun token of the last induction; the
772
- // same-run idempotency guard.
773
- // promoted_at TEXT — non-null once promoted; guards against
774
- // double-promoting on every subsequent run.
775
- // metadata_json TEXT — reserved for future forensics; defaults to '{}'.
776
- {
777
- id: "014-recombine-hypotheses",
778
- up: `
779
- CREATE TABLE IF NOT EXISTS recombine_hypotheses (
780
- hypothesis_ref TEXT PRIMARY KEY,
781
- signature TEXT NOT NULL,
782
- member_key TEXT NOT NULL,
783
- consecutive_count INTEGER NOT NULL DEFAULT 0,
784
- first_seen_at TEXT NOT NULL,
785
- last_seen_at TEXT NOT NULL,
786
- last_run TEXT,
787
- promoted_at TEXT,
788
- metadata_json TEXT NOT NULL DEFAULT '{}'
789
- );
790
- CREATE INDEX IF NOT EXISTS idx_recombine_hypotheses_last_seen
791
- ON recombine_hypotheses(last_seen_at);
792
- `,
793
- },
794
- // ── Migration 015 — asset_salience: encoding_source provenance column (#644) ──
795
- //
796
- // Records HOW the stored `encoding_salience` was derived so an improve run's
797
- // type-weight fallback can no longer clobber a real content-derived score:
798
- //
799
- // "content" — written by the distill path from `scoreEncodingSalience`
800
- // (novelty·0.40 + magnitude·0.35 + predictionError·0.25).
801
- // "type-stub" — written by `computeSalience`'s `DEFAULT_TYPE_ENCODING_WEIGHTS`
802
- // fallback (no content-based score available for this ref yet).
803
- // NULL — legacy row written before this migration; provenance unknown.
804
- // Treated as a stub by readers UNLESS its stored value differs
805
- // from the pure type-weight (best-effort heuristic for old data).
806
- //
807
- // Why a column rather than inference: before #644, every improve run overwrote
808
- // the distill-written content score with the type-weight stub, so the stored
809
- // value alone cannot distinguish a real score from a stub. The provenance flag
810
- // is the single source of truth going forward; `upsertAssetSalience` refuses to
811
- // lower a "content" row to a "type-stub" so the high-salience gate (#608) keys
812
- // on genuine novelty/magnitude/prediction-error, not the asset type.
813
- {
814
- id: "015-asset-salience-encoding-source",
815
- up: `
816
- ALTER TABLE asset_salience ADD COLUMN encoding_source TEXT DEFAULT NULL;
817
- `,
818
- },
819
- ];
104
+ export function withStateDb(fn, opts) {
105
+ return withManagedDb(() => openStateDatabase(opts?.path), fn, opts);
106
+ }
820
107
  /**
821
- * Apply every pending migration in a single transaction per migration.
822
- *
823
- * Delegates to the shared SQLite migration engine; state.db has no
824
- * pre-versioning bootstrap step, so no `bootstrap` hook is passed.
825
- *
826
- * Called automatically by `openStateDatabase()`.
108
+ * Async sibling of {@link withStateDb} for `fn`s that hold the handle across
109
+ * an `await` (the sync version would close it too early). Same borrow/own +
110
+ * optional `path` override semantics.
827
111
  */
828
- export function runMigrations(db) {
829
- runSqliteMigrations(db, MIGRATIONS);
112
+ export function withStateDbAsync(fn, opts) {
113
+ return withManagedDbAsync(() => openStateDatabase(opts?.path), fn, opts);
830
114
  }
115
+ // ── Migration engine ─────────────────────────────────────────────────────────
116
+ //
117
+ // The MIGRATIONS registry + runMigrations live in ./state/migrations (the single
118
+ // append-only ordered source of truth). Imported for internal use by
119
+ // openStateDatabase + re-exported so existing importers keep resolving.
120
+ import { runMigrations } from "./state/migrations.js";
121
+ export { runMigrations } from "./state/migrations.js";
831
122
  /**
832
123
  * Convert a raw `EventRow` from the database to the public `EventEnvelope`
833
124
  * interface used throughout the events module.
@@ -1897,45 +1188,3 @@ export function upsertBodyEmbeddings(db, entries) {
1897
1188
  }
1898
1189
  })();
1899
1190
  }
1900
- // ── registry_index_cache (goes in index.db, not state.db) ───────────────────
1901
- /**
1902
- * DDL for the `registry_index_cache` table that lives in the EXISTING index.db
1903
- * (managed by src/indexer/db/db.ts).
1904
- *
1905
- * Design: uses the same migration-safe ADD COLUMN approach. The table is
1906
- * created with CREATE TABLE IF NOT EXISTS so it is safe to call inside
1907
- * ensureSchema() or as a standalone migration.
1908
- *
1909
- * Purpose: caches the result of resolving and fetching remote registry stash
1910
- * indexes so `akm search` does not hit the network on every invocation.
1911
- *
1912
- * Indexed (query) columns:
1913
- * registry_url TEXT PK — canonical URL of the registry; cache key.
1914
- * fetched_at TEXT — ISO-8601; used to detect stale entries (TTL).
1915
- * etag TEXT — HTTP ETag for conditional GET (If-None-Match).
1916
- * last_modified TEXT — HTTP Last-Modified for conditional GET.
1917
- *
1918
- * Non-indexed payload:
1919
- * index_json TEXT — JSON blob of the fetched registry index document.
1920
- *
1921
- * ADD COLUMN extension points (future migrations in db.ts):
1922
- * ALTER TABLE registry_index_cache ADD COLUMN schema_version INTEGER DEFAULT 1;
1923
- * ALTER TABLE registry_index_cache ADD COLUMN kit_count INTEGER DEFAULT NULL;
1924
- * ALTER TABLE registry_index_cache ADD COLUMN error_message TEXT DEFAULT NULL;
1925
- *
1926
- * To add this table to index.db, call ensureRegistryIndexCacheSchema(db) from
1927
- * within ensureSchema() in src/indexer/db/db.ts, or add it as a new CREATE TABLE
1928
- * IF NOT EXISTS block inside the existing ensureSchema() call.
1929
- */
1930
- export const REGISTRY_INDEX_CACHE_DDL = `
1931
- CREATE TABLE IF NOT EXISTS registry_index_cache (
1932
- registry_url TEXT PRIMARY KEY,
1933
- fetched_at TEXT NOT NULL,
1934
- etag TEXT,
1935
- last_modified TEXT,
1936
- index_json TEXT NOT NULL DEFAULT '{}'
1937
- );
1938
-
1939
- CREATE INDEX IF NOT EXISTS idx_registry_cache_fetched
1940
- ON registry_index_cache(fetched_at);
1941
- `;