akm-cli 0.9.0-beta.9 → 0.9.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +592 -0
- package/README.md +12 -4
- package/dist/akm +38 -0
- package/dist/akm-migrate-storage +38 -0
- package/dist/assets/help/help-improve.md +9 -6
- package/dist/assets/hints/cli-hints-full.md +6 -5
- package/dist/assets/profiles/default.json +9 -4
- package/dist/assets/profiles/frequent.json +1 -1
- package/dist/assets/profiles/memory-focus.json +1 -1
- package/dist/assets/profiles/proactive-maintenance.json +25 -0
- package/dist/assets/profiles/quick.json +1 -1
- package/dist/assets/profiles/recombine-only.json +21 -0
- package/dist/assets/profiles/reflect-distill.json +30 -0
- package/dist/assets/profiles/synthesize.json +15 -0
- package/dist/assets/profiles/thorough.json +1 -1
- package/dist/assets/prompts/consolidate-system.md +23 -0
- package/dist/assets/prompts/contradiction-judge.md +33 -0
- package/dist/assets/prompts/distill-knowledge-system.md +22 -0
- package/dist/assets/prompts/distill-lesson-system.md +36 -0
- package/dist/assets/prompts/extract-session.md +11 -3
- package/dist/assets/prompts/graph-extract-system.md +1 -0
- package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
- package/dist/assets/prompts/memory-infer-system.md +1 -0
- package/dist/assets/prompts/memory-infer-user.md +5 -0
- package/dist/assets/prompts/metadata-enhance-system.md +1 -0
- package/dist/assets/prompts/procedural-system.md +44 -0
- package/dist/assets/prompts/recombine-system.md +40 -0
- package/dist/assets/prompts/staleness-detect-system.md +6 -0
- package/dist/assets/prompts/validate-summary-judge.md +1 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
- package/dist/assets/templates/html/health.html +281 -111
- package/dist/assets/wiki/ingest-workflow-template.md +45 -16
- package/dist/assets/wiki/schema-template.md +4 -4
- package/dist/cli/clack.js +56 -0
- package/dist/cli/config-migrate.js +7 -1
- package/dist/cli/confirm.js +1 -1
- package/dist/cli/parse-args.js +46 -1
- package/dist/cli/shared.js +28 -0
- package/dist/cli.js +25 -21
- package/dist/commands/agent/agent-dispatch.js +3 -2
- package/dist/commands/agent/agent-support.js +0 -7
- package/dist/commands/agent/contribute-cli.js +26 -7
- package/dist/commands/config-cli.js +26 -13
- package/dist/commands/env/child-env.js +47 -0
- package/dist/commands/env/env-cli.js +220 -227
- package/dist/commands/env/env.js +14 -67
- package/dist/commands/env/secret-cli.js +140 -138
- package/dist/commands/feedback-cli.js +156 -155
- package/dist/commands/graph/graph-cli.js +5 -13
- package/dist/commands/graph/graph.js +3 -3
- package/dist/commands/health/advisories.js +151 -0
- package/dist/commands/health/checks.js +103 -16
- package/dist/commands/health/html-report.js +447 -81
- package/dist/commands/health/improve-metrics.js +771 -0
- package/dist/commands/health/llm-usage.js +65 -0
- package/dist/commands/health/md-report.js +103 -0
- package/dist/commands/health/metrics.js +278 -0
- package/dist/commands/health/stash-exposure.js +46 -0
- package/dist/commands/health/surfaces.js +216 -0
- package/dist/commands/health/task-runs.js +135 -0
- package/dist/commands/health/types.js +26 -0
- package/dist/commands/health/windows.js +195 -0
- package/dist/commands/health.js +91 -1091
- package/dist/commands/improve/anti-collapse.js +170 -0
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/collapse-detector.js +421 -0
- package/dist/commands/improve/consolidate/chunking.js +141 -0
- package/dist/commands/improve/consolidate/eligibility.js +64 -0
- package/dist/commands/improve/consolidate/merge.js +145 -0
- package/dist/commands/improve/consolidate/sanitize.js +231 -0
- package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
- package/dist/commands/improve/consolidate.js +1295 -1277
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill/content-repair.js +202 -0
- package/dist/commands/improve/distill/promote-memory.js +229 -0
- package/dist/commands/improve/distill/quality-gate.js +236 -0
- package/dist/commands/improve/distill-guards.js +127 -0
- package/dist/commands/improve/distill-promotion-policy.js +826 -167
- package/dist/commands/improve/distill.js +228 -605
- package/dist/commands/improve/eligibility.js +434 -0
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +179 -59
- package/dist/commands/improve/extract-prompt.js +54 -3
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +409 -43
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/hot-probation.js +45 -0
- package/dist/commands/improve/improve-auto-accept.js +157 -10
- package/dist/commands/improve/improve-cli.js +115 -73
- package/dist/commands/improve/improve-profiles.js +28 -8
- package/dist/commands/improve/improve-result-file.js +15 -25
- package/dist/commands/improve/improve-session.js +58 -0
- package/dist/commands/improve/improve.js +485 -2764
- package/dist/commands/improve/locks.js +154 -0
- package/dist/commands/improve/loop-stages.js +1100 -0
- package/dist/commands/improve/memory/memory-belief.js +14 -15
- package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
- package/dist/commands/improve/memory/memory-improve.js +27 -27
- package/dist/commands/improve/outcome-loop.js +270 -0
- package/dist/commands/improve/preparation.js +2002 -0
- package/dist/commands/improve/proactive-maintenance.js +37 -35
- package/dist/commands/improve/procedural.js +398 -0
- package/dist/commands/improve/recombine.js +818 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +206 -45
- package/dist/commands/improve/salience.js +455 -0
- package/dist/commands/improve/schema-similarity-gate.js +168 -0
- package/dist/commands/improve/shared.js +51 -0
- package/dist/commands/improve/triage.js +93 -0
- package/dist/commands/lint/agent-linter.js +19 -24
- package/dist/commands/lint/base-linter.js +173 -60
- package/dist/commands/lint/command-linter.js +19 -24
- package/dist/commands/lint/env-key-rules.js +38 -1
- package/dist/commands/lint/fact-linter.js +39 -0
- package/dist/commands/lint/index.js +31 -13
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +7 -2
- package/dist/commands/lint/task-linter.js +3 -3
- package/dist/commands/lint/workflow-linter.js +26 -1
- package/dist/commands/observability-cli.js +4 -4
- package/dist/commands/proposal/drain-policies.js +13 -4
- package/dist/commands/proposal/drain.js +45 -51
- package/dist/commands/proposal/legacy-import.js +115 -0
- package/dist/commands/proposal/proposal-cli.js +24 -34
- package/dist/commands/proposal/proposal.js +2 -1
- package/dist/commands/proposal/propose.js +8 -3
- package/dist/commands/proposal/repository.js +829 -0
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +93 -895
- package/dist/commands/read/curate.js +410 -111
- package/dist/commands/read/knowledge.js +10 -3
- package/dist/commands/read/remember-cli.js +133 -138
- package/dist/commands/read/search-cli.js +15 -8
- package/dist/commands/read/search.js +22 -11
- package/dist/commands/read/show.js +106 -14
- package/dist/commands/registry-cli.js +76 -87
- package/dist/commands/remember.js +11 -12
- package/dist/commands/sources/add-cli.js +91 -95
- package/dist/commands/sources/history.js +1 -1
- package/dist/commands/sources/init.js +66 -18
- package/dist/commands/sources/installed-stashes.js +11 -3
- package/dist/commands/sources/schema-repair.js +44 -46
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-add.js +7 -3
- package/dist/commands/sources/sources-cli.js +3 -3
- package/dist/commands/sources/stash-cli.js +19 -39
- package/dist/commands/sources/stash-skeleton.js +57 -8
- package/dist/commands/tasks/default-tasks.js +15 -2
- package/dist/commands/tasks/tasks-cli.js +20 -29
- package/dist/commands/tasks/tasks.js +39 -11
- package/dist/commands/wiki-cli.js +23 -38
- package/dist/commands/workflow-cli.js +15 -1
- package/dist/core/asset/asset-registry.js +3 -1
- package/dist/core/asset/asset-spec.js +21 -4
- package/dist/core/asset/frontmatter.js +188 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/authoring-rules.js +92 -0
- package/dist/core/common.js +4 -23
- package/dist/core/concurrent.js +10 -1
- package/dist/core/config/config-io.js +10 -1
- package/dist/core/config/config-migration.js +18 -40
- package/dist/core/config/config-schema.js +382 -62
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config.js +67 -22
- package/dist/core/deep-merge.js +38 -0
- package/dist/core/errors.js +1 -0
- package/dist/core/eval/rank-metrics.js +113 -0
- package/dist/core/events.js +4 -7
- package/dist/core/improve-types.js +47 -8
- package/dist/core/logs-db.js +14 -75
- package/dist/core/parse.js +36 -16
- package/dist/core/paths.js +18 -18
- package/dist/core/standards/resolve-standards-context.js +87 -0
- package/dist/core/standards/resolve-stash-standards.js +99 -0
- package/dist/core/standards/resolve-type-conventions.js +66 -0
- package/dist/core/state/migrations.js +770 -0
- package/dist/core/state-db.js +132 -1126
- package/dist/core/structured.js +69 -0
- package/dist/core/time.js +53 -0
- package/dist/core/warn.js +21 -0
- package/dist/core/write-source.js +37 -0
- package/dist/indexer/db/db.js +259 -769
- package/dist/indexer/db/entry-mapper.js +41 -0
- package/dist/indexer/db/graph-db.js +129 -86
- package/dist/indexer/db/llm-cache.js +2 -2
- package/dist/indexer/db/schema.js +516 -0
- package/dist/indexer/ensure-index.js +36 -92
- package/dist/indexer/feedback/utility-policy.js +75 -0
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/graph/graph-extraction.js +207 -4
- package/dist/indexer/index-writer-lock.js +18 -11
- package/dist/indexer/index-written-assets.js +105 -0
- package/dist/indexer/indexer.js +182 -204
- package/dist/indexer/passes/dir-staleness.js +114 -0
- package/dist/indexer/passes/memory-inference.js +13 -5
- package/dist/indexer/passes/metadata.js +20 -0
- package/dist/indexer/read-preflight.js +23 -0
- package/dist/indexer/search/db-search.js +89 -13
- package/dist/indexer/search/fts-query.js +51 -0
- package/dist/indexer/search/ranking-contributors.js +95 -9
- package/dist/indexer/search/ranking.js +79 -3
- package/dist/indexer/search/search-fields.js +6 -0
- package/dist/indexer/search/search-source.js +32 -21
- package/dist/indexer/search/semantic-status.js +4 -0
- package/dist/indexer/walk/matchers.js +9 -0
- package/dist/indexer/walk/walker.js +21 -13
- package/dist/integrations/agent/builders.js +39 -13
- package/dist/integrations/agent/config.js +20 -59
- package/dist/integrations/agent/detect.js +9 -0
- package/dist/integrations/agent/index.js +3 -19
- package/dist/integrations/agent/model-aliases.js +7 -2
- package/dist/integrations/agent/profiles.js +7 -1
- package/dist/integrations/agent/prompts.js +75 -9
- package/dist/integrations/agent/runner-dispatch.js +59 -0
- package/dist/integrations/agent/runner.js +13 -9
- package/dist/integrations/agent/spawn.js +69 -67
- package/dist/integrations/harnesses/claude/agent-builder.js +1 -1
- package/dist/integrations/harnesses/claude/index.js +2 -0
- package/dist/integrations/harnesses/claude/session-log.js +10 -0
- package/dist/integrations/harnesses/index.js +2 -3
- package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
- package/dist/integrations/harnesses/opencode/index.js +2 -0
- package/dist/integrations/harnesses/opencode/session-log.js +173 -3
- package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +98 -17
- package/dist/integrations/harnesses/types.js +1 -0
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/call-ai.js +2 -2
- package/dist/llm/client.js +34 -11
- package/dist/llm/embedder.js +67 -4
- package/dist/llm/embedders/cache.js +3 -1
- package/dist/llm/embedders/deterministic.js +66 -0
- package/dist/llm/embedders/local.js +73 -3
- package/dist/llm/feature-gate.js +16 -15
- package/dist/llm/graph-extract.js +67 -44
- package/dist/llm/memory-infer-impl.js +138 -0
- package/dist/llm/memory-infer.js +1 -127
- package/dist/llm/metadata-enhance.js +44 -31
- package/dist/llm/structured-call.js +49 -0
- package/dist/migrate-storage-node.mjs +8 -0
- package/dist/output/context.js +5 -5
- package/dist/output/renderers.js +85 -14
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/helpers.js +0 -3
- package/dist/output/shapes/passthrough.js +2 -1
- package/dist/output/text/helpers.js +29 -1
- package/dist/output/text/workflow.js +1 -0
- package/dist/registry/providers/skills-sh.js +21 -147
- package/dist/registry/providers/static-index.js +15 -157
- package/dist/registry/resolve.js +27 -9
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +2661 -2369
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +883 -596
- package/dist/setup/detect.js +9 -0
- package/dist/setup/legacy-config.js +106 -0
- package/dist/setup/prompt.js +57 -0
- package/dist/setup/providers.js +14 -0
- package/dist/setup/registry-stash-loader.js +12 -0
- package/dist/setup/semantic-assets.js +124 -0
- package/dist/setup/setup.js +52 -1614
- package/dist/setup/steps/connection.js +734 -0
- package/dist/setup/steps/output.js +31 -0
- package/dist/setup/steps/platforms.js +124 -0
- package/dist/setup/steps/semantic.js +27 -0
- package/dist/setup/steps/sources.js +222 -0
- package/dist/setup/steps/stashdir.js +42 -0
- package/dist/setup/steps/tasks.js +152 -0
- package/dist/sources/include.js +6 -2
- package/dist/sources/providers/filesystem.js +0 -1
- package/dist/sources/providers/git-install.js +210 -0
- package/dist/sources/providers/git-provider.js +234 -0
- package/dist/sources/providers/git-stash.js +248 -0
- package/dist/sources/providers/git.js +10 -661
- package/dist/sources/providers/npm.js +2 -6
- package/dist/sources/providers/provider-utils.js +13 -7
- package/dist/sources/providers/sync-from-ref.js +9 -1
- package/dist/sources/providers/website.js +9 -5
- package/dist/sources/website-ingest.js +187 -29
- package/dist/sources/wiki-fetchers/registry.js +53 -0
- package/dist/sources/wiki-fetchers/youtube.js +239 -0
- package/dist/storage/database.js +45 -10
- package/dist/storage/managed-db.js +82 -0
- package/dist/storage/repositories/canaries-repository.js +107 -0
- package/dist/storage/repositories/consolidation-repository.js +38 -0
- package/dist/storage/repositories/embeddings-repository.js +72 -0
- package/dist/storage/repositories/events-repository.js +187 -0
- package/dist/storage/repositories/extract-sessions-repository.js +96 -0
- package/dist/storage/repositories/improve-runs-repository.js +146 -0
- package/dist/storage/repositories/index-db.js +14 -8
- package/dist/storage/repositories/proposals-repository.js +220 -0
- package/dist/storage/repositories/recombine-repository.js +213 -0
- package/dist/storage/repositories/registry-cache.js +93 -0
- package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
- package/dist/storage/repositories/task-history-repository.js +93 -0
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/backends/cron.js +1 -1
- package/dist/tasks/backends/index.js +9 -0
- package/dist/tasks/backends/launchd.js +1 -1
- package/dist/tasks/backends/schtasks.js +1 -1
- package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
- package/dist/tasks/runner.js +15 -13
- package/dist/text-import-hook.mjs +0 -0
- package/dist/wiki/wiki.js +52 -11
- package/dist/workflows/cli.js +1 -0
- package/dist/workflows/db.js +3 -4
- package/dist/workflows/runtime/runs.js +43 -118
- package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/README.md +69 -18
- package/docs/data-and-telemetry.md +5 -4
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.9.0.md +39 -0
- package/package.json +10 -10
- package/dist/assets/tasks/core/update-stashes.yml +0 -4
- package/dist/commands/db-cli.js +0 -23
- package/dist/indexer/db/db-backup.js +0 -376
- package/dist/indexer/passes/staleness-detect.js +0 -488
package/dist/core/state-db.js
CHANGED
|
@@ -4,20 +4,26 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* state.db — Durable SQLite database for non-regenerable akm state.
|
|
6
6
|
*
|
|
7
|
-
* This module
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* This module OWNS the state database's shared infrastructure: path resolution,
|
|
8
|
+
* the managed-db open/loan wrappers, the `BEGIN IMMEDIATE` transaction helper,
|
|
9
|
+
* and schema introspection. The table-specific query helpers live by domain in
|
|
10
|
+
* `src/storage/repositories/*-repository.ts` (events, proposals, task-history,
|
|
11
|
+
* improve-runs, extract-sessions, consolidation, recombine, embeddings,
|
|
12
|
+
* canaries); importers reference those modules directly. The migration engine
|
|
13
|
+
* lives in `./state/migrations`.
|
|
14
|
+
*
|
|
15
|
+
* The state DB replaces flat-file storage for data that is NON-REGENERABLE —
|
|
16
|
+
* events (events.jsonl), proposals (per-uuid JSON directories), task history
|
|
17
|
+
* (per-task JSONL), and the improve-pipeline ledgers.
|
|
12
18
|
*
|
|
13
19
|
* ## Why a separate database from index.db
|
|
14
20
|
*
|
|
15
|
-
* index.db
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* task history are NON-REGENERABLE — losing them is data loss. They
|
|
19
|
-
* a database whose schema evolves via incremental, additive migrations
|
|
20
|
-
* drop rows.
|
|
21
|
+
* index.db is a derived cache built by an idempotent baseline schema; it is fully
|
|
22
|
+
* regenerable from the stash on disk, so a corrupt index is recovered by deleting
|
|
23
|
+
* it and re-running `akm index` (no destructive version-bump rebuild). Events,
|
|
24
|
+
* proposals, and task history are NON-REGENERABLE — losing them is data loss. They
|
|
25
|
+
* must live in a database whose schema evolves via incremental, additive migrations
|
|
26
|
+
* that never drop rows.
|
|
21
27
|
*
|
|
22
28
|
* ## Migration-safety contract
|
|
23
29
|
*
|
|
@@ -49,13 +55,9 @@
|
|
|
49
55
|
*
|
|
50
56
|
* @module state-db
|
|
51
57
|
*/
|
|
52
|
-
import fs from "node:fs";
|
|
53
58
|
import path from "node:path";
|
|
54
|
-
import {
|
|
55
|
-
import { runMigrations as runSqliteMigrations } from "../storage/engines/sqlite-migrations.js";
|
|
56
|
-
import { classifyImproveAction } from "./improve-types.js";
|
|
59
|
+
import { openManagedDatabase, withManagedDb, withManagedDbAsync } from "../storage/managed-db.js";
|
|
57
60
|
import { getDataDir } from "./paths.js";
|
|
58
|
-
import { error } from "./warn.js";
|
|
59
61
|
// ── Path helper ──────────────────────────────────────────────────────────────
|
|
60
62
|
/**
|
|
61
63
|
* Default path: `<dataDir>/state.db`.
|
|
@@ -94,716 +96,33 @@ export function getStateDbPath() {
|
|
|
94
96
|
* narrow when a post-inference reindex overlapped a parallel event write.
|
|
95
97
|
*/
|
|
96
98
|
export function openStateDatabase(dbPath) {
|
|
97
|
-
|
|
98
|
-
const dir = path.dirname(resolvedPath);
|
|
99
|
-
if (!fs.existsSync(dir)) {
|
|
100
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
101
|
-
}
|
|
102
|
-
const db = openDatabase(resolvedPath);
|
|
103
|
-
// PRAGMAs must run before any DDL or DML.
|
|
104
|
-
db.exec("PRAGMA journal_mode = WAL");
|
|
105
|
-
db.exec("PRAGMA foreign_keys = ON");
|
|
106
|
-
db.exec("PRAGMA busy_timeout = 30000");
|
|
107
|
-
runMigrations(db);
|
|
108
|
-
return db;
|
|
109
|
-
}
|
|
110
|
-
// ── Migration engine ─────────────────────────────────────────────────────────
|
|
111
|
-
//
|
|
112
|
-
// The runner itself (ensureMigrationsTable + runMigrations) lives in the shared
|
|
113
|
-
// engine at src/storage/engines/sqlite-migrations.ts. This module owns only its
|
|
114
|
-
// own MIGRATIONS array and delegates application to that shared runner. The
|
|
115
|
-
// {@link Migration} interface is imported from there.
|
|
116
|
-
/**
|
|
117
|
-
* All migrations in application order. New migrations are APPENDED to this
|
|
118
|
-
* array — never inserted in the middle or reordered.
|
|
119
|
-
*
|
|
120
|
-
* @see Migration
|
|
121
|
-
*/
|
|
122
|
-
const MIGRATIONS = [
|
|
123
|
-
// ── Migration 001 — initial schema ──────────────────────────────────────────
|
|
124
|
-
{
|
|
125
|
-
id: "001-initial-schema",
|
|
126
|
-
up: `
|
|
127
|
-
-- ── events ──────────────────────────────────────────────────────────────
|
|
128
|
-
--
|
|
129
|
-
-- Replaces events.jsonl. Indexed (query) columns:
|
|
130
|
-
-- id INTEGER PK — monotonic rowid; replaces byte-offset cursor.
|
|
131
|
-
-- Callers store this as "sinceId" for resume.
|
|
132
|
-
-- event_type TEXT — indexed; replaces the type filter in readEvents().
|
|
133
|
-
-- ts TEXT — ISO-8601 UTC ms; indexed for range queries.
|
|
134
|
-
-- ref TEXT — nullable asset ref; indexed for ref-scoped queries.
|
|
135
|
-
--
|
|
136
|
-
-- Extensible (metadata_json) columns:
|
|
137
|
-
-- metadata_json TEXT — JSON object storing all non-indexed payload
|
|
138
|
-
-- fields (tags, any future structured fields).
|
|
139
|
-
-- Maps directly to EventEnvelope.metadata.
|
|
140
|
-
--
|
|
141
|
-
-- schema_version mirrors EventEnvelope.schemaVersion — always 1 for v1
|
|
142
|
-
-- rows. Stored as a column (not in the JSON blob) so future schema
|
|
143
|
-
-- changes can be detected and migrated row-by-row if ever needed.
|
|
144
|
-
--
|
|
145
|
-
-- TTL: rows where ts < NOW() - 90 days can be deleted by a maintenance job.
|
|
146
|
-
-- No automatic deletion occurs here — callers call purgeOldEvents().
|
|
147
|
-
--
|
|
148
|
-
-- ADD COLUMN extension points (future migrations):
|
|
149
|
-
-- ALTER TABLE events ADD COLUMN stash_dir TEXT DEFAULT NULL;
|
|
150
|
-
-- ALTER TABLE events ADD COLUMN correlation_id TEXT DEFAULT NULL;
|
|
151
|
-
-- ALTER TABLE events ADD COLUMN schema_version INTEGER NOT NULL DEFAULT 1;
|
|
152
|
-
--
|
|
153
|
-
CREATE TABLE IF NOT EXISTS events (
|
|
154
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
155
|
-
event_type TEXT NOT NULL,
|
|
156
|
-
ts TEXT NOT NULL,
|
|
157
|
-
ref TEXT,
|
|
158
|
-
metadata_json TEXT NOT NULL DEFAULT '{}'
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
-- Query patterns supported by these indexes:
|
|
162
|
-
-- SELECT … WHERE event_type = ? → idx_events_type
|
|
163
|
-
-- SELECT … WHERE ref = ? → idx_events_ref
|
|
164
|
-
-- SELECT … WHERE ts >= ? AND ts <= ? → idx_events_ts
|
|
165
|
-
-- SELECT … WHERE event_type = ? AND ref = ? → idx_events_type (prefix scan) + filter
|
|
166
|
-
-- SELECT … WHERE id > ? → PK (rowid) — no extra index needed
|
|
167
|
-
CREATE INDEX IF NOT EXISTS idx_events_type ON events(event_type);
|
|
168
|
-
CREATE INDEX IF NOT EXISTS idx_events_ref ON events(ref);
|
|
169
|
-
CREATE INDEX IF NOT EXISTS idx_events_ts ON events(ts);
|
|
170
|
-
|
|
171
|
-
-- ── proposals ────────────────────────────────────────────────────────────
|
|
172
|
-
--
|
|
173
|
-
-- Replaces per-uuid JSON directories under <stashDir>/.akm/proposals/.
|
|
174
|
-
--
|
|
175
|
-
-- Indexed (query) columns:
|
|
176
|
-
-- id TEXT PK — UUID (crypto.randomUUID()); stable directory name.
|
|
177
|
-
-- stash_dir TEXT — absolute stash root; multi-stash installs need
|
|
178
|
-
-- this to partition proposal lists per stash.
|
|
179
|
-
-- ref TEXT — target asset ref (e.g. "lesson:alpha");
|
|
180
|
-
-- indexed for ref-scoped queue views.
|
|
181
|
-
-- status TEXT — "pending" | "accepted" | "rejected"; indexed
|
|
182
|
-
-- so pending-queue queries are fast.
|
|
183
|
-
-- source TEXT — human-readable origin tag (e.g. "reflect").
|
|
184
|
-
-- created_at TEXT — ISO-8601; used for ORDER BY created_at ASC.
|
|
185
|
-
-- updated_at TEXT — ISO-8601; updated on accept/reject.
|
|
186
|
-
--
|
|
187
|
-
-- Large payload columns (NOT indexed):
|
|
188
|
-
-- content TEXT — full markdown text; the proposal payload body.
|
|
189
|
-
-- frontmatter_json TEXT — JSON of parsed frontmatter (may be NULL when
|
|
190
|
-
-- the content has no frontmatter block).
|
|
191
|
-
--
|
|
192
|
-
-- Extensible (metadata_json) columns:
|
|
193
|
-
-- metadata_json TEXT — JSON object for future proposal fields.
|
|
194
|
-
-- Current fields stored here: sourceRun,
|
|
195
|
-
-- review, confidence, gateDecision (#577),
|
|
196
|
-
-- backupContent, eligibilitySource.
|
|
197
|
-
--
|
|
198
|
-
-- ADD COLUMN extension points (future migrations):
|
|
199
|
-
-- ALTER TABLE proposals ADD COLUMN source_run TEXT DEFAULT NULL;
|
|
200
|
-
-- ALTER TABLE proposals ADD COLUMN review_outcome TEXT DEFAULT NULL;
|
|
201
|
-
-- ALTER TABLE proposals ADD COLUMN review_reason TEXT DEFAULT NULL;
|
|
202
|
-
-- ALTER TABLE proposals ADD COLUMN review_decided_at TEXT DEFAULT NULL;
|
|
203
|
-
-- ALTER TABLE proposals ADD COLUMN archived INTEGER NOT NULL DEFAULT 0;
|
|
204
|
-
--
|
|
205
|
-
CREATE TABLE IF NOT EXISTS proposals (
|
|
206
|
-
id TEXT PRIMARY KEY,
|
|
207
|
-
stash_dir TEXT NOT NULL,
|
|
208
|
-
ref TEXT NOT NULL,
|
|
209
|
-
status TEXT NOT NULL DEFAULT 'pending',
|
|
210
|
-
source TEXT NOT NULL,
|
|
211
|
-
created_at TEXT NOT NULL,
|
|
212
|
-
updated_at TEXT NOT NULL,
|
|
213
|
-
content TEXT NOT NULL DEFAULT '',
|
|
214
|
-
frontmatter_json TEXT,
|
|
215
|
-
metadata_json TEXT NOT NULL DEFAULT '{}'
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
-- Query patterns:
|
|
219
|
-
-- SELECT … WHERE stash_dir = ? AND status = ? → idx_proposals_stash_status
|
|
220
|
-
-- SELECT … WHERE ref = ? AND status = ? → idx_proposals_ref_status
|
|
221
|
-
-- SELECT … WHERE id = ? → PK
|
|
222
|
-
CREATE INDEX IF NOT EXISTS idx_proposals_stash_status
|
|
223
|
-
ON proposals(stash_dir, status);
|
|
224
|
-
CREATE INDEX IF NOT EXISTS idx_proposals_ref_status
|
|
225
|
-
ON proposals(ref, status);
|
|
226
|
-
|
|
227
|
-
-- ── task_history ─────────────────────────────────────────────────────────
|
|
228
|
-
--
|
|
229
|
-
-- Replaces per-task JSONL files under <cacheDir>/tasks/history/.
|
|
230
|
-
--
|
|
231
|
-
-- Indexed (query) columns:
|
|
232
|
-
-- task_id TEXT PK — stable task identifier string.
|
|
233
|
-
-- status TEXT — terminal status (e.g. "completed", "failed",
|
|
234
|
-
-- "cancelled"); indexed for status-scoped queries.
|
|
235
|
-
-- started_at TEXT — ISO-8601; indexed for time-range queries.
|
|
236
|
-
-- target_kind TEXT — kind of the target entity (e.g. "issue",
|
|
237
|
-
-- "workflow", "agent"); indexed for kind-scoped queries.
|
|
238
|
-
-- target_ref TEXT — stable ref of the target entity; indexed for
|
|
239
|
-
-- per-target history lookups.
|
|
240
|
-
--
|
|
241
|
-
-- Non-indexed time columns:
|
|
242
|
-
-- completed_at TEXT — ISO-8601 or NULL if still running.
|
|
243
|
-
-- failed_at TEXT — ISO-8601 or NULL.
|
|
244
|
-
--
|
|
245
|
-
-- Non-indexed diagnostic columns:
|
|
246
|
-
-- log_path TEXT — absolute path to the task log file, if any.
|
|
247
|
-
--
|
|
248
|
-
-- Extensible (metadata_json) columns:
|
|
249
|
-
-- metadata_json TEXT — JSON object for future task fields (exit_code,
|
|
250
|
-
-- runner, priority, parent_task_id, …).
|
|
251
|
-
--
|
|
252
|
-
-- ADD COLUMN extension points (future migrations):
|
|
253
|
-
-- ALTER TABLE task_history ADD COLUMN exit_code INTEGER DEFAULT NULL;
|
|
254
|
-
-- ALTER TABLE task_history ADD COLUMN runner TEXT DEFAULT NULL;
|
|
255
|
-
-- ALTER TABLE task_history ADD COLUMN parent_task_id TEXT DEFAULT NULL;
|
|
256
|
-
-- ALTER TABLE task_history ADD COLUMN priority INTEGER NOT NULL DEFAULT 0;
|
|
257
|
-
--
|
|
258
|
-
CREATE TABLE IF NOT EXISTS task_history (
|
|
259
|
-
task_id TEXT PRIMARY KEY,
|
|
260
|
-
status TEXT NOT NULL,
|
|
261
|
-
started_at TEXT NOT NULL,
|
|
262
|
-
completed_at TEXT,
|
|
263
|
-
failed_at TEXT,
|
|
264
|
-
log_path TEXT,
|
|
265
|
-
target_kind TEXT,
|
|
266
|
-
target_ref TEXT,
|
|
267
|
-
metadata_json TEXT NOT NULL DEFAULT '{}'
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
-- Query patterns:
|
|
271
|
-
-- SELECT … WHERE task_id = ? → PK
|
|
272
|
-
-- SELECT … WHERE started_at >= ? AND started_at <= ? → idx_task_history_started
|
|
273
|
-
-- SELECT … WHERE target_kind = ? AND target_ref = ? → idx_task_history_target
|
|
274
|
-
-- SELECT … WHERE status = ? → idx_task_history_status
|
|
275
|
-
CREATE INDEX IF NOT EXISTS idx_task_history_started
|
|
276
|
-
ON task_history(started_at);
|
|
277
|
-
CREATE INDEX IF NOT EXISTS idx_task_history_target
|
|
278
|
-
ON task_history(target_kind, target_ref);
|
|
279
|
-
CREATE INDEX IF NOT EXISTS idx_task_history_status
|
|
280
|
-
ON task_history(status);
|
|
281
|
-
`,
|
|
282
|
-
},
|
|
283
|
-
// Migration 002 — fix task_history to be a true per-run log.
|
|
284
|
-
//
|
|
285
|
-
// Migration 001 used task_id as PRIMARY KEY, meaning each task had exactly
|
|
286
|
-
// one row and every new run overwrote the previous one. This silently
|
|
287
|
-
// discarded all historical runs — the opposite of a history table.
|
|
288
|
-
//
|
|
289
|
-
// This migration recreates the table with an AUTOINCREMENT id so each run
|
|
290
|
-
// appends a new row. The old single-row table is renamed to _old, the new
|
|
291
|
-
// table is created, data is copied, and the old table is dropped.
|
|
292
|
-
{
|
|
293
|
-
id: "002-task-history-per-run",
|
|
294
|
-
up: `
|
|
295
|
-
ALTER TABLE task_history RENAME TO task_history_v1;
|
|
296
|
-
|
|
297
|
-
CREATE TABLE task_history (
|
|
298
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
299
|
-
task_id TEXT NOT NULL,
|
|
300
|
-
status TEXT NOT NULL,
|
|
301
|
-
started_at TEXT NOT NULL,
|
|
302
|
-
completed_at TEXT,
|
|
303
|
-
failed_at TEXT,
|
|
304
|
-
log_path TEXT,
|
|
305
|
-
target_kind TEXT,
|
|
306
|
-
target_ref TEXT,
|
|
307
|
-
metadata_json TEXT NOT NULL DEFAULT '{}'
|
|
308
|
-
);
|
|
309
|
-
|
|
310
|
-
INSERT INTO task_history
|
|
311
|
-
(task_id, status, started_at, completed_at, failed_at,
|
|
312
|
-
log_path, target_kind, target_ref, metadata_json)
|
|
313
|
-
SELECT task_id, status, started_at, completed_at, failed_at,
|
|
314
|
-
log_path, target_kind, target_ref, metadata_json
|
|
315
|
-
FROM task_history_v1;
|
|
316
|
-
|
|
317
|
-
DROP TABLE task_history_v1;
|
|
318
|
-
|
|
319
|
-
-- Unique constraint: same task cannot have two runs with the same start time.
|
|
320
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_task_history_run
|
|
321
|
-
ON task_history(task_id, started_at);
|
|
322
|
-
CREATE INDEX IF NOT EXISTS idx_task_history_task_id
|
|
323
|
-
ON task_history(task_id);
|
|
324
|
-
CREATE INDEX IF NOT EXISTS idx_task_history_started
|
|
325
|
-
ON task_history(started_at);
|
|
326
|
-
CREATE INDEX IF NOT EXISTS idx_task_history_target
|
|
327
|
-
ON task_history(target_kind, target_ref);
|
|
328
|
-
CREATE INDEX IF NOT EXISTS idx_task_history_status
|
|
329
|
-
ON task_history(status);
|
|
330
|
-
`,
|
|
331
|
-
},
|
|
332
|
-
// ── Migration 003 — improve_runs ────────────────────────────────────────────
|
|
333
|
-
//
|
|
334
|
-
// Records every `akm improve` invocation as a durable row, replacing the
|
|
335
|
-
// legacy `<stash>/.akm/runs/<runId>/improve-result.json` artifact files.
|
|
336
|
-
//
|
|
337
|
-
// The `dry_run` column is FIRST-CLASS and indexed so productivity audits can
|
|
338
|
-
// cleanly filter dry-run probes out of real-run analyses without parsing
|
|
339
|
-
// `result_json`. The dry-run/real-run artifact-trap (recorded in
|
|
340
|
-
// feedback_akm_dryrun_artifact_trap) was the specific motivating bug.
|
|
341
|
-
//
|
|
342
|
-
// Indexed (query) columns:
|
|
343
|
-
// id TEXT PK — runId (`buildImproveRunId()` output).
|
|
344
|
-
// started_at TEXT — ISO-8601; indexed for time-range queries.
|
|
345
|
-
// stash_dir TEXT — absolute stash root; multi-stash scoping.
|
|
346
|
-
// dry_run INTEGER — 0/1; indexed for productivity audits.
|
|
347
|
-
// scope_mode TEXT — "all" | "type" | "ref"; indexed via composite
|
|
348
|
-
// with stash_dir for stash-scoped scope queries.
|
|
349
|
-
//
|
|
350
|
-
// Non-indexed payload:
|
|
351
|
-
// completed_at TEXT — ISO-8601 or NULL if interrupted.
|
|
352
|
-
// profile TEXT — improve profile name (nullable).
|
|
353
|
-
// scope_value TEXT — type name or asset ref (nullable).
|
|
354
|
-
// guidance TEXT — user-provided guidance text, if any.
|
|
355
|
-
// ok INTEGER — 0/1; whether the run produced ok=true.
|
|
356
|
-
// result_json TEXT — full AkmImproveResult JSON.
|
|
357
|
-
// metrics_json TEXT — aggregate counts extracted from result, cheap
|
|
358
|
-
// to query without parsing result_json.
|
|
359
|
-
//
|
|
360
|
-
// Extensible (metadata_json) columns:
|
|
361
|
-
// metadata_json TEXT — JSON object for future improve-run fields.
|
|
362
|
-
//
|
|
363
|
-
// ADD COLUMN extension points (future migrations):
|
|
364
|
-
// ALTER TABLE improve_runs ADD COLUMN duration_ms INTEGER DEFAULT NULL;
|
|
365
|
-
// ALTER TABLE improve_runs ADD COLUMN host TEXT DEFAULT NULL;
|
|
366
|
-
//
|
|
367
|
-
// TTL: rows where started_at < NOW() - 90 days can be deleted by
|
|
368
|
-
// `purgeOldImproveRuns()`. No automatic deletion occurs here.
|
|
369
|
-
{
|
|
370
|
-
id: "003-improve-runs",
|
|
371
|
-
up: `
|
|
372
|
-
CREATE TABLE IF NOT EXISTS improve_runs (
|
|
373
|
-
id TEXT PRIMARY KEY,
|
|
374
|
-
started_at TEXT NOT NULL,
|
|
375
|
-
completed_at TEXT,
|
|
376
|
-
stash_dir TEXT NOT NULL,
|
|
377
|
-
dry_run INTEGER NOT NULL DEFAULT 0,
|
|
378
|
-
profile TEXT,
|
|
379
|
-
scope_mode TEXT NOT NULL,
|
|
380
|
-
scope_value TEXT,
|
|
381
|
-
guidance TEXT,
|
|
382
|
-
ok INTEGER NOT NULL,
|
|
383
|
-
result_json TEXT NOT NULL,
|
|
384
|
-
metrics_json TEXT,
|
|
385
|
-
metadata_json TEXT NOT NULL DEFAULT '{}'
|
|
386
|
-
);
|
|
387
|
-
|
|
388
|
-
-- Query patterns supported:
|
|
389
|
-
-- SELECT … WHERE started_at >= ? AND started_at <= ?
|
|
390
|
-
-- → idx_improve_runs_started
|
|
391
|
-
-- SELECT … WHERE dry_run = 0
|
|
392
|
-
-- → idx_improve_runs_dry_run (productivity audits filter trap)
|
|
393
|
-
-- SELECT … WHERE stash_dir = ? AND scope_mode = ?
|
|
394
|
-
-- → idx_improve_runs_stash_scope
|
|
395
|
-
CREATE INDEX IF NOT EXISTS idx_improve_runs_started
|
|
396
|
-
ON improve_runs(started_at);
|
|
397
|
-
CREATE INDEX IF NOT EXISTS idx_improve_runs_dry_run
|
|
398
|
-
ON improve_runs(dry_run);
|
|
399
|
-
CREATE INDEX IF NOT EXISTS idx_improve_runs_stash_scope
|
|
400
|
-
ON improve_runs(stash_dir, scope_mode);
|
|
401
|
-
`,
|
|
402
|
-
},
|
|
403
|
-
// ── Migration 004 — extract_sessions_seen ───────────────────────────────────
|
|
404
|
-
//
|
|
405
|
-
// Tracks which platform sessions the extractor has processed, so the discovery
|
|
406
|
-
// pass in `akm extract --since <window>` skips sessions whose content hasn't
|
|
407
|
-
// changed since the last successful run. Replaces the akm-plugin
|
|
408
|
-
// session-checkpoint hook's implicit "write-once" memory of what's been
|
|
409
|
-
// captured — but persistent and queryable.
|
|
410
|
-
//
|
|
411
|
-
// Indexed (query) columns:
|
|
412
|
-
// harness TEXT — harness name (claude-code, opencode, ...).
|
|
413
|
-
// session_id TEXT — platform-native session identifier.
|
|
414
|
-
// processed_at TEXT — ISO-8601 UTC; when extract last ran on this session.
|
|
415
|
-
// session_ended_at TEXT — session.endedAt at processing time. When a
|
|
416
|
-
// later listSessions reports a *newer* endedAt
|
|
417
|
-
// for the same session_id, the extractor
|
|
418
|
-
// re-processes the appended events.
|
|
419
|
-
// outcome TEXT — "candidates_queued" | "no_candidates" |
|
|
420
|
-
// "skipped" | "failed".
|
|
421
|
-
//
|
|
422
|
-
// Non-indexed columns:
|
|
423
|
-
// candidate_count INTEGER — number of candidates the LLM produced.
|
|
424
|
-
// proposal_count INTEGER — number of proposals actually queued
|
|
425
|
-
// (candidates may fail downstream validation).
|
|
426
|
-
// rationale TEXT — for "no_candidates", the LLM's explanation.
|
|
427
|
-
// source_run TEXT — sourceRun id for PROV-DM traceability.
|
|
428
|
-
// metadata_json TEXT — future-proofing (pre-filter stats, LLM
|
|
429
|
-
// model+version, prompt token count, etc.).
|
|
430
|
-
//
|
|
431
|
-
// PK: (harness, session_id) — one row per session per harness. A re-extract
|
|
432
|
-
// updates the row in place via INSERT OR REPLACE.
|
|
433
|
-
//
|
|
434
|
-
// TTL: no automatic deletion. Sessions stay tracked as long as the source
|
|
435
|
-
// session files exist on disk. Operator can `DELETE FROM extract_sessions_seen
|
|
436
|
-
// WHERE processed_at < ?` for cleanup if desired.
|
|
437
|
-
{
|
|
438
|
-
id: "004-extract-sessions-seen",
|
|
439
|
-
up: `
|
|
440
|
-
CREATE TABLE IF NOT EXISTS extract_sessions_seen (
|
|
441
|
-
harness TEXT NOT NULL,
|
|
442
|
-
session_id TEXT NOT NULL,
|
|
443
|
-
processed_at TEXT NOT NULL,
|
|
444
|
-
session_ended_at TEXT,
|
|
445
|
-
outcome TEXT NOT NULL,
|
|
446
|
-
candidate_count INTEGER NOT NULL DEFAULT 0,
|
|
447
|
-
proposal_count INTEGER NOT NULL DEFAULT 0,
|
|
448
|
-
rationale TEXT,
|
|
449
|
-
source_run TEXT,
|
|
450
|
-
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
451
|
-
PRIMARY KEY (harness, session_id)
|
|
452
|
-
);
|
|
453
|
-
|
|
454
|
-
-- Query patterns:
|
|
455
|
-
-- SELECT … WHERE harness = ? → idx_extract_sessions_harness
|
|
456
|
-
-- SELECT … WHERE processed_at >= ? → idx_extract_sessions_processed
|
|
457
|
-
-- SELECT … WHERE harness = ? AND session_id = ? → PK
|
|
458
|
-
CREATE INDEX IF NOT EXISTS idx_extract_sessions_harness
|
|
459
|
-
ON extract_sessions_seen(harness);
|
|
460
|
-
CREATE INDEX IF NOT EXISTS idx_extract_sessions_processed
|
|
461
|
-
ON extract_sessions_seen(processed_at);
|
|
462
|
-
`,
|
|
463
|
-
},
|
|
464
|
-
// ── Migration 005 — proposal_fs_imports ─────────────────────────────────────
|
|
465
|
-
//
|
|
466
|
-
// One-shot ledger for the legacy filesystem→SQLite proposal import (#578).
|
|
467
|
-
//
|
|
468
|
-
// Before 0.9.0 the proposal queue lived as per-uuid JSON directories under
|
|
469
|
-
// `<stashDir>/.akm/proposals/` and the `proposals` table (created in 001) was
|
|
470
|
-
// dead weight. 0.9.0 makes the table canonical; the first proposal operation
|
|
471
|
-
// against a stash imports any legacy `proposal.json` files it finds (INSERT
|
|
472
|
-
// OR IGNORE, so re-runs never duplicate) and records the stash here so later
|
|
473
|
-
// invocations skip the directory walk entirely.
|
|
474
|
-
//
|
|
475
|
-
// Indexed (query) columns:
|
|
476
|
-
// stash_dir TEXT PK — absolute stash root the import ran against.
|
|
477
|
-
//
|
|
478
|
-
// Non-indexed columns:
|
|
479
|
-
// imported_at TEXT — ISO-8601 UTC; when the import completed.
|
|
480
|
-
// imported_count INTEGER — rows actually inserted by the import.
|
|
481
|
-
{
|
|
482
|
-
id: "005-proposal-fs-imports",
|
|
483
|
-
up: `
|
|
484
|
-
CREATE TABLE IF NOT EXISTS proposal_fs_imports (
|
|
485
|
-
stash_dir TEXT PRIMARY KEY,
|
|
486
|
-
imported_at TEXT NOT NULL,
|
|
487
|
-
imported_count INTEGER NOT NULL DEFAULT 0
|
|
488
|
-
);
|
|
489
|
-
`,
|
|
490
|
-
},
|
|
491
|
-
// ── Migration 006 — pending proposal lookup index ──────────────────────────
|
|
492
|
-
//
|
|
493
|
-
// Supports the transaction-scoped dedup / queue-mutation hardening added in
|
|
494
|
-
// 0.9.x. The queue now acquires an IMMEDIATE write transaction before it
|
|
495
|
-
// reads pending proposals, so the hot path is a stash-scoped `status='pending'
|
|
496
|
-
// AND ref=?` probe followed by an update/insert. This composite index keeps
|
|
497
|
-
// that lookup index-covered under contention.
|
|
498
|
-
{
|
|
499
|
-
id: "006-proposals-pending-ref-source",
|
|
500
|
-
up: `
|
|
501
|
-
CREATE INDEX IF NOT EXISTS idx_proposals_stash_status_ref_source
|
|
502
|
-
ON proposals(stash_dir, status, ref, source);
|
|
503
|
-
`,
|
|
504
|
-
},
|
|
505
|
-
];
|
|
506
|
-
/**
|
|
507
|
-
* Apply every pending migration in a single transaction per migration.
|
|
508
|
-
*
|
|
509
|
-
* Delegates to the shared SQLite migration engine; state.db has no
|
|
510
|
-
* pre-versioning bootstrap step, so no `bootstrap` hook is passed.
|
|
511
|
-
*
|
|
512
|
-
* Called automatically by `openStateDatabase()`.
|
|
513
|
-
*/
|
|
514
|
-
export function runMigrations(db) {
|
|
515
|
-
runSqliteMigrations(db, MIGRATIONS);
|
|
516
|
-
}
|
|
517
|
-
/**
|
|
518
|
-
* Convert a raw `EventRow` from the database to the public `EventEnvelope`
|
|
519
|
-
* interface used throughout the events module.
|
|
520
|
-
*/
|
|
521
|
-
export function eventRowToEnvelope(row) {
|
|
522
|
-
let metadata;
|
|
523
|
-
try {
|
|
524
|
-
const parsed = JSON.parse(row.metadata_json);
|
|
525
|
-
// Only attach metadata when the JSON blob is non-empty so downstream
|
|
526
|
-
// consumers that check `envelope.metadata !== undefined` keep working.
|
|
527
|
-
if (Object.keys(parsed).length > 0) {
|
|
528
|
-
metadata = parsed;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
catch {
|
|
532
|
-
// Corrupt JSON in the DB — treat as no metadata.
|
|
533
|
-
}
|
|
534
|
-
return {
|
|
535
|
-
schemaVersion: 1,
|
|
536
|
-
id: row.id,
|
|
537
|
-
ts: row.ts,
|
|
538
|
-
eventType: row.event_type,
|
|
539
|
-
...(row.ref !== null ? { ref: row.ref } : {}),
|
|
540
|
-
...(metadata !== undefined ? { metadata } : {}),
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
/**
|
|
544
|
-
* Convert a raw `ProposalRow` to the public `Proposal` shape.
|
|
545
|
-
*/
|
|
546
|
-
export function proposalRowToProposal(row) {
|
|
547
|
-
let frontmatter;
|
|
548
|
-
if (row.frontmatter_json) {
|
|
549
|
-
try {
|
|
550
|
-
frontmatter = JSON.parse(row.frontmatter_json);
|
|
551
|
-
}
|
|
552
|
-
catch {
|
|
553
|
-
/* ignore corrupt frontmatter JSON */
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
let meta = {};
|
|
557
|
-
try {
|
|
558
|
-
meta = JSON.parse(row.metadata_json);
|
|
559
|
-
}
|
|
560
|
-
catch {
|
|
561
|
-
/* ignore */
|
|
562
|
-
}
|
|
563
|
-
return {
|
|
564
|
-
id: row.id,
|
|
565
|
-
ref: row.ref,
|
|
566
|
-
status: row.status,
|
|
567
|
-
source: row.source,
|
|
568
|
-
...(typeof meta.sourceRun === "string" ? { sourceRun: meta.sourceRun } : {}),
|
|
569
|
-
createdAt: row.created_at,
|
|
570
|
-
updatedAt: row.updated_at,
|
|
571
|
-
payload: {
|
|
572
|
-
content: row.content,
|
|
573
|
-
...(frontmatter !== undefined ? { frontmatter } : {}),
|
|
574
|
-
},
|
|
575
|
-
...(meta.review !== undefined ? { review: meta.review } : {}),
|
|
576
|
-
...(typeof meta.confidence === "number" ? { confidence: meta.confidence } : {}),
|
|
577
|
-
...(meta.gateDecision !== undefined ? { gateDecision: meta.gateDecision } : {}),
|
|
578
|
-
...(typeof meta.backupContent === "string" ? { backupContent: meta.backupContent } : {}),
|
|
579
|
-
...(typeof meta.eligibilitySource === "string"
|
|
580
|
-
? { eligibilitySource: meta.eligibilitySource }
|
|
581
|
-
: {}),
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
|
-
/**
|
|
585
|
-
* Convert a public `Proposal` to column values ready for an INSERT/UPDATE.
|
|
586
|
-
* The `stash_dir` comes from the call site (proposals.ts has it in scope).
|
|
587
|
-
*/
|
|
588
|
-
export function proposalToRowValues(proposal, stashDir) {
|
|
589
|
-
// Fields that have no dedicated column live in metadata_json.
|
|
590
|
-
const metaObj = {};
|
|
591
|
-
if (proposal.sourceRun !== undefined)
|
|
592
|
-
metaObj.sourceRun = proposal.sourceRun;
|
|
593
|
-
if (proposal.review !== undefined)
|
|
594
|
-
metaObj.review = proposal.review;
|
|
595
|
-
if (proposal.confidence !== undefined)
|
|
596
|
-
metaObj.confidence = proposal.confidence;
|
|
597
|
-
if (proposal.gateDecision !== undefined)
|
|
598
|
-
metaObj.gateDecision = proposal.gateDecision;
|
|
599
|
-
if (proposal.backupContent !== undefined)
|
|
600
|
-
metaObj.backupContent = proposal.backupContent;
|
|
601
|
-
if (proposal.eligibilitySource !== undefined)
|
|
602
|
-
metaObj.eligibilitySource = proposal.eligibilitySource;
|
|
603
|
-
return {
|
|
604
|
-
id: proposal.id,
|
|
605
|
-
stash_dir: stashDir,
|
|
606
|
-
ref: proposal.ref,
|
|
607
|
-
status: proposal.status,
|
|
608
|
-
source: proposal.source,
|
|
609
|
-
created_at: proposal.createdAt,
|
|
610
|
-
updated_at: proposal.updatedAt,
|
|
611
|
-
content: proposal.payload.content,
|
|
612
|
-
frontmatter_json: proposal.payload.frontmatter ? JSON.stringify(proposal.payload.frontmatter) : null,
|
|
613
|
-
metadata_json: JSON.stringify(metaObj),
|
|
614
|
-
};
|
|
615
|
-
}
|
|
616
|
-
// ── events table helpers ─────────────────────────────────────────────────────
|
|
617
|
-
/**
|
|
618
|
-
* Insert a single event. Returns the auto-assigned monotonic rowid, which
|
|
619
|
-
* callers can store as a "sinceId" cursor for future `readEventsSince` calls.
|
|
620
|
-
*
|
|
621
|
-
* Best-effort: mirrors the behaviour of the old `appendEvent` — errors are
|
|
622
|
-
* caught and logged to stderr rather than propagated so observability never
|
|
623
|
-
* breaks mutation.
|
|
624
|
-
*/
|
|
625
|
-
export function insertEvent(db, input) {
|
|
626
|
-
try {
|
|
627
|
-
const result = db
|
|
628
|
-
.prepare(`INSERT INTO events (event_type, ts, ref, metadata_json)
|
|
629
|
-
VALUES (?, ?, ?, ?)
|
|
630
|
-
RETURNING id`)
|
|
631
|
-
.get(input.eventType, input.ts, input.ref ?? null, JSON.stringify(input.metadata ?? {}));
|
|
632
|
-
return result?.id;
|
|
633
|
-
}
|
|
634
|
-
catch (err) {
|
|
635
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
636
|
-
error(`akm: state.db event insert failed (${message})`);
|
|
637
|
-
return undefined;
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
/**
|
|
641
|
-
* Read events from the database matching the filter. Returns events in
|
|
642
|
-
* ascending id order so consumers can process them in emission order.
|
|
643
|
-
*
|
|
644
|
-
* The returned `nextId` is the maximum id seen (or `sinceId` when no rows
|
|
645
|
-
* match), suitable as the next `sinceId` cursor value.
|
|
646
|
-
*/
|
|
647
|
-
export function readStateEvents(db, options = {}) {
|
|
648
|
-
const conditions = [];
|
|
649
|
-
const params = [];
|
|
650
|
-
if (options.sinceId !== undefined && options.sinceId > 0) {
|
|
651
|
-
conditions.push("id > ?");
|
|
652
|
-
params.push(options.sinceId);
|
|
653
|
-
}
|
|
654
|
-
if (options.since) {
|
|
655
|
-
conditions.push("ts >= ?");
|
|
656
|
-
params.push(options.since);
|
|
657
|
-
}
|
|
658
|
-
if (options.type) {
|
|
659
|
-
conditions.push("event_type = ?");
|
|
660
|
-
params.push(options.type);
|
|
661
|
-
}
|
|
662
|
-
if (options.ref) {
|
|
663
|
-
conditions.push("ref = ?");
|
|
664
|
-
params.push(options.ref);
|
|
665
|
-
}
|
|
666
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
667
|
-
const rows = db
|
|
668
|
-
.prepare(`SELECT id, event_type, ts, ref, metadata_json FROM events ${where} ORDER BY id ASC`)
|
|
669
|
-
.all(...params);
|
|
670
|
-
const events = rows.map(eventRowToEnvelope);
|
|
671
|
-
const nextId = events.length > 0 ? events[events.length - 1].id : (options.sinceId ?? 0);
|
|
672
|
-
return { events, nextId };
|
|
673
|
-
}
|
|
674
|
-
/**
|
|
675
|
-
* Delete events older than `retentionDays` (default: 90). Safe to call from
|
|
676
|
-
* a maintenance cron; uses a single DELETE with an index-covered ts predicate.
|
|
677
|
-
*
|
|
678
|
-
* Returns the number of rows actually deleted so callers can emit an
|
|
679
|
-
* `events_purged` observability event. A non-positive or non-finite
|
|
680
|
-
* `retentionDays` is treated as "disabled" and returns 0 without scanning.
|
|
681
|
-
*/
|
|
682
|
-
export function purgeOldEvents(db, retentionDays = 90) {
|
|
683
|
-
if (!Number.isFinite(retentionDays) || retentionDays <= 0)
|
|
684
|
-
return 0;
|
|
685
|
-
const cutoff = new Date(Date.now() - retentionDays * 86_400_000).toISOString();
|
|
686
|
-
const result = db.prepare("DELETE FROM events WHERE ts < ?").run(cutoff);
|
|
687
|
-
// bun:sqlite's run() returns { changes, lastInsertRowid }. `changes` may be
|
|
688
|
-
// a number or bigint depending on the underlying lib; coerce to number for
|
|
689
|
-
// the metadata payload.
|
|
690
|
-
const changes = result.changes ?? 0;
|
|
691
|
-
return typeof changes === "bigint" ? Number(changes) : changes;
|
|
692
|
-
}
|
|
693
|
-
// ── proposals table helpers ──────────────────────────────────────────────────
|
|
694
|
-
/**
|
|
695
|
-
* Upsert a proposal row. Called by the proposal write path when state.db is
|
|
696
|
-
* the active backend.
|
|
697
|
-
*/
|
|
698
|
-
export function upsertProposal(db, proposal, stashDir) {
|
|
699
|
-
const v = proposalToRowValues(proposal, stashDir);
|
|
700
|
-
db.prepare(`
|
|
701
|
-
INSERT INTO proposals
|
|
702
|
-
(id, stash_dir, ref, status, source, created_at, updated_at, content, frontmatter_json, metadata_json)
|
|
703
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
704
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
705
|
-
stash_dir = excluded.stash_dir,
|
|
706
|
-
ref = excluded.ref,
|
|
707
|
-
status = excluded.status,
|
|
708
|
-
source = excluded.source,
|
|
709
|
-
updated_at = excluded.updated_at,
|
|
710
|
-
content = excluded.content,
|
|
711
|
-
frontmatter_json = excluded.frontmatter_json,
|
|
712
|
-
metadata_json = excluded.metadata_json
|
|
713
|
-
`).run(v.id, v.stash_dir, v.ref, v.status, v.source, v.created_at, v.updated_at, v.content, v.frontmatter_json, v.metadata_json);
|
|
714
|
-
}
|
|
715
|
-
/**
|
|
716
|
-
* List proposals, optionally filtered by stashDir, status, and/or ref.
|
|
717
|
-
*
|
|
718
|
-
* Results are ordered by `created_at ASC` (matching the historical
|
|
719
|
-
* `listProposals()` sort), with `rowid` as a deterministic tiebreak so two
|
|
720
|
-
* proposals created in the same millisecond list in insertion order.
|
|
721
|
-
*/
|
|
722
|
-
export function listStateProposals(db, options = {}) {
|
|
723
|
-
const conditions = [];
|
|
724
|
-
const params = [];
|
|
725
|
-
if (options.stashDir) {
|
|
726
|
-
conditions.push("stash_dir = ?");
|
|
727
|
-
params.push(options.stashDir);
|
|
728
|
-
}
|
|
729
|
-
if (options.status) {
|
|
730
|
-
conditions.push("status = ?");
|
|
731
|
-
params.push(options.status);
|
|
732
|
-
}
|
|
733
|
-
if (options.ref) {
|
|
734
|
-
conditions.push("ref = ?");
|
|
735
|
-
params.push(options.ref);
|
|
736
|
-
}
|
|
737
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
738
|
-
const rows = db
|
|
739
|
-
.prepare(`SELECT id, stash_dir, ref, status, source, created_at, updated_at,
|
|
740
|
-
content, frontmatter_json, metadata_json
|
|
741
|
-
FROM proposals ${where} ORDER BY created_at ASC, rowid ASC`)
|
|
742
|
-
.all(...params);
|
|
743
|
-
return rows.map(proposalRowToProposal);
|
|
744
|
-
}
|
|
745
|
-
/**
|
|
746
|
-
* Look up a single proposal by id, optionally scoped to one stash root.
|
|
747
|
-
* Returns undefined when not found.
|
|
748
|
-
*/
|
|
749
|
-
export function getStateProposal(db, id, stashDir) {
|
|
750
|
-
const sql = `SELECT id, stash_dir, ref, status, source, created_at, updated_at,
|
|
751
|
-
content, frontmatter_json, metadata_json
|
|
752
|
-
FROM proposals WHERE id = ?${stashDir ? " AND stash_dir = ?" : ""}`;
|
|
753
|
-
const row = (stashDir ? db.prepare(sql).get(id, stashDir) : db.prepare(sql).get(id));
|
|
754
|
-
return row ? proposalRowToProposal(row) : undefined;
|
|
755
|
-
}
|
|
756
|
-
/**
|
|
757
|
-
* Find PENDING proposal ids in one stash whose id starts with `idPrefix`.
|
|
758
|
-
* Backs the UUID-prefix form of `akm proposal show/accept/... <prefix>` —
|
|
759
|
-
* prefix resolution is deliberately scoped to the live (pending) queue,
|
|
760
|
-
* mirroring the historical behaviour of scanning only the live directory.
|
|
761
|
-
*
|
|
762
|
-
* `%` / `_` / `\` in the prefix are escaped so the LIKE pattern is literal.
|
|
763
|
-
*/
|
|
764
|
-
export function listStateProposalIdsByPrefix(db, stashDir, idPrefix) {
|
|
765
|
-
const escaped = idPrefix.replace(/[\\%_]/g, (ch) => `\\${ch}`);
|
|
766
|
-
const rows = db
|
|
767
|
-
.prepare(`SELECT id FROM proposals
|
|
768
|
-
WHERE stash_dir = ? AND status = 'pending' AND id LIKE ? ESCAPE '\\'
|
|
769
|
-
ORDER BY id ASC`)
|
|
770
|
-
.all(stashDir, `${escaped}%`);
|
|
771
|
-
return rows.map((r) => r.id);
|
|
772
|
-
}
|
|
773
|
-
/**
|
|
774
|
-
* Whether the legacy filesystem proposal import has already run for `stashDir`.
|
|
775
|
-
* See migration 005 (`proposal_fs_imports`).
|
|
776
|
-
*/
|
|
777
|
-
export function hasImportedFsProposals(db, stashDir) {
|
|
778
|
-
// Drivers disagree on the no-row sentinel (bun:sqlite → null,
|
|
779
|
-
// better-sqlite3 → undefined) — Boolean() covers both.
|
|
780
|
-
return Boolean(db.prepare("SELECT 1 FROM proposal_fs_imports WHERE stash_dir = ?").get(stashDir));
|
|
99
|
+
return openManagedDatabase({ path: dbPath ?? getStateDbPath(), init: runMigrations });
|
|
781
100
|
}
|
|
782
101
|
/**
|
|
783
|
-
*
|
|
784
|
-
*
|
|
785
|
-
*
|
|
102
|
+
* Run `fn` against state.db, owning the handle unless one is borrowed. The loan
|
|
103
|
+
* helper for state.db, mirroring `withIndexDb` / `withWorkflowRunsRepo`. Pass
|
|
104
|
+
* `{ borrowed: ctx?.db }` to reuse an already-open run-scoped handle rather than
|
|
105
|
+
* opening + closing a fresh one — this replaces the hand-rolled
|
|
106
|
+
* `ctx?.db ?? open()` + `ownsDb` flag + `finally`/close idiom at call sites.
|
|
786
107
|
*/
|
|
787
|
-
export function
|
|
788
|
-
|
|
108
|
+
export function withStateDb(fn, opts) {
|
|
109
|
+
return withManagedDb(() => openStateDatabase(opts?.path), fn, opts);
|
|
789
110
|
}
|
|
790
111
|
/**
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
*
|
|
112
|
+
* Async sibling of {@link withStateDb} — for `fn`s that hold the handle across
|
|
113
|
+
* an `await` (the sync version would close it too early). Same borrow/own +
|
|
114
|
+
* optional `path` override semantics.
|
|
794
115
|
*/
|
|
795
|
-
export function
|
|
796
|
-
|
|
797
|
-
const result = db
|
|
798
|
-
.prepare(`
|
|
799
|
-
INSERT OR IGNORE INTO proposals
|
|
800
|
-
(id, stash_dir, ref, status, source, created_at, updated_at, content, frontmatter_json, metadata_json)
|
|
801
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
802
|
-
`)
|
|
803
|
-
.run(v.id, v.stash_dir, v.ref, v.status, v.source, v.created_at, v.updated_at, v.content, v.frontmatter_json, v.metadata_json);
|
|
804
|
-
const changes = result.changes ?? 0;
|
|
805
|
-
return Number(changes) > 0;
|
|
116
|
+
export function withStateDbAsync(fn, opts) {
|
|
117
|
+
return withManagedDbAsync(() => openStateDatabase(opts?.path), fn, opts);
|
|
806
118
|
}
|
|
119
|
+
// ── Migration engine ─────────────────────────────────────────────────────────
|
|
120
|
+
//
|
|
121
|
+
// The MIGRATIONS registry + runMigrations live in ./state/migrations (the single
|
|
122
|
+
// append-only ordered source of truth). Imported for internal use by
|
|
123
|
+
// openStateDatabase.
|
|
124
|
+
import { runMigrations } from "./state/migrations.js";
|
|
125
|
+
// ── BEGIN IMMEDIATE transaction helper ───────────────────────────────────────
|
|
807
126
|
/**
|
|
808
127
|
* Run `fn` inside a `BEGIN IMMEDIATE` transaction.
|
|
809
128
|
*
|
|
@@ -813,113 +132,105 @@ export function insertProposalIfAbsent(db, proposal, stashDir) {
|
|
|
813
132
|
* need the write lock BEFORE those reads so concurrent processes serialize on
|
|
814
133
|
* the live queue state rather than clobbering each other.
|
|
815
134
|
*/
|
|
135
|
+
/**
|
|
136
|
+
* Errors `BEGIN IMMEDIATE` can throw under concurrent-writer contention that are
|
|
137
|
+
* transient (the statement did NOT start a usable transaction) and safe to
|
|
138
|
+
* retry:
|
|
139
|
+
* - "database is locked" / SQLITE_BUSY — another writer holds the lock.
|
|
140
|
+
* These are start-of-transaction failures only; an error thrown by `fn` is a
|
|
141
|
+
* real failure and is NEVER retried.
|
|
142
|
+
*
|
|
143
|
+
* "cannot start a transaction within a transaction" is deliberately NOT
|
|
144
|
+
* retryable: it means a transaction is already open on this connection (a
|
|
145
|
+
* re-entrant call — handled by the entry guard in withImmediateTransaction),
|
|
146
|
+
* and "retrying" it with a ROLLBACK would destroy the caller's transaction
|
|
147
|
+
* (issue #686).
|
|
148
|
+
*/
|
|
149
|
+
function isRetryableBeginError(err) {
|
|
150
|
+
const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
|
|
151
|
+
return (msg.includes("database is locked") ||
|
|
152
|
+
msg.includes("database table is locked") ||
|
|
153
|
+
// Phantom BEGIN (see below) — synthesized when BEGIN IMMEDIATE returns
|
|
154
|
+
// without opening a transaction. Safe to retry: fn() has not run.
|
|
155
|
+
msg.includes("did not open a transaction"));
|
|
156
|
+
}
|
|
157
|
+
const WITH_IMMEDIATE_TX_MAX_ATTEMPTS = 5;
|
|
158
|
+
/** Portable synchronous sleep (works under both Bun and Node). */
|
|
159
|
+
function sleepSyncMs(ms) {
|
|
160
|
+
if (ms <= 0)
|
|
161
|
+
return;
|
|
162
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
163
|
+
}
|
|
816
164
|
export function withImmediateTransaction(db, fn) {
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
165
|
+
// Re-entrancy guard (issue #686): if a transaction is already open on this
|
|
166
|
+
// connection (e.g. a nested withImmediateTransaction call inside an outer
|
|
167
|
+
// frame's fn), join it — run fn directly with no BEGIN/COMMIT/ROLLBACK of
|
|
168
|
+
// our own. Without this, the nested BEGIN throws "cannot start a transaction
|
|
169
|
+
// within a transaction", which the old retry path answered with an
|
|
170
|
+
// unconditional ROLLBACK — destroying the OUTER transaction and leaving its
|
|
171
|
+
// COMMIT to fail with "cannot commit - no transaction is active".
|
|
172
|
+
if (db.inTransaction) {
|
|
173
|
+
return fn();
|
|
174
|
+
}
|
|
175
|
+
let lastBeginErr;
|
|
176
|
+
for (let attempt = 1; attempt <= WITH_IMMEDIATE_TX_MAX_ATTEMPTS; attempt++) {
|
|
824
177
|
try {
|
|
825
|
-
db.exec("
|
|
178
|
+
db.exec("BEGIN IMMEDIATE");
|
|
179
|
+
// bun:sqlite can return from BEGIN IMMEDIATE under writer contention WITHOUT
|
|
180
|
+
// actually opening a transaction (no throw). That phantom state otherwise
|
|
181
|
+
// surfaces as "cannot commit - no transaction is active" at COMMIT — AFTER
|
|
182
|
+
// fn() has already run in autocommit, so its writes escaped the intended
|
|
183
|
+
// serialization (the concurrent proposal-queue race). Detect it here, before
|
|
184
|
+
// fn(), and route it through the same retry path as a contended BEGIN.
|
|
185
|
+
if (!db.inTransaction) {
|
|
186
|
+
throw new Error("BEGIN IMMEDIATE did not open a transaction (phantom contention state)");
|
|
187
|
+
}
|
|
826
188
|
}
|
|
827
|
-
catch {
|
|
828
|
-
|
|
189
|
+
catch (err) {
|
|
190
|
+
lastBeginErr = err;
|
|
191
|
+
if (isRetryableBeginError(err) && attempt < WITH_IMMEDIATE_TX_MAX_ATTEMPTS) {
|
|
192
|
+
// Only roll back a transaction we can see — never blind-ROLLBACK, since
|
|
193
|
+
// that could destroy a transaction this frame does not own.
|
|
194
|
+
if (db.inTransaction) {
|
|
195
|
+
try {
|
|
196
|
+
db.exec("ROLLBACK");
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// Transaction already gone — fine.
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
sleepSyncMs(2 ** (attempt - 1));
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
throw err;
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
const result = fn();
|
|
209
|
+
if (!db.inTransaction) {
|
|
210
|
+
// The transaction we opened vanished while fn() ran (e.g. an
|
|
211
|
+
// auto-rollback or a stray ROLLBACK inside fn). fn's writes may have
|
|
212
|
+
// escaped serialization, so retrying is unsafe — fail loudly instead of
|
|
213
|
+
// letting COMMIT throw the opaque "cannot commit - no transaction is
|
|
214
|
+
// active" SQLiteError.
|
|
215
|
+
throw new Error("withImmediateTransaction invariant violated: transaction opened by BEGIN IMMEDIATE was no longer active after the transaction body ran; refusing to COMMIT (writes may have escaped serialization)");
|
|
216
|
+
}
|
|
217
|
+
db.exec("COMMIT");
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
if (db.inTransaction) {
|
|
222
|
+
try {
|
|
223
|
+
db.exec("ROLLBACK");
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// Ignore rollback failures so the original error is preserved.
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
throw err; // a real error inside the transaction body — never retried.
|
|
829
230
|
}
|
|
830
|
-
throw err;
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
// ── task_history table helpers ───────────────────────────────────────────────
|
|
834
|
-
/**
|
|
835
|
-
* Upsert a task history row.
|
|
836
|
-
*/
|
|
837
|
-
export function upsertTaskHistory(db, row) {
|
|
838
|
-
// INSERT OR IGNORE: if a run with the same (task_id, started_at) was already
|
|
839
|
-
// imported (e.g. by the migration script), skip it silently.
|
|
840
|
-
db.prepare(`
|
|
841
|
-
INSERT OR IGNORE INTO task_history
|
|
842
|
-
(task_id, status, started_at, completed_at, failed_at, log_path,
|
|
843
|
-
target_kind, target_ref, metadata_json)
|
|
844
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
845
|
-
`).run(row.task_id, row.status, row.started_at, row.completed_at ?? null, row.failed_at ?? null, row.log_path ?? null, row.target_kind ?? null, row.target_ref ?? null, row.metadata_json);
|
|
846
|
-
}
|
|
847
|
-
/**
|
|
848
|
-
* Look up a task history row by task_id. Returns undefined when not found.
|
|
849
|
-
*/
|
|
850
|
-
/**
|
|
851
|
-
* Return the most recent run for a given task_id, or undefined if no runs exist.
|
|
852
|
-
*/
|
|
853
|
-
export function getTaskHistory(db, taskId) {
|
|
854
|
-
return db
|
|
855
|
-
.prepare(`SELECT id, task_id, status, started_at, completed_at, failed_at, log_path,
|
|
856
|
-
target_kind, target_ref, metadata_json
|
|
857
|
-
FROM task_history WHERE task_id = ? ORDER BY started_at DESC LIMIT 1`)
|
|
858
|
-
.get(taskId);
|
|
859
|
-
}
|
|
860
|
-
/**
|
|
861
|
-
* Return all runs for a given task_id, newest first.
|
|
862
|
-
*/
|
|
863
|
-
export function getTaskHistoryRuns(db, taskId, limit = 50) {
|
|
864
|
-
return db
|
|
865
|
-
.prepare(`SELECT id, task_id, status, started_at, completed_at, failed_at, log_path,
|
|
866
|
-
target_kind, target_ref, metadata_json
|
|
867
|
-
FROM task_history WHERE task_id = ? ORDER BY started_at DESC LIMIT ?`)
|
|
868
|
-
.all(taskId, limit);
|
|
869
|
-
}
|
|
870
|
-
/**
|
|
871
|
-
* Query task history rows by started_at range and/or status.
|
|
872
|
-
*/
|
|
873
|
-
export function queryTaskHistory(db, options = {}) {
|
|
874
|
-
const conditions = [];
|
|
875
|
-
const params = [];
|
|
876
|
-
if (options.since) {
|
|
877
|
-
conditions.push("started_at >= ?");
|
|
878
|
-
params.push(options.since);
|
|
879
|
-
}
|
|
880
|
-
if (options.until) {
|
|
881
|
-
conditions.push("started_at <= ?");
|
|
882
|
-
params.push(options.until);
|
|
883
|
-
}
|
|
884
|
-
if (options.status) {
|
|
885
|
-
conditions.push("status = ?");
|
|
886
|
-
params.push(options.status);
|
|
887
|
-
}
|
|
888
|
-
if (options.targetKind) {
|
|
889
|
-
conditions.push("target_kind = ?");
|
|
890
|
-
params.push(options.targetKind);
|
|
891
|
-
}
|
|
892
|
-
if (options.targetRef) {
|
|
893
|
-
conditions.push("target_ref = ?");
|
|
894
|
-
params.push(options.targetRef);
|
|
895
231
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
.prepare(`SELECT task_id, status, started_at, completed_at, failed_at, log_path,
|
|
899
|
-
target_kind, target_ref, metadata_json
|
|
900
|
-
FROM task_history ${where} ORDER BY started_at DESC`)
|
|
901
|
-
.all(...params);
|
|
902
|
-
}
|
|
903
|
-
/**
|
|
904
|
-
* Read COMPLETED `akm-improve` task_history runs whose `started_at` falls in
|
|
905
|
-
* `[since, until)` (or `started_at >= since` when `until` is omitted), ordered
|
|
906
|
-
* oldest-first by `started_at`. Only rows with a non-null `completed_at` are
|
|
907
|
-
* returned (in-flight runs are excluded). The `task_id = 'akm-improve'`
|
|
908
|
-
* predicate is fixed because the only caller (commands/health.ts
|
|
909
|
-
* `loadTaskIntervals`) builds wall-time intervals for the improve cron task.
|
|
910
|
-
*
|
|
911
|
-
* Owns the SQL formerly inlined in commands/health.ts. Note the bound is
|
|
912
|
-
* EXCLUSIVE on the upper end (`started_at < ?`) — callers pass an already
|
|
913
|
-
* widened window; this helper does not widen.
|
|
914
|
-
*
|
|
915
|
-
* Connection-lifetime rule (WS5): `.all()` materializes a plain array before
|
|
916
|
-
* returning.
|
|
917
|
-
*/
|
|
918
|
-
export function queryCompletedTaskIntervals(db, since, until) {
|
|
919
|
-
const sql = until
|
|
920
|
-
? "SELECT started_at, completed_at FROM task_history WHERE task_id = 'akm-improve' AND started_at >= ? AND started_at < ? AND completed_at IS NOT NULL ORDER BY started_at"
|
|
921
|
-
: "SELECT started_at, completed_at FROM task_history WHERE task_id = 'akm-improve' AND started_at >= ? AND completed_at IS NOT NULL ORDER BY started_at";
|
|
922
|
-
return (until ? db.prepare(sql).all(since, until) : db.prepare(sql).all(since));
|
|
232
|
+
// Exhausted retries on transient begin failures.
|
|
233
|
+
throw lastBeginErr;
|
|
923
234
|
}
|
|
924
235
|
// ── schema introspection ─────────────────────────────────────────────────────
|
|
925
236
|
/**
|
|
@@ -941,308 +252,3 @@ export function listExistingTableNames(db, names) {
|
|
|
941
252
|
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name IN (${placeholders}) ORDER BY name`)
|
|
942
253
|
.all(...names);
|
|
943
254
|
}
|
|
944
|
-
// ── events.jsonl import ──────────────────────────────────────────────────────
|
|
945
|
-
/**
|
|
946
|
-
* Import all events from an `events.jsonl` file into the `events` table.
|
|
947
|
-
*
|
|
948
|
-
* The old byte-offset `id` is NOT preserved — the database assigns new
|
|
949
|
-
* monotonic integer ids. Callers that persisted a byte-offset cursor must
|
|
950
|
-
* discard it after migration and use the returned `maxId` as the new cursor.
|
|
951
|
-
*
|
|
952
|
-
* **Idempotency**: each line is pre-checked against the `events` table using
|
|
953
|
-
* `(event_type, ts, ref, metadata_json)` as the duplicate key. Lines whose
|
|
954
|
-
* exact tuple is already present are skipped and reported as `skipped` in the
|
|
955
|
-
* return value. This makes the migration safe to re-run (the v0.7→v0.8
|
|
956
|
-
* migration guide recommends re-running the script as a recovery path; without
|
|
957
|
-
* this guard, every re-run would double-import the entire event log).
|
|
958
|
-
*
|
|
959
|
-
* Duplicate detection is per-import-tuple, not a table-wide UNIQUE constraint:
|
|
960
|
-
* the events table has no UNIQUE constraint at runtime so that
|
|
961
|
-
* `appendEvent` can write multiple events with the same ts (sub-millisecond
|
|
962
|
-
* bursts produce identical `(event_type, ts, ref)` triples in practice). The
|
|
963
|
-
* SELECT-first check is scoped to the import path only.
|
|
964
|
-
*
|
|
965
|
-
* The import is wrapped in a single transaction for atomicity.
|
|
966
|
-
*
|
|
967
|
-
* @param db - Open state.db connection.
|
|
968
|
-
* @param jsonlPath - Absolute path to the events.jsonl file to import.
|
|
969
|
-
* @returns Number of rows inserted, the max id assigned, and the
|
|
970
|
-
* count of rows skipped because an identical event already
|
|
971
|
-
* existed in the table.
|
|
972
|
-
*/
|
|
973
|
-
export async function importEventsJsonl(db, jsonlPath) {
|
|
974
|
-
const { readFileSync, existsSync } = await import("node:fs");
|
|
975
|
-
if (!existsSync(jsonlPath)) {
|
|
976
|
-
return { imported: 0, maxId: 0, skipped: 0 };
|
|
977
|
-
}
|
|
978
|
-
const text = readFileSync(jsonlPath, "utf8");
|
|
979
|
-
const lines = text.split("\n").filter((l) => l.trim().length > 0);
|
|
980
|
-
let imported = 0;
|
|
981
|
-
let maxId = 0;
|
|
982
|
-
let skipped = 0;
|
|
983
|
-
const insertStmt = db.prepare(`INSERT INTO events (event_type, ts, ref, metadata_json)
|
|
984
|
-
VALUES (?, ?, ?, ?)
|
|
985
|
-
RETURNING id`);
|
|
986
|
-
// Dedup pre-check: matches by the full tuple including metadata_json so an
|
|
987
|
-
// import is idempotent over identical rows but does not collide with two
|
|
988
|
-
// genuinely different events that happen to share (event_type, ts, ref).
|
|
989
|
-
//
|
|
990
|
-
// Uses IS for ref so two NULL refs compare equal (a plain `=` would treat
|
|
991
|
-
// NULL = NULL as NULL and the row would be re-inserted on every run).
|
|
992
|
-
const existsStmt = db.prepare(`SELECT 1 FROM events
|
|
993
|
-
WHERE event_type = ?
|
|
994
|
-
AND ts = ?
|
|
995
|
-
AND ref IS ?
|
|
996
|
-
AND metadata_json = ?
|
|
997
|
-
LIMIT 1`);
|
|
998
|
-
db.transaction(() => {
|
|
999
|
-
for (const line of lines) {
|
|
1000
|
-
let parsed;
|
|
1001
|
-
try {
|
|
1002
|
-
parsed = JSON.parse(line);
|
|
1003
|
-
}
|
|
1004
|
-
catch {
|
|
1005
|
-
continue; // skip malformed lines — same behaviour as readEvents()
|
|
1006
|
-
}
|
|
1007
|
-
const eventType = typeof parsed.eventType === "string" ? parsed.eventType : "unknown";
|
|
1008
|
-
const ts = typeof parsed.ts === "string" ? parsed.ts : new Date().toISOString();
|
|
1009
|
-
const ref = typeof parsed.ref === "string" ? parsed.ref : null;
|
|
1010
|
-
const metadata = parsed.metadata !== undefined && typeof parsed.metadata === "object" ? JSON.stringify(parsed.metadata) : "{}";
|
|
1011
|
-
const duplicate = existsStmt.get(eventType, ts, ref, metadata);
|
|
1012
|
-
if (duplicate) {
|
|
1013
|
-
skipped++;
|
|
1014
|
-
continue;
|
|
1015
|
-
}
|
|
1016
|
-
const result = insertStmt.get(eventType, ts, ref, metadata);
|
|
1017
|
-
if (result) {
|
|
1018
|
-
imported++;
|
|
1019
|
-
if (result.id > maxId)
|
|
1020
|
-
maxId = result.id;
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
})();
|
|
1024
|
-
return { imported, maxId, skipped };
|
|
1025
|
-
}
|
|
1026
|
-
/**
|
|
1027
|
-
* Compute the cheap aggregate metrics blob from a full improve result.
|
|
1028
|
-
*
|
|
1029
|
-
* Pure function — no I/O. Used by {@link recordImproveRun} to populate
|
|
1030
|
-
* `metrics_json`. Exposed for tests and for any future call site that wants
|
|
1031
|
-
* the same aggregation logic without hitting state.db.
|
|
1032
|
-
*/
|
|
1033
|
-
export function computeImproveRunMetrics(result) {
|
|
1034
|
-
const plannedCount = Array.isArray(result.plannedRefs) ? result.plannedRefs.length : 0;
|
|
1035
|
-
const actions = Array.isArray(result.actions) ? result.actions : [];
|
|
1036
|
-
const actionsCount = actions.length;
|
|
1037
|
-
let acceptedCount = 0;
|
|
1038
|
-
let rejectedCount = 0;
|
|
1039
|
-
let autoAcceptedCount = 0;
|
|
1040
|
-
let errorCount = 0;
|
|
1041
|
-
for (const action of actions) {
|
|
1042
|
-
// Bucketing delegated to the shared classifyImproveAction so this aggregate
|
|
1043
|
-
// and the improve_completed event in improve.ts can never disagree, and so a
|
|
1044
|
-
// new union variant is a compile error rather than a silent drop. Note:
|
|
1045
|
-
// `reflect-guard-rejected` now counts as "rejected" (previously this switch
|
|
1046
|
-
// omitted it entirely — a data-integrity miscount). "noop" (memory-prune) is
|
|
1047
|
-
// intentionally counted in none of the three numeric buckets.
|
|
1048
|
-
switch (classifyImproveAction(action.mode)) {
|
|
1049
|
-
case "accepted":
|
|
1050
|
-
acceptedCount++;
|
|
1051
|
-
break;
|
|
1052
|
-
case "rejected":
|
|
1053
|
-
rejectedCount++;
|
|
1054
|
-
break;
|
|
1055
|
-
case "error":
|
|
1056
|
-
errorCount++;
|
|
1057
|
-
break;
|
|
1058
|
-
case "noop":
|
|
1059
|
-
break;
|
|
1060
|
-
}
|
|
1061
|
-
// Legacy: pre-gate action results may carry autoAccepted: true (reflect path).
|
|
1062
|
-
const r = action.result;
|
|
1063
|
-
if (r && r.autoAccepted === true)
|
|
1064
|
-
autoAcceptedCount++;
|
|
1065
|
-
}
|
|
1066
|
-
// Add gate-promoted count from the unified PostPhaseAutoAcceptGate (all phases).
|
|
1067
|
-
autoAcceptedCount += result.gateAutoAcceptedCount ?? 0;
|
|
1068
|
-
return { plannedCount, actionsCount, acceptedCount, rejectedCount, autoAcceptedCount, errorCount };
|
|
1069
|
-
}
|
|
1070
|
-
/**
|
|
1071
|
-
* Insert a single improve-run row into `improve_runs`. Uses parameterised SQL.
|
|
1072
|
-
*
|
|
1073
|
-
* Idempotency: the table's PRIMARY KEY is `id`, so re-running with the same
|
|
1074
|
-
* runId would error. Callers mint a fresh runId per invocation via
|
|
1075
|
-
* {@link buildImproveRunId} so this is not a concern in practice — but the
|
|
1076
|
-
* default behaviour is INSERT (not REPLACE) so accidental dupes surface as
|
|
1077
|
-
* a SQLite constraint error rather than silently overwriting a prior record.
|
|
1078
|
-
*
|
|
1079
|
-
* The `metrics` parameter defaults to the output of
|
|
1080
|
-
* {@link computeImproveRunMetrics} when not supplied. Pass an explicit
|
|
1081
|
-
* `metrics` object to override the derivation (e.g. tests).
|
|
1082
|
-
*/
|
|
1083
|
-
export function recordImproveRun(db, input) {
|
|
1084
|
-
const metricsObj = input.metrics ?? computeImproveRunMetrics(input.result);
|
|
1085
|
-
db.prepare(`
|
|
1086
|
-
INSERT INTO improve_runs
|
|
1087
|
-
(id, started_at, completed_at, stash_dir, dry_run, profile,
|
|
1088
|
-
scope_mode, scope_value, guidance, ok, result_json, metrics_json, metadata_json)
|
|
1089
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1090
|
-
`).run(input.id, input.startedAt, input.completedAt, input.stashDir, input.dryRun ? 1 : 0, input.profile, input.scopeMode, input.scopeValue, input.guidance, input.ok ? 1 : 0, JSON.stringify(input.result), JSON.stringify(metricsObj), JSON.stringify(input.metadata ?? {}));
|
|
1091
|
-
}
|
|
1092
|
-
/**
|
|
1093
|
-
* Read real (non-dry-run) improve_runs rows whose `started_at` falls in the
|
|
1094
|
-
* window `[since, until)`. When `until` is omitted the window is open-ended
|
|
1095
|
-
* (`started_at >= since`). Rows are returned newest-first (`ORDER BY
|
|
1096
|
-
* started_at DESC`).
|
|
1097
|
-
*
|
|
1098
|
-
* Owns the SQL formerly inlined in commands/health.ts (`loadImproveRunRows`).
|
|
1099
|
-
* The `dry_run = 0` filter is first-class so dry-run probes never pollute
|
|
1100
|
-
* productivity audits.
|
|
1101
|
-
*
|
|
1102
|
-
* Connection-lifetime rule (WS5): `.all()` fully materializes the result set
|
|
1103
|
-
* into a plain array before returning — no live cursor escapes the caller's
|
|
1104
|
-
* `openStateDatabase` scope.
|
|
1105
|
-
*/
|
|
1106
|
-
export function queryImproveRuns(db, since, until) {
|
|
1107
|
-
const sql = until
|
|
1108
|
-
? "SELECT id, started_at, completed_at, ok, scope_mode, scope_value, result_json FROM improve_runs WHERE started_at >= ? AND started_at < ? AND dry_run = 0 ORDER BY started_at DESC"
|
|
1109
|
-
: "SELECT id, started_at, completed_at, ok, scope_mode, scope_value, result_json FROM improve_runs WHERE started_at >= ? AND dry_run = 0 ORDER BY started_at DESC";
|
|
1110
|
-
return (until ? db.prepare(sql).all(since, until) : db.prepare(sql).all(since));
|
|
1111
|
-
}
|
|
1112
|
-
/**
|
|
1113
|
-
* Delete improve_runs rows older than `retentionDays` (default: 90). Mirrors
|
|
1114
|
-
* {@link purgeOldEvents} — same default, same return shape (number of rows
|
|
1115
|
-
* actually deleted), same disabled-when-non-finite semantics.
|
|
1116
|
-
*
|
|
1117
|
-
* Safe to call from the improve post-loop maintenance pass alongside
|
|
1118
|
-
* `purgeOldEvents(db, retentionDays)`.
|
|
1119
|
-
*/
|
|
1120
|
-
export function purgeOldImproveRuns(db, retentionDays = 90) {
|
|
1121
|
-
if (!Number.isFinite(retentionDays) || retentionDays <= 0)
|
|
1122
|
-
return 0;
|
|
1123
|
-
const cutoff = new Date(Date.now() - retentionDays * 86_400_000).toISOString();
|
|
1124
|
-
const result = db.prepare("DELETE FROM improve_runs WHERE started_at < ?").run(cutoff);
|
|
1125
|
-
const changes = result.changes ?? 0;
|
|
1126
|
-
return typeof changes === "bigint" ? Number(changes) : changes;
|
|
1127
|
-
}
|
|
1128
|
-
/**
|
|
1129
|
-
* Record (or update) one session's extract outcome. INSERT-OR-REPLACE so the
|
|
1130
|
-
* row reflects the most recent run; downstream skip-logic compares
|
|
1131
|
-
* `session_ended_at` against the live session metadata to decide if anything
|
|
1132
|
-
* new arrived since `processed_at`.
|
|
1133
|
-
*/
|
|
1134
|
-
export function upsertExtractedSession(db, input) {
|
|
1135
|
-
const endedAtIso = typeof input.sessionEndedAt === "number" && Number.isFinite(input.sessionEndedAt)
|
|
1136
|
-
? new Date(input.sessionEndedAt).toISOString()
|
|
1137
|
-
: null;
|
|
1138
|
-
db.prepare(`
|
|
1139
|
-
INSERT OR REPLACE INTO extract_sessions_seen
|
|
1140
|
-
(harness, session_id, processed_at, session_ended_at, outcome,
|
|
1141
|
-
candidate_count, proposal_count, rationale, source_run, metadata_json)
|
|
1142
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1143
|
-
`).run(input.harness, input.sessionId, input.processedAt, endedAtIso, input.outcome, input.candidateCount, input.proposalCount, input.rationale ?? null, input.sourceRun ?? null, JSON.stringify(input.metadata ?? {}));
|
|
1144
|
-
}
|
|
1145
|
-
/**
|
|
1146
|
-
* Fetch a single session's last extract record, or `undefined` when the
|
|
1147
|
-
* session has never been processed.
|
|
1148
|
-
*/
|
|
1149
|
-
export function getExtractedSession(db, harness, sessionId) {
|
|
1150
|
-
// bun:sqlite returns null (not undefined) when no row matches — normalize so
|
|
1151
|
-
// callers can rely on `if (!row)` and `toBeUndefined()` equivalently.
|
|
1152
|
-
const row = db
|
|
1153
|
-
.prepare("SELECT * FROM extract_sessions_seen WHERE harness = ? AND session_id = ?")
|
|
1154
|
-
.get(harness, sessionId);
|
|
1155
|
-
return row ?? undefined;
|
|
1156
|
-
}
|
|
1157
|
-
/**
|
|
1158
|
-
* Bulk-fetch session-extract status for a list of sessionIds in one harness.
|
|
1159
|
-
* Returns a Map keyed by sessionId so callers can do O(1) lookups while
|
|
1160
|
-
* iterating the discovery list.
|
|
1161
|
-
*/
|
|
1162
|
-
export function getExtractedSessionsMap(db, harness, sessionIds) {
|
|
1163
|
-
const out = new Map();
|
|
1164
|
-
if (sessionIds.length === 0)
|
|
1165
|
-
return out;
|
|
1166
|
-
// SQLite has a ~999 param ceiling; chunk if a caller ever exceeds that.
|
|
1167
|
-
const CHUNK = 500;
|
|
1168
|
-
for (let i = 0; i < sessionIds.length; i += CHUNK) {
|
|
1169
|
-
const chunk = sessionIds.slice(i, i + CHUNK);
|
|
1170
|
-
const placeholders = chunk.map(() => "?").join(",");
|
|
1171
|
-
const rows = db
|
|
1172
|
-
.prepare(`SELECT * FROM extract_sessions_seen
|
|
1173
|
-
WHERE harness = ? AND session_id IN (${placeholders})`)
|
|
1174
|
-
.all(harness, ...chunk);
|
|
1175
|
-
for (const row of rows)
|
|
1176
|
-
out.set(row.session_id, row);
|
|
1177
|
-
}
|
|
1178
|
-
return out;
|
|
1179
|
-
}
|
|
1180
|
-
/**
|
|
1181
|
-
* Decide whether a session should be skipped because the extractor has
|
|
1182
|
-
* already processed it AND nothing has changed since. The "anything new since
|
|
1183
|
-
* last extract?" rule is: the live `sessionEndedAtMs` is strictly later than
|
|
1184
|
-
* the recorded `session_ended_at`. Same-or-earlier endedAt means we'd be
|
|
1185
|
-
* re-processing the exact same content for no gain.
|
|
1186
|
-
*
|
|
1187
|
-
* Returns:
|
|
1188
|
-
* - `false` — no prior row, or session has new content since last extract.
|
|
1189
|
-
* The caller should process it.
|
|
1190
|
-
* - `true` — the session was already processed and hasn't been updated.
|
|
1191
|
-
* The caller should skip.
|
|
1192
|
-
*/
|
|
1193
|
-
export function shouldSkipAlreadyExtractedSession(prior, liveSessionEndedAtMs) {
|
|
1194
|
-
if (!prior)
|
|
1195
|
-
return false;
|
|
1196
|
-
// No live timestamp → can't tell if anything's new. Be conservative and
|
|
1197
|
-
// skip — the operator can pass --force later if we add it.
|
|
1198
|
-
if (typeof liveSessionEndedAtMs !== "number" || !Number.isFinite(liveSessionEndedAtMs)) {
|
|
1199
|
-
return true;
|
|
1200
|
-
}
|
|
1201
|
-
const priorMs = prior.session_ended_at ? Date.parse(prior.session_ended_at) : Number.NaN;
|
|
1202
|
-
if (!Number.isFinite(priorMs))
|
|
1203
|
-
return false;
|
|
1204
|
-
// Re-process when there's new content; skip when the session is unchanged.
|
|
1205
|
-
return liveSessionEndedAtMs <= priorMs;
|
|
1206
|
-
}
|
|
1207
|
-
// ── registry_index_cache (goes in index.db, not state.db) ───────────────────
|
|
1208
|
-
/**
|
|
1209
|
-
* DDL for the `registry_index_cache` table that lives in the EXISTING index.db
|
|
1210
|
-
* (managed by src/indexer/db/db.ts).
|
|
1211
|
-
*
|
|
1212
|
-
* Design: uses the same migration-safe ADD COLUMN approach. The table is
|
|
1213
|
-
* created with CREATE TABLE IF NOT EXISTS so it is safe to call inside
|
|
1214
|
-
* ensureSchema() or as a standalone migration.
|
|
1215
|
-
*
|
|
1216
|
-
* Purpose: caches the result of resolving and fetching remote registry stash
|
|
1217
|
-
* indexes so `akm search` does not hit the network on every invocation.
|
|
1218
|
-
*
|
|
1219
|
-
* Indexed (query) columns:
|
|
1220
|
-
* registry_url TEXT PK — canonical URL of the registry; cache key.
|
|
1221
|
-
* fetched_at TEXT — ISO-8601; used to detect stale entries (TTL).
|
|
1222
|
-
* etag TEXT — HTTP ETag for conditional GET (If-None-Match).
|
|
1223
|
-
* last_modified TEXT — HTTP Last-Modified for conditional GET.
|
|
1224
|
-
*
|
|
1225
|
-
* Non-indexed payload:
|
|
1226
|
-
* index_json TEXT — JSON blob of the fetched registry index document.
|
|
1227
|
-
*
|
|
1228
|
-
* ADD COLUMN extension points (future migrations in db.ts):
|
|
1229
|
-
* ALTER TABLE registry_index_cache ADD COLUMN schema_version INTEGER DEFAULT 1;
|
|
1230
|
-
* ALTER TABLE registry_index_cache ADD COLUMN kit_count INTEGER DEFAULT NULL;
|
|
1231
|
-
* ALTER TABLE registry_index_cache ADD COLUMN error_message TEXT DEFAULT NULL;
|
|
1232
|
-
*
|
|
1233
|
-
* To add this table to index.db, call ensureRegistryIndexCacheSchema(db) from
|
|
1234
|
-
* within ensureSchema() in src/indexer/db/db.ts, or add it as a new CREATE TABLE
|
|
1235
|
-
* IF NOT EXISTS block inside the existing ensureSchema() call.
|
|
1236
|
-
*/
|
|
1237
|
-
export const REGISTRY_INDEX_CACHE_DDL = `
|
|
1238
|
-
CREATE TABLE IF NOT EXISTS registry_index_cache (
|
|
1239
|
-
registry_url TEXT PRIMARY KEY,
|
|
1240
|
-
fetched_at TEXT NOT NULL,
|
|
1241
|
-
etag TEXT,
|
|
1242
|
-
last_modified TEXT,
|
|
1243
|
-
index_json TEXT NOT NULL DEFAULT '{}'
|
|
1244
|
-
);
|
|
1245
|
-
|
|
1246
|
-
CREATE INDEX IF NOT EXISTS idx_registry_cache_fetched
|
|
1247
|
-
ON registry_index_cache(fetched_at);
|
|
1248
|
-
`;
|