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
|
@@ -9,8 +9,8 @@ import { VALID_HARNESS_IDS } from "../../integrations/harnesses/index.js";
|
|
|
9
9
|
/**
|
|
10
10
|
* Canonical list of valid agent harness / platform ids. Re-exported from the
|
|
11
11
|
* unified harness registry (#562) so the Zod `AgentPlatformSchema` enum, the
|
|
12
|
-
* `
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* `AgentProfileConfig` platform union, and setup's `DetectedHarness` union all
|
|
13
|
+
* derive from one place and cannot drift. Add a harness in
|
|
14
|
+
* `src/integrations/harnesses/index.ts`.
|
|
15
15
|
*/
|
|
16
16
|
export { VALID_HARNESS_IDS };
|
|
@@ -13,20 +13,9 @@ import { warn } from "../warn.js";
|
|
|
13
13
|
// Canonical harness-id source of truth (#565) — runtime value re-export.
|
|
14
14
|
export { VALID_HARNESS_IDS } from "./config-types.js";
|
|
15
15
|
// ── Feedback failure-mode constants (F-3 / #384) ────────────────────────────
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
* Structured failure modes enable aggregation across feedback events so the
|
|
20
|
-
* distill pipeline can detect that "5 assets failed for the same reason" and
|
|
21
|
-
* act on it — free-text strings about the same issue are not aggregatable.
|
|
22
|
-
*/
|
|
23
|
-
export const FEEDBACK_FAILURE_MODES = [
|
|
24
|
-
"incorrect", // Factually wrong or logically flawed content
|
|
25
|
-
"outdated", // Correct at some point but now stale
|
|
26
|
-
"dangerous", // Could cause harm if followed (security, safety)
|
|
27
|
-
"incomplete", // Missing key steps, context, or caveats
|
|
28
|
-
"redundant", // Duplicates another asset without adding value
|
|
29
|
-
];
|
|
16
|
+
// Canonical taxonomy lives in the schema/validator layer; re-exported here so
|
|
17
|
+
// existing `../core/config/config` import sites keep working.
|
|
18
|
+
export { FEEDBACK_FAILURE_MODES } from "./config-schema.js";
|
|
30
19
|
/**
|
|
31
20
|
* Default value for {@link IndexPassConfig.graphExtractionBatchSize}. Chosen
|
|
32
21
|
* empirically: 4 amortises the per-call HTTP overhead 4× while keeping the
|
|
@@ -205,6 +194,25 @@ export function getDefaultLlmConfig(config) {
|
|
|
205
194
|
return undefined;
|
|
206
195
|
return config.profiles?.llm?.[defaultName];
|
|
207
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Resolve the per-process config section for an improve process,
|
|
199
|
+
* centralizing the deeply-nested lookup
|
|
200
|
+
* `profile?.processes?.<name>` that was previously copy-pasted across the
|
|
201
|
+
* improve command family (20+ call sites).
|
|
202
|
+
*
|
|
203
|
+
* When an `activeProfile` is supplied (the profile resolved for the current
|
|
204
|
+
* `akm improve --profile <name>` run), its per-process override wins; otherwise
|
|
205
|
+
* — and as a fallback when the active profile does not define the section — the
|
|
206
|
+
* lookup falls back to the `"default"` improve profile from the on-disk config.
|
|
207
|
+
* Callers that have not yet threaded the active profile pass only `config` and
|
|
208
|
+
* get the historical default-profile behavior unchanged.
|
|
209
|
+
*/
|
|
210
|
+
export function getImproveProcessConfig(config, processName, activeProfile) {
|
|
211
|
+
const fromActiveProfile = activeProfile?.processes?.[processName];
|
|
212
|
+
if (fromActiveProfile !== undefined)
|
|
213
|
+
return fromActiveProfile;
|
|
214
|
+
return config.profiles?.improve?.default?.processes?.[processName];
|
|
215
|
+
}
|
|
208
216
|
/**
|
|
209
217
|
* Run `migrateConfigShape` on the raw text and — unless `AKM_NO_AUTO_MIGRATE=1`
|
|
210
218
|
* is set — persist the migrated result. Returns the (possibly migrated) text
|
|
@@ -221,7 +229,18 @@ function maybeAutoMigrateConfigFile(configPath, text) {
|
|
|
221
229
|
catch {
|
|
222
230
|
return text; // Malformed JSON — let parseAndValidate surface the error.
|
|
223
231
|
}
|
|
224
|
-
|
|
232
|
+
// Downgrade protection. Skip migration when the on-disk config is NEWER than
|
|
233
|
+
// this binary (=== 1), OR when it carries a configVersion we cannot order
|
|
234
|
+
// against ours (compareConfigVersion returns undefined for an unparseable
|
|
235
|
+
// value — e.g. one written by a newer/foreign akm). Migrating such a config
|
|
236
|
+
// could strip fields a newer binary added, a cross-version data-loss path.
|
|
237
|
+
//
|
|
238
|
+
// A MISSING configVersion (absent → undefined) is a legacy pre-versioning
|
|
239
|
+
// config that MUST still migrate, so the unparseable-skip is gated on the
|
|
240
|
+
// field being PRESENT (`onDiskVersion !== undefined`).
|
|
241
|
+
const onDiskVersion = obj.configVersion;
|
|
242
|
+
const versionOrder = compareConfigVersion(onDiskVersion, CURRENT_CONFIG_VERSION);
|
|
243
|
+
if (versionOrder === 1 || (onDiskVersion !== undefined && versionOrder === undefined)) {
|
|
225
244
|
return text;
|
|
226
245
|
}
|
|
227
246
|
const { changed, result } = migrateConfigShape(obj);
|
|
@@ -249,8 +268,8 @@ function maybeAutoMigrateConfigFile(configPath, text) {
|
|
|
249
268
|
" to preview a dry-run diff: akm config migrate --dry-run --print-diff",
|
|
250
269
|
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
|
251
270
|
].join("\n");
|
|
252
|
-
process.stderr
|
|
253
|
-
process.stdout
|
|
271
|
+
process.stderr?.write?.(`${banner}\n`);
|
|
272
|
+
process.stdout?.write?.(`${banner}\n`);
|
|
254
273
|
}
|
|
255
274
|
catch (err) {
|
|
256
275
|
// #461: never return migrated bytes when disk write fails — that triggers
|
|
@@ -267,15 +286,32 @@ export function loadConfig() {
|
|
|
267
286
|
warnIfProjectConfigPresent(process.cwd());
|
|
268
287
|
return loadUserConfig();
|
|
269
288
|
}
|
|
289
|
+
let saveConfigOverride;
|
|
290
|
+
/** TEST-ONLY. Swap the implementation of `saveConfig`; pass undefined to restore. */
|
|
291
|
+
export function _setSaveConfigForTests(fake) {
|
|
292
|
+
saveConfigOverride = fake;
|
|
293
|
+
}
|
|
270
294
|
export function saveConfig(config) {
|
|
295
|
+
if (saveConfigOverride) {
|
|
296
|
+
saveConfigOverride(config);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
saveConfigReal(config);
|
|
300
|
+
}
|
|
301
|
+
function saveConfigReal(config) {
|
|
271
302
|
cachedConfig = undefined;
|
|
272
303
|
const configPath = getConfigPath();
|
|
273
304
|
const dir = path.dirname(configPath);
|
|
274
305
|
fs.mkdirSync(dir, { recursive: true });
|
|
275
306
|
const sanitized = sanitizeConfigForWrite(config);
|
|
276
|
-
// Final validation gate before bytes hit disk.
|
|
277
|
-
//
|
|
278
|
-
//
|
|
307
|
+
// Final validation gate before bytes hit disk. Runs the FULL schema —
|
|
308
|
+
// including the cross-field superRefine guards (removed `feedbackDistillation`
|
|
309
|
+
// process key, `defaultWriteTarget` resolution, writable npm/website sources)
|
|
310
|
+
// and all type/enum/range checks — so an `akm config set` (leaf OR object
|
|
311
|
+
// form) cannot persist a guard-violating or mistyped value. NOTE: unknown
|
|
312
|
+
// keys are intentionally NOT rejected here — object schemas are `.passthrough()`
|
|
313
|
+
// so cross-version skew round-trips (see config-schema.ts header); the lenient
|
|
314
|
+
// tolerance is by design, not an oversight.
|
|
279
315
|
const parseResult = AkmConfigSchema.safeParse(sanitized);
|
|
280
316
|
if (!parseResult.success) {
|
|
281
317
|
const lines = parseResult.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
@@ -398,7 +434,11 @@ export function resolveSecret(value) {
|
|
|
398
434
|
* filtering out the reserved feature-section keys so callers don't mistake
|
|
399
435
|
* `metadataEnhance` / `stalenessDetection` for a pass.
|
|
400
436
|
*/
|
|
401
|
-
/**
|
|
437
|
+
/**
|
|
438
|
+
* Reserved well-known keys on IndexConfig that are NOT per-pass entries.
|
|
439
|
+
* `stalenessDetection` is retired (10-Q3) but stays reserved so a leftover
|
|
440
|
+
* config section is never misread as a pass entry.
|
|
441
|
+
*/
|
|
402
442
|
const INDEX_RESERVED_KEYS = new Set(["metadataEnhance", "stalenessDetection"]);
|
|
403
443
|
export function getIndexPassConfig(config, passName) {
|
|
404
444
|
if (!config)
|
|
@@ -450,7 +490,12 @@ function applyRuntimeEnvApiKeys(config) {
|
|
|
450
490
|
}
|
|
451
491
|
// LLM profile keys: AKM_LLM_API_KEY for the default profile, then
|
|
452
492
|
// AKM_PROFILE_<UPPER>_API_KEY for any profile (per-profile wins).
|
|
453
|
-
|
|
493
|
+
// Resolve the default profile the SAME way the rest of the config layer does
|
|
494
|
+
// (resolveDefaultLlmProfileName), so the implicit `profiles.llm.default`
|
|
495
|
+
// fallback is honored. Keying off the raw `defaults.llm` field alone silently
|
|
496
|
+
// dropped AKM_LLM_API_KEY for configs that rely on the implicit default —
|
|
497
|
+
// the same no-op-run class as the 2026-05-23 incident.
|
|
498
|
+
const defaultProfile = resolveDefaultLlmProfileName(next);
|
|
454
499
|
if (next.profiles?.llm) {
|
|
455
500
|
const updated = { ...next.profiles.llm };
|
|
456
501
|
let changed = false;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Generic recursive object merge, extracted from the setup wizard (it is not
|
|
6
|
+
* setup-specific). Plain objects merge key-by-key; arrays and scalars replace
|
|
7
|
+
* wholesale. Used to apply a partial `--file` config over the existing config
|
|
8
|
+
* without dropping sibling subkeys.
|
|
9
|
+
*/
|
|
10
|
+
/** True for non-null, non-array plain objects. */
|
|
11
|
+
export function isPlainObject(value) {
|
|
12
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Recursively merge `incoming` into `base`: plain objects merge key-by-key,
|
|
16
|
+
* while arrays and scalars replace wholesale. A partial input therefore only
|
|
17
|
+
* updates the keys it carries and never drops sibling subkeys (e.g. a file
|
|
18
|
+
* containing `{ output: { format: "text" } }` leaves `output.detail` intact).
|
|
19
|
+
*
|
|
20
|
+
* `base` is treated as immutable — a fresh object graph is returned.
|
|
21
|
+
*/
|
|
22
|
+
export function deepMergeConfig(base, incoming) {
|
|
23
|
+
if (!isPlainObject(incoming))
|
|
24
|
+
return incoming;
|
|
25
|
+
const baseObj = isPlainObject(base) ? base : {};
|
|
26
|
+
const out = { ...baseObj };
|
|
27
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
28
|
+
if (value === undefined)
|
|
29
|
+
continue;
|
|
30
|
+
if (isPlainObject(value) && isPlainObject(baseObj[key])) {
|
|
31
|
+
out[key] = deepMergeConfig(baseObj[key], value);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
out[key] = value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
package/dist/core/errors.js
CHANGED
|
@@ -14,6 +14,7 @@ const CONFIG_HINTS = {
|
|
|
14
14
|
TEST_ISOLATION_MISSING: "Under bun test, when AKM_STASH_DIR is set you MUST also set XDG_DATA_HOME (or AKM_DATA_DIR) and XDG_STATE_HOME (or AKM_STATE_DIR) to temp directories so the test does not touch the developer's real ~/.local/share/akm or ~/.local/state/akm.",
|
|
15
15
|
SETUP_TMP_STASH_REFUSED: "Use a persistent directory, or set AKM_FORCE_SETUP_TMP_STASH=1 to opt in to a sandboxed setup (setup also pre-sets AKM_STASH_DIR so config and cache writes auto-isolate into $stashDir/.akm/ — host config is preserved).",
|
|
16
16
|
UNSAFE_STASH_DIR: "Choose a path inside your home directory (e.g. ~/akm) or another empty workspace. The stash directory cannot be the filesystem root, your home directory itself, or a sensitive system path like /etc, /var, ~/.config, or ~/.ssh.",
|
|
17
|
+
UNKNOWN_IMPROVE_PROFILE: "Pass one of the listed profile names to `--profile`, or define it under `profiles.improve` in your config. Names are case-sensitive.",
|
|
17
18
|
};
|
|
18
19
|
/** Default hint for each UsageError code. */
|
|
19
20
|
const USAGE_HINTS = {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
export const DEFAULT_CURATE_WEIGHTS = {
|
|
5
|
+
ndcg: 0.5,
|
|
6
|
+
recall: 0.2,
|
|
7
|
+
mrr: 0.1,
|
|
8
|
+
noBannedAboveRequired: 0.2,
|
|
9
|
+
};
|
|
10
|
+
/** nDCG@k with binary relevance: gain 1 for relevant refs, 0 otherwise. */
|
|
11
|
+
export function ndcgAtK(returned, relevant, k) {
|
|
12
|
+
const top = returned.slice(0, k);
|
|
13
|
+
let dcg = 0;
|
|
14
|
+
for (let i = 0; i < top.length; i++) {
|
|
15
|
+
if (relevant.has(top[i]))
|
|
16
|
+
dcg += 1 / Math.log2(i + 2);
|
|
17
|
+
}
|
|
18
|
+
const idealCount = Math.min(k, relevant.size);
|
|
19
|
+
let idcg = 0;
|
|
20
|
+
for (let i = 0; i < idealCount; i++)
|
|
21
|
+
idcg += 1 / Math.log2(i + 2);
|
|
22
|
+
return idcg === 0 ? 1 : dcg / idcg;
|
|
23
|
+
}
|
|
24
|
+
export function recallAtK(returned, relevant, k) {
|
|
25
|
+
if (relevant.size === 0)
|
|
26
|
+
return 1;
|
|
27
|
+
const top = new Set(returned.slice(0, k));
|
|
28
|
+
let hit = 0;
|
|
29
|
+
for (const r of relevant)
|
|
30
|
+
if (top.has(r))
|
|
31
|
+
hit += 1;
|
|
32
|
+
return hit / relevant.size;
|
|
33
|
+
}
|
|
34
|
+
export function mrr(returned, relevant) {
|
|
35
|
+
for (let i = 0; i < returned.length; i++) {
|
|
36
|
+
if (relevant.has(returned[i]))
|
|
37
|
+
return 1 / (i + 1);
|
|
38
|
+
}
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Leapfrog gate. A banned ref "leapfrogs" when it appears ABOVE at least one
|
|
43
|
+
* present relevant ref. Returns the fraction of present banned refs that do
|
|
44
|
+
* NOT leapfrog (1.0 when no banned ref is present, or none leapfrog), plus the
|
|
45
|
+
* raw violation count.
|
|
46
|
+
*/
|
|
47
|
+
export function noBannedAboveRequired(returned, relevant, banned) {
|
|
48
|
+
const rankOf = new Map();
|
|
49
|
+
returned.forEach((ref, i) => {
|
|
50
|
+
if (!rankOf.has(ref))
|
|
51
|
+
rankOf.set(ref, i);
|
|
52
|
+
});
|
|
53
|
+
const relevantRanks = returned.map((ref, i) => (relevant.has(ref) ? i : -1)).filter((i) => i >= 0);
|
|
54
|
+
if (relevantRanks.length === 0) {
|
|
55
|
+
// No relevant ref present to be leapfrogged — gate is vacuously satisfied.
|
|
56
|
+
return { score: 1, leapfrogCount: 0 };
|
|
57
|
+
}
|
|
58
|
+
const worstRelevantRank = Math.max(...relevantRanks);
|
|
59
|
+
const bannedPresent = returned.filter((ref) => banned.has(ref));
|
|
60
|
+
if (bannedPresent.length === 0)
|
|
61
|
+
return { score: 1, leapfrogCount: 0 };
|
|
62
|
+
let leapfrog = 0;
|
|
63
|
+
for (const b of bannedPresent) {
|
|
64
|
+
const rb = rankOf.get(b);
|
|
65
|
+
if (rb !== undefined && rb < worstRelevantRank)
|
|
66
|
+
leapfrog += 1;
|
|
67
|
+
}
|
|
68
|
+
return { score: 1 - leapfrog / bannedPresent.length, leapfrogCount: leapfrog };
|
|
69
|
+
}
|
|
70
|
+
/** Score a single curate result (ordered refs) against its judgment. */
|
|
71
|
+
export function scoreCurateCase(returned, judgment, weights = DEFAULT_CURATE_WEIGHTS) {
|
|
72
|
+
const k = judgment.limit;
|
|
73
|
+
const relevant = new Set(judgment.relevant);
|
|
74
|
+
const banned = new Set(judgment.banned);
|
|
75
|
+
const ndcg = ndcgAtK(returned, relevant, k);
|
|
76
|
+
const recall = recallAtK(returned, relevant, k);
|
|
77
|
+
const rr = mrr(returned, relevant);
|
|
78
|
+
const gate = noBannedAboveRequired(returned, relevant, banned);
|
|
79
|
+
const score = ndcg * weights.ndcg + recall * weights.recall + rr * weights.mrr + gate.score * weights.noBannedAboveRequired;
|
|
80
|
+
return {
|
|
81
|
+
ndcg,
|
|
82
|
+
recall,
|
|
83
|
+
mrr: rr,
|
|
84
|
+
noBannedAboveRequired: gate.score,
|
|
85
|
+
bannedLeapfrogCount: gate.leapfrogCount,
|
|
86
|
+
score,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** Aggregate per-case metrics into a suite summary. */
|
|
90
|
+
export function summarizeCurateMetrics(metrics) {
|
|
91
|
+
const n = metrics.length;
|
|
92
|
+
if (n === 0) {
|
|
93
|
+
return {
|
|
94
|
+
caseCount: 0,
|
|
95
|
+
meanScore: 0,
|
|
96
|
+
meanNdcg: 0,
|
|
97
|
+
meanRecall: 0,
|
|
98
|
+
meanMrr: 0,
|
|
99
|
+
meanNoBannedAboveRequired: 1,
|
|
100
|
+
totalBannedLeapfrog: 0,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const sum = (sel) => metrics.reduce((a, m) => a + sel(m), 0);
|
|
104
|
+
return {
|
|
105
|
+
caseCount: n,
|
|
106
|
+
meanScore: sum((m) => m.score) / n,
|
|
107
|
+
meanNdcg: sum((m) => m.ndcg) / n,
|
|
108
|
+
meanRecall: sum((m) => m.recall) / n,
|
|
109
|
+
meanMrr: sum((m) => m.mrr) / n,
|
|
110
|
+
meanNoBannedAboveRequired: sum((m) => m.noBannedAboveRequired) / n,
|
|
111
|
+
totalBannedLeapfrog: sum((m) => m.bannedLeapfrogCount),
|
|
112
|
+
};
|
|
113
|
+
}
|
package/dist/core/events.js
CHANGED
|
@@ -25,9 +25,10 @@
|
|
|
25
25
|
* - `ts` is ISO-8601 (UTC, millisecond precision).
|
|
26
26
|
*/
|
|
27
27
|
import path from "node:path";
|
|
28
|
+
import { insertEvent, readStateEvents } from "../storage/repositories/events-repository.js";
|
|
28
29
|
import { rethrowIfTestIsolationError } from "./errors.js";
|
|
29
30
|
import { getDataDir } from "./paths.js";
|
|
30
|
-
import {
|
|
31
|
+
import { openStateDatabase, withStateDb } from "./state-db.js";
|
|
31
32
|
import { error } from "./warn.js";
|
|
32
33
|
/**
|
|
33
34
|
* Legacy events.jsonl path — used only by the migration script
|
|
@@ -82,18 +83,14 @@ export function appendEvent(input, ctx) {
|
|
|
82
83
|
// Default path: open, insert, close.
|
|
83
84
|
const dbPath = resolveDbPath(ctx);
|
|
84
85
|
try {
|
|
85
|
-
|
|
86
|
-
try {
|
|
86
|
+
withStateDb((db) => {
|
|
87
87
|
insertEvent(db, {
|
|
88
88
|
eventType: input.eventType,
|
|
89
89
|
ts,
|
|
90
90
|
ref: input.ref,
|
|
91
91
|
metadata: input.metadata,
|
|
92
92
|
});
|
|
93
|
-
}
|
|
94
|
-
finally {
|
|
95
|
-
db.close();
|
|
96
|
-
}
|
|
93
|
+
}, { path: dbPath });
|
|
97
94
|
}
|
|
98
95
|
catch (err) {
|
|
99
96
|
// Never mask the bun-test isolation guard as a silent "events failed".
|
|
@@ -11,16 +11,18 @@ import { assertNever } from "./assert.js";
|
|
|
11
11
|
*
|
|
12
12
|
* Buckets:
|
|
13
13
|
* - `accepted` — a write/content-authoring action succeeded.
|
|
14
|
-
* - `
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
14
|
+
* - `skipped` — the ref was GATED OUT before any content was produced: a
|
|
15
|
+
* cooldown, a signal-delta/eligibility skip, or a distill pool-delta skip.
|
|
16
|
+
* These are NOT rejections of produced content — they are the run declining
|
|
17
|
+
* to act on a ref it had no new reason to touch, and they scale with the
|
|
18
|
+
* whole indexed-ref pool (~13k/run), so folding them into `rejected` made the
|
|
19
|
+
* "accept rate" meaningless (deep-tuning analysis 2026-06-29, finding #1).
|
|
20
|
+
* - `rejected` — the run PRODUCED a change and a content-policy guard then
|
|
21
|
+
* rejected it (`reflect-guard-rejected`). This is the genuine value-rejection
|
|
22
|
+
* signal; it is small and meaningful, no longer drowned by gated skips.
|
|
21
23
|
* - `error` — the action failed (LLM/runtime error).
|
|
22
24
|
* - `noop` — bookkeeping that is neither a write nor a rejection (memory-prune);
|
|
23
|
-
* intentionally counted in none of the
|
|
25
|
+
* intentionally counted in none of the numeric buckets.
|
|
24
26
|
*
|
|
25
27
|
* The `default: assertNever(mode)` arm makes any future union variant a
|
|
26
28
|
* compile-time error here, forcing an explicit bucket choice.
|
|
@@ -35,6 +37,7 @@ export function classifyImproveAction(mode) {
|
|
|
35
37
|
case "reflect-cooldown":
|
|
36
38
|
case "reflect-skipped":
|
|
37
39
|
case "distill-skipped":
|
|
40
|
+
return "skipped";
|
|
38
41
|
case "reflect-guard-rejected":
|
|
39
42
|
return "rejected";
|
|
40
43
|
case "reflect-failed":
|
|
@@ -46,3 +49,39 @@ export function classifyImproveAction(mode) {
|
|
|
46
49
|
return assertNever(mode);
|
|
47
50
|
}
|
|
48
51
|
}
|
|
52
|
+
/** Upper bound on retained sample refs PER reason in {@link DistillSkippedAggregate}. */
|
|
53
|
+
export const DISTILL_SKIPPED_SAMPLE_CAP_PER_REASON = 3;
|
|
54
|
+
/**
|
|
55
|
+
* Partition an action list into the rows to persist and the `distill-skipped`
|
|
56
|
+
* aggregate. Pure — no I/O. Called once at improve-result assembly time so the
|
|
57
|
+
* serialized envelope never carries per-ref distill-skipped rows.
|
|
58
|
+
*
|
|
59
|
+
* Non-`distill-skipped` actions are returned verbatim and in order. When there
|
|
60
|
+
* are zero distill-skipped actions the aggregate is omitted (the envelope stays
|
|
61
|
+
* byte-identical to a run that skipped nothing).
|
|
62
|
+
*/
|
|
63
|
+
export function foldDistillSkipped(actions) {
|
|
64
|
+
const kept = [];
|
|
65
|
+
const byReason = {};
|
|
66
|
+
const samples = [];
|
|
67
|
+
const sampleCountByReason = {};
|
|
68
|
+
let total = 0;
|
|
69
|
+
for (const action of actions) {
|
|
70
|
+
if (action.mode !== "distill-skipped") {
|
|
71
|
+
kept.push(action);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
total += 1;
|
|
75
|
+
const r = action.result;
|
|
76
|
+
const reason = typeof r?.reason === "string" && r.reason.trim() ? r.reason : "unknown";
|
|
77
|
+
byReason[reason] = (byReason[reason] ?? 0) + 1;
|
|
78
|
+
const seen = sampleCountByReason[reason] ?? 0;
|
|
79
|
+
if (seen < DISTILL_SKIPPED_SAMPLE_CAP_PER_REASON) {
|
|
80
|
+
sampleCountByReason[reason] = seen + 1;
|
|
81
|
+
samples.push({ ref: action.ref, reason });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (total === 0)
|
|
85
|
+
return { actions: kept };
|
|
86
|
+
return { actions: kept, aggregate: { total, byReason, samples } };
|
|
87
|
+
}
|
package/dist/core/logs-db.js
CHANGED
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
* Log lines are high-volume, append-only, and freely purgeable; state.db rows
|
|
17
17
|
* (events, proposals, task_history) are durable records. Separating them keeps
|
|
18
18
|
* state.db small and lets log retention be aggressive without touching durable
|
|
19
|
-
* state.
|
|
20
|
-
*
|
|
19
|
+
* state. Callers that need to correlate a task_history row with its log lines do
|
|
20
|
+
* an application-side join on the {@link buildTaskRunId} key (e.g. `health` via
|
|
21
|
+
* {@link getLoggedRunIds}) — no SQLite ATTACH, so the split survives a future
|
|
22
|
+
* provider change.
|
|
21
23
|
*
|
|
22
24
|
* ## run_id
|
|
23
25
|
*
|
|
@@ -35,12 +37,10 @@
|
|
|
35
37
|
*
|
|
36
38
|
* @module logs-db
|
|
37
39
|
*/
|
|
38
|
-
import fs from "node:fs";
|
|
39
40
|
import path from "node:path";
|
|
40
|
-
import { openDatabase } from "../storage/database.js";
|
|
41
41
|
import { runMigrations as runSqliteMigrations } from "../storage/engines/sqlite-migrations.js";
|
|
42
|
+
import { openManagedDatabase } from "../storage/managed-db.js";
|
|
42
43
|
import { getDataDir } from "./paths.js";
|
|
43
|
-
import { getStateDbPath } from "./state-db.js";
|
|
44
44
|
// ── Path helper ──────────────────────────────────────────────────────────────
|
|
45
45
|
/**
|
|
46
46
|
* Default path: `<dataDir>/logs.db` — alongside state.db so cooperating
|
|
@@ -70,16 +70,13 @@ export function getLogsDbPath() {
|
|
|
70
70
|
*/
|
|
71
71
|
export function openLogsDatabase(dbPath) {
|
|
72
72
|
const resolvedPath = dbPath ?? getLogsDbPath();
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
db.exec("PRAGMA busy_timeout = 30000");
|
|
81
|
-
runMigrations(db);
|
|
82
|
-
return db;
|
|
73
|
+
// foreignKeys:false preserves this opener's historical behaviour — logs.db
|
|
74
|
+
// has never enforced foreign keys.
|
|
75
|
+
return openManagedDatabase({
|
|
76
|
+
path: resolvedPath,
|
|
77
|
+
pragmas: { dataDir: path.dirname(resolvedPath), foreignKeys: false },
|
|
78
|
+
init: runMigrations,
|
|
79
|
+
});
|
|
83
80
|
}
|
|
84
81
|
// ── Migrations ───────────────────────────────────────────────────────────────
|
|
85
82
|
/**
|
|
@@ -145,8 +142,8 @@ export function runMigrations(db) {
|
|
|
145
142
|
* Encode a task run's identity — the unique `(task_id, started_at)` pair from
|
|
146
143
|
* state.db `task_history` — as a single run_id string.
|
|
147
144
|
*
|
|
148
|
-
* The format MUST stay in sync with the
|
|
149
|
-
*
|
|
145
|
+
* The format MUST stay in sync with the application-side join key that callers
|
|
146
|
+
* build from a `task_history` row's `task_id` and `started_at`.
|
|
150
147
|
*/
|
|
151
148
|
export function buildTaskRunId(taskId, startedAtIso) {
|
|
152
149
|
return `${taskId}@${startedAtIso}`;
|
|
@@ -229,64 +226,6 @@ export function getLoggedRunIds(db, runIds) {
|
|
|
229
226
|
}
|
|
230
227
|
return out;
|
|
231
228
|
}
|
|
232
|
-
// ── Cross-db: ATTACH state.db ────────────────────────────────────────────────
|
|
233
|
-
/**
|
|
234
|
-
* ATTACH state.db to an open logs.db handle under the schema name `state`,
|
|
235
|
-
* enabling cross-db joins like task_history × task_logs.
|
|
236
|
-
*
|
|
237
|
-
* The state.db file must already exist (callers always open state.db first in
|
|
238
|
-
* practice); attaching a non-existent path would silently create an empty,
|
|
239
|
-
* unmigrated database file, so this throws instead.
|
|
240
|
-
*/
|
|
241
|
-
export function attachStateDatabase(db, stateDbPath) {
|
|
242
|
-
const resolved = stateDbPath ?? getStateDbPath();
|
|
243
|
-
if (!fs.existsSync(resolved)) {
|
|
244
|
-
throw new Error(`Cannot ATTACH state.db: file does not exist at ${resolved}`);
|
|
245
|
-
}
|
|
246
|
-
// prepare().run() rather than db.run(): both drivers support parameterised
|
|
247
|
-
// ATTACH through a prepared statement, and no other call site uses db.run().
|
|
248
|
-
db.prepare("ATTACH DATABASE ? AS state").run(resolved);
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Convenience: open logs.db with state.db attached as `state`. The returned
|
|
252
|
-
* handle supports cross-db queries such as {@link queryFailedRunLogLines}.
|
|
253
|
-
* Close it like any other handle (DETACH is implicit on close).
|
|
254
|
-
*/
|
|
255
|
-
export function openLogsDatabaseWithState(logsDbPath, stateDbPath) {
|
|
256
|
-
const db = openLogsDatabase(logsDbPath);
|
|
257
|
-
try {
|
|
258
|
-
attachStateDatabase(db, stateDbPath);
|
|
259
|
-
}
|
|
260
|
-
catch (err) {
|
|
261
|
-
db.close();
|
|
262
|
-
throw err;
|
|
263
|
-
}
|
|
264
|
-
return db;
|
|
265
|
-
}
|
|
266
|
-
/**
|
|
267
|
-
* Cross-db join: every log line belonging to a FAILED task_history run whose
|
|
268
|
-
* `started_at` is `>= since` (all failed runs when omitted). Requires a handle
|
|
269
|
-
* opened via {@link openLogsDatabaseWithState}.
|
|
270
|
-
*
|
|
271
|
-
* The join key is the run_id encoding documented on {@link buildTaskRunId}:
|
|
272
|
-
* `task_logs.run_id = task_history.task_id || '@' || task_history.started_at`.
|
|
273
|
-
*/
|
|
274
|
-
export function queryFailedRunLogLines(db, options = {}) {
|
|
275
|
-
const conditions = ["th.status = 'failed'"];
|
|
276
|
-
const params = [];
|
|
277
|
-
if (options.since) {
|
|
278
|
-
conditions.push("th.started_at >= ?");
|
|
279
|
-
params.push(options.since);
|
|
280
|
-
}
|
|
281
|
-
const limit = options.limit !== undefined && options.limit >= 0 ? ` LIMIT ${Math.floor(options.limit)}` : "";
|
|
282
|
-
return db
|
|
283
|
-
.prepare(`SELECT th.task_id, l.run_id, th.started_at, th.status, l.ts, l.stream, l.level, l.line
|
|
284
|
-
FROM state.task_history th
|
|
285
|
-
JOIN task_logs l ON l.run_id = th.task_id || '@' || th.started_at
|
|
286
|
-
WHERE ${conditions.join(" AND ")}
|
|
287
|
-
ORDER BY th.started_at DESC, l.id ASC${limit}`)
|
|
288
|
-
.all(...params);
|
|
289
|
-
}
|
|
290
229
|
// ── Retention ────────────────────────────────────────────────────────────────
|
|
291
230
|
/**
|
|
292
231
|
* Delete task_logs rows older than `retentionDays` (default: 90). Mirrors
|
package/dist/core/parse.js
CHANGED
|
@@ -98,16 +98,24 @@ export function parseJsonResponse(raw) {
|
|
|
98
98
|
* balanced `{ }` or `[ ]` structure in the text and attempts to parse that
|
|
99
99
|
* substring. Returns `undefined` if no valid JSON structure is found.
|
|
100
100
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* returned
|
|
101
|
+
* Shape preference is controlled by {@link ParseEmbeddedJsonOptions.expect}:
|
|
102
|
+
* - `"any"` (default): non-array results are preferred — a `{…}` object found
|
|
103
|
+
* first is returned immediately; arrays (`[…]`) are a fallback.
|
|
104
|
+
* - `"array"`: only top-level arrays are returned. The direct parse is
|
|
105
|
+
* accepted only if it is an array, and the scanner returns the first
|
|
106
|
+
* balanced `[…]` while skipping `{…}` openers entirely.
|
|
104
107
|
*/
|
|
105
|
-
export function parseEmbeddedJsonResponse(raw) {
|
|
108
|
+
export function parseEmbeddedJsonResponse(raw, options) {
|
|
109
|
+
const expectArray = options?.expect === "array";
|
|
106
110
|
const direct = parseJsonResponse(raw);
|
|
107
|
-
if (direct !== undefined)
|
|
111
|
+
if (direct !== undefined && (!expectArray || Array.isArray(direct)))
|
|
108
112
|
return direct;
|
|
109
113
|
const text = escapeJsonStringControls(stripCodeFences(stripThinkBlocks(raw)));
|
|
110
114
|
let arrayFallback;
|
|
115
|
+
// Scan only *top-level* balanced structures: once a `{…}`/`[…]` is matched we
|
|
116
|
+
// jump `start` past its closing bracket rather than re-scanning its interior.
|
|
117
|
+
// This keeps array mode from salvaging an array *nested inside* a leading
|
|
118
|
+
// object (e.g. the `entities` array of a bare `{entities,relations}` object).
|
|
111
119
|
for (let start = 0; start < text.length; start++) {
|
|
112
120
|
const opener = text[start];
|
|
113
121
|
if (opener !== "{" && opener !== "[")
|
|
@@ -116,6 +124,7 @@ export function parseEmbeddedJsonResponse(raw) {
|
|
|
116
124
|
let depth = 0;
|
|
117
125
|
let inString = false;
|
|
118
126
|
let escaped = false;
|
|
127
|
+
let end = -1;
|
|
119
128
|
for (let i = start; i < text.length; i++) {
|
|
120
129
|
const ch = text[i];
|
|
121
130
|
if (inString) {
|
|
@@ -139,20 +148,31 @@ export function parseEmbeddedJsonResponse(raw) {
|
|
|
139
148
|
if (ch === closer) {
|
|
140
149
|
depth -= 1;
|
|
141
150
|
if (depth === 0) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (!Array.isArray(parsed)) {
|
|
145
|
-
return parsed;
|
|
146
|
-
}
|
|
147
|
-
arrayFallback ??= parsed;
|
|
148
|
-
break;
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
151
|
-
break;
|
|
152
|
-
}
|
|
151
|
+
end = i;
|
|
152
|
+
break;
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
|
+
if (end === -1)
|
|
157
|
+
continue; // never balanced — let the next opener try
|
|
158
|
+
try {
|
|
159
|
+
const parsed = JSON.parse(text.slice(start, end + 1));
|
|
160
|
+
if (Array.isArray(parsed)) {
|
|
161
|
+
// First valid array wins in array mode; in "any" mode it is the
|
|
162
|
+
// fallback returned only if no object is found.
|
|
163
|
+
if (expectArray)
|
|
164
|
+
return parsed;
|
|
165
|
+
arrayFallback ??= parsed;
|
|
166
|
+
}
|
|
167
|
+
else if (!expectArray) {
|
|
168
|
+
return parsed;
|
|
169
|
+
}
|
|
170
|
+
// Skip past this balanced structure so we don't descend into it.
|
|
171
|
+
start = end;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// Malformed candidate — advance one char and try the next opener.
|
|
175
|
+
}
|
|
156
176
|
}
|
|
157
177
|
return arrayFallback;
|
|
158
178
|
}
|