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
|
@@ -0,0 +1,239 @@
|
|
|
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
|
+
import { fetchWithRetry } from "../../core/common.js";
|
|
5
|
+
const YOUTUBE_HOSTS = new Set(["www.youtube.com", "youtube.com", "m.youtube.com", "youtu.be"]);
|
|
6
|
+
const WATCH_HOST = "https://www.youtube.com/watch?v=";
|
|
7
|
+
// Captions: the WEB watch-page caption baseUrls are now Proof-of-Origin-Token
|
|
8
|
+
// gated (they carry `exp=xpe` and return an empty body without a `pot` token).
|
|
9
|
+
// The ANDROID InnerTube `player` client still returns UNGATED caption baseUrls
|
|
10
|
+
// that serve the transcript, so captions are sourced from there. See
|
|
11
|
+
// https://github.com/yt-dlp/yt-dlp/issues/13075.
|
|
12
|
+
const INNERTUBE_PLAYER_URL = "https://www.youtube.com/youtubei/v1/player";
|
|
13
|
+
// Long-stable public InnerTube key; only authorizes the player call (not auth).
|
|
14
|
+
const DEFAULT_INNERTUBE_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8";
|
|
15
|
+
const ANDROID_INNERTUBE_CLIENT = {
|
|
16
|
+
clientName: "ANDROID",
|
|
17
|
+
clientVersion: "20.10.38",
|
|
18
|
+
androidSdkVersion: 34,
|
|
19
|
+
hl: "en",
|
|
20
|
+
gl: "US",
|
|
21
|
+
};
|
|
22
|
+
function extractVideoId(url) {
|
|
23
|
+
if (!YOUTUBE_HOSTS.has(url.hostname))
|
|
24
|
+
return null;
|
|
25
|
+
if (url.hostname === "youtu.be") {
|
|
26
|
+
const id = url.pathname.replace(/^\/+/, "").split("/")[0]?.trim();
|
|
27
|
+
return id || null;
|
|
28
|
+
}
|
|
29
|
+
if (url.pathname === "/watch") {
|
|
30
|
+
const id = url.searchParams.get("v")?.trim();
|
|
31
|
+
return id || null;
|
|
32
|
+
}
|
|
33
|
+
if (url.pathname.startsWith("/shorts/")) {
|
|
34
|
+
const id = url.pathname.slice("/shorts/".length).split("/")[0]?.trim();
|
|
35
|
+
return id || null;
|
|
36
|
+
}
|
|
37
|
+
if (url.pathname.startsWith("/embed/")) {
|
|
38
|
+
const id = url.pathname.slice("/embed/".length).split("/")[0]?.trim();
|
|
39
|
+
return id || null;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
function canonicalWatchUrl(videoId) {
|
|
44
|
+
return `${WATCH_HOST}${encodeURIComponent(videoId)}`;
|
|
45
|
+
}
|
|
46
|
+
function decodeEntities(value) {
|
|
47
|
+
const safeFromCodePoint = (codePoint, fallback) => {
|
|
48
|
+
if (!Number.isFinite(codePoint) || codePoint < 0 || codePoint > 0x10ffff)
|
|
49
|
+
return fallback;
|
|
50
|
+
try {
|
|
51
|
+
return String.fromCodePoint(codePoint);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return fallback;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
return value
|
|
58
|
+
.replace(/&#x([0-9a-f]+);/gi, (m, hex) => safeFromCodePoint(Number.parseInt(hex, 16), m))
|
|
59
|
+
.replace(/&#([0-9]+);/g, (m, dec) => safeFromCodePoint(Number.parseInt(dec, 10), m))
|
|
60
|
+
.replace(/"/g, '"')
|
|
61
|
+
.replace(/'|'/g, "'")
|
|
62
|
+
.replace(/</g, "<")
|
|
63
|
+
.replace(/>/g, ">")
|
|
64
|
+
.replace(/&/g, "&");
|
|
65
|
+
}
|
|
66
|
+
function stripTags(value) {
|
|
67
|
+
return value.replace(/<[^>]+>/g, "");
|
|
68
|
+
}
|
|
69
|
+
function extractJsonObjectAfter(marker, source) {
|
|
70
|
+
const markerIndex = source.indexOf(marker);
|
|
71
|
+
if (markerIndex === -1)
|
|
72
|
+
return null;
|
|
73
|
+
const start = source.indexOf("{", markerIndex + marker.length);
|
|
74
|
+
if (start === -1)
|
|
75
|
+
return null;
|
|
76
|
+
let depth = 0;
|
|
77
|
+
let inString = false;
|
|
78
|
+
let escaped = false;
|
|
79
|
+
for (let i = start; i < source.length; i += 1) {
|
|
80
|
+
const char = source[i];
|
|
81
|
+
if (inString) {
|
|
82
|
+
if (escaped) {
|
|
83
|
+
escaped = false;
|
|
84
|
+
}
|
|
85
|
+
else if (char === "\\") {
|
|
86
|
+
escaped = true;
|
|
87
|
+
}
|
|
88
|
+
else if (char === '"') {
|
|
89
|
+
inString = false;
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (char === '"') {
|
|
94
|
+
inString = true;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (char === "{")
|
|
98
|
+
depth += 1;
|
|
99
|
+
if (char === "}") {
|
|
100
|
+
depth -= 1;
|
|
101
|
+
if (depth === 0)
|
|
102
|
+
return source.slice(start, i + 1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
function parsePlayerResponse(html) {
|
|
108
|
+
const jsonText = extractJsonObjectAfter("ytInitialPlayerResponse =", html);
|
|
109
|
+
if (!jsonText)
|
|
110
|
+
return null;
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(jsonText);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function chooseCaptionTrack(tracks) {
|
|
119
|
+
const normalized = tracks
|
|
120
|
+
.map((track) => ({
|
|
121
|
+
baseUrl: typeof track.baseUrl === "string" ? track.baseUrl : "",
|
|
122
|
+
languageCode: typeof track.languageCode === "string" ? track.languageCode : "",
|
|
123
|
+
kind: typeof track.kind === "string" ? track.kind : "",
|
|
124
|
+
}))
|
|
125
|
+
.filter((track) => track.baseUrl);
|
|
126
|
+
if (normalized.length === 0)
|
|
127
|
+
return null;
|
|
128
|
+
const preferred = normalized.find((track) => track.languageCode === "en" && track.kind !== "asr") ??
|
|
129
|
+
normalized.find((track) => track.languageCode.startsWith("en") && track.kind !== "asr") ??
|
|
130
|
+
normalized.find((track) => track.languageCode.startsWith("en")) ??
|
|
131
|
+
normalized[0];
|
|
132
|
+
return preferred.baseUrl;
|
|
133
|
+
}
|
|
134
|
+
async function fetchText(url, timeoutMs, signal) {
|
|
135
|
+
const response = await fetchWithRetry(url, {
|
|
136
|
+
headers: {
|
|
137
|
+
Accept: "text/html,application/json,application/xml,text/xml,text/plain;q=0.9,*/*;q=0.1",
|
|
138
|
+
"User-Agent": "akm-cli youtube fetcher",
|
|
139
|
+
},
|
|
140
|
+
signal,
|
|
141
|
+
}, { timeout: timeoutMs, retries: 1 });
|
|
142
|
+
if (!response.ok)
|
|
143
|
+
return null;
|
|
144
|
+
return response.text();
|
|
145
|
+
}
|
|
146
|
+
function parseTranscript(xml) {
|
|
147
|
+
// Two timedtext shapes: legacy srv1 `<text>` cues, and the format-3
|
|
148
|
+
// `<timedtext format="3">` `<p>` cues that the ANDROID InnerTube caption
|
|
149
|
+
// URLs return. Strip any nested `<s>` word tags via stripTags.
|
|
150
|
+
const matches = [...xml.matchAll(/<text\b[^>]*>([\s\S]*?)<\/text>/g), ...xml.matchAll(/<p\b[^>]*>([\s\S]*?)<\/p>/g)];
|
|
151
|
+
const texts = matches.map((match) => decodeEntities(stripTags(match[1] ?? "")).trim()).filter(Boolean);
|
|
152
|
+
if (texts.length === 0)
|
|
153
|
+
return null;
|
|
154
|
+
return texts.join("\n");
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Fetch caption tracks via the ANDROID InnerTube `player` endpoint, whose
|
|
158
|
+
* caption baseUrls are not POT-gated. Returns [] on any failure so the caller
|
|
159
|
+
* can fall back to the (usually empty) watch-page tracks and degrade to a
|
|
160
|
+
* description-only snapshot.
|
|
161
|
+
*/
|
|
162
|
+
async function fetchInnertubeCaptionTracks(videoId, apiKey, timeoutMs, signal) {
|
|
163
|
+
try {
|
|
164
|
+
const response = await fetchWithRetry(`${INNERTUBE_PLAYER_URL}?key=${encodeURIComponent(apiKey)}`, {
|
|
165
|
+
method: "POST",
|
|
166
|
+
headers: {
|
|
167
|
+
"Content-Type": "application/json",
|
|
168
|
+
"User-Agent": "com.google.android.youtube/20.10.38 (Linux; U; Android 14) gzip",
|
|
169
|
+
},
|
|
170
|
+
body: JSON.stringify({ context: { client: ANDROID_INNERTUBE_CLIENT }, videoId }),
|
|
171
|
+
signal,
|
|
172
|
+
}, { timeout: timeoutMs, retries: 1 });
|
|
173
|
+
if (!response.ok)
|
|
174
|
+
return [];
|
|
175
|
+
const json = (await response.json());
|
|
176
|
+
return json.captions?.playerCaptionsTracklistRenderer?.captionTracks ?? [];
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** Extract the page's InnerTube API key, falling back to the stable default. */
|
|
183
|
+
function extractInnertubeKey(html) {
|
|
184
|
+
return html.match(/"INNERTUBE_API_KEY":"([^"]+)"/)?.[1] ?? DEFAULT_INNERTUBE_KEY;
|
|
185
|
+
}
|
|
186
|
+
const youtubeFetcher = {
|
|
187
|
+
name: "youtube-transcript",
|
|
188
|
+
matches(url) {
|
|
189
|
+
return extractVideoId(url) !== null;
|
|
190
|
+
},
|
|
191
|
+
async fetch(url, context) {
|
|
192
|
+
const videoId = extractVideoId(url);
|
|
193
|
+
if (!videoId)
|
|
194
|
+
return null;
|
|
195
|
+
const watchUrl = canonicalWatchUrl(videoId);
|
|
196
|
+
const watchHtml = await fetchText(watchUrl, context.timeoutMs, context.signal);
|
|
197
|
+
if (!watchHtml)
|
|
198
|
+
return null;
|
|
199
|
+
const playerResponse = parsePlayerResponse(watchHtml);
|
|
200
|
+
if (!playerResponse)
|
|
201
|
+
return null;
|
|
202
|
+
const title = typeof playerResponse.videoDetails?.title === "string" ? playerResponse.videoDetails.title.trim() : "";
|
|
203
|
+
const description = typeof playerResponse.videoDetails?.shortDescription === "string"
|
|
204
|
+
? playerResponse.videoDetails.shortDescription.trim()
|
|
205
|
+
: "";
|
|
206
|
+
const sections = [];
|
|
207
|
+
let hasTranscript = false;
|
|
208
|
+
if (description) {
|
|
209
|
+
sections.push("## Description", "", description);
|
|
210
|
+
}
|
|
211
|
+
// Prefer ungated ANDROID InnerTube caption tracks; fall back to the
|
|
212
|
+
// (now usually empty) watch-page tracks so a description-only snapshot
|
|
213
|
+
// still works when InnerTube is unavailable.
|
|
214
|
+
const apiKey = extractInnertubeKey(watchHtml);
|
|
215
|
+
let tracks = await fetchInnertubeCaptionTracks(videoId, apiKey, context.timeoutMs, context.signal);
|
|
216
|
+
if (tracks.length === 0) {
|
|
217
|
+
tracks = playerResponse.captions?.playerCaptionsTracklistRenderer?.captionTracks ?? [];
|
|
218
|
+
}
|
|
219
|
+
const captionUrl = chooseCaptionTrack(tracks);
|
|
220
|
+
if (captionUrl) {
|
|
221
|
+
const transcriptXml = await fetchText(captionUrl, context.timeoutMs, context.signal);
|
|
222
|
+
const transcript = transcriptXml ? parseTranscript(transcriptXml) : null;
|
|
223
|
+
if (transcript) {
|
|
224
|
+
hasTranscript = true;
|
|
225
|
+
sections.push("## Transcript", "", transcript);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (sections.length === 0)
|
|
229
|
+
return null;
|
|
230
|
+
return {
|
|
231
|
+
url: watchUrl,
|
|
232
|
+
title: title || `YouTube ${videoId}`,
|
|
233
|
+
markdown: sections.join("\n"),
|
|
234
|
+
preferredName: `youtube/${videoId}`,
|
|
235
|
+
tags: hasTranscript ? ["youtube", "video", "transcript"] : ["youtube", "video"],
|
|
236
|
+
};
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
export default youtubeFetcher;
|
package/dist/storage/database.js
CHANGED
|
@@ -35,18 +35,39 @@ const isBun = !!process.versions?.bun;
|
|
|
35
35
|
// Bun built-in, unresolvable on Node) nor `better-sqlite3` (an optional native
|
|
36
36
|
// dep, possibly absent under Bun) is statically imported.
|
|
37
37
|
const nodeRequire = createRequire(import.meta.url);
|
|
38
|
+
// bun:sqlite — Bun built-in, no native build. Cannot run on Node.
|
|
39
|
+
const bunSqliteProvider = {
|
|
40
|
+
name: "bun:sqlite",
|
|
41
|
+
supported: () => isBun,
|
|
42
|
+
open: openBunDatabase,
|
|
43
|
+
};
|
|
44
|
+
// better-sqlite3 — native Node driver. Cannot run on Bun (oven-sh/bun#4290).
|
|
45
|
+
const nodeSqliteProvider = {
|
|
46
|
+
name: "better-sqlite3",
|
|
47
|
+
supported: () => !isBun,
|
|
48
|
+
open: openNodeDatabase,
|
|
49
|
+
};
|
|
38
50
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* Bun, so importing this module under Bun never touches `better-sqlite3`.
|
|
51
|
+
* Ordered provider registry. The factory selects the first supported provider.
|
|
52
|
+
* A future Postgres provider is appended here — and only here. Both SQLite
|
|
53
|
+
* engines are kept as distinct providers (not collapsed) because no single
|
|
54
|
+
* SQLite driver runs on both Bun and Node today.
|
|
44
55
|
*/
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
const PROVIDERS = [bunSqliteProvider, nodeSqliteProvider];
|
|
57
|
+
/** Select the provider for the current runtime. */
|
|
58
|
+
function selectProvider() {
|
|
59
|
+
const provider = PROVIDERS.find((p) => p.supported());
|
|
60
|
+
if (!provider) {
|
|
61
|
+
throw new Error(`No storage provider supports the current runtime (${isBun ? "Bun" : "Node"}).`);
|
|
48
62
|
}
|
|
49
|
-
return
|
|
63
|
+
return provider;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Open a SQLite database handle at `path` via the active {@link StorageProvider}.
|
|
67
|
+
* Returns a handle conforming to the structural {@link Database} type.
|
|
68
|
+
*/
|
|
69
|
+
export function openDatabase(path, opts) {
|
|
70
|
+
return selectProvider().open(path, opts);
|
|
50
71
|
}
|
|
51
72
|
function openBunDatabase(path, opts) {
|
|
52
73
|
const { Database: BunDatabase } = loadBunSqlite();
|
|
@@ -81,7 +102,21 @@ function loadBetterSqlite3() {
|
|
|
81
102
|
if (!betterSqlite3Ctor) {
|
|
82
103
|
// Runtime-gated dynamic require: only reached when NOT on Bun, so Bun never
|
|
83
104
|
// resolves or loads the optional `better-sqlite3` native dependency.
|
|
84
|
-
|
|
105
|
+
// `better-sqlite3` is an optionalDependency, so `npm i` can succeed without
|
|
106
|
+
// it (or with a native build that failed). Convert the raw MODULE_NOT_FOUND
|
|
107
|
+
// into an actionable message instead of a cryptic onboarding crash.
|
|
108
|
+
let mod;
|
|
109
|
+
try {
|
|
110
|
+
mod = nodeRequire("better-sqlite3");
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
throw new Error("akm could not load 'better-sqlite3', the SQLite driver it needs on Node.js.\n" +
|
|
114
|
+
" • Reinstall akm with a working C/C++ build toolchain so its optional\n" +
|
|
115
|
+
" 'better-sqlite3' native binding rebuilds (a global `npm i -g better-sqlite3`\n" +
|
|
116
|
+
" will NOT be resolved — Node loads it from akm's own node_modules).\n" +
|
|
117
|
+
" • Or run akm under Bun, which has a built-in SQLite driver and needs no native build.\n" +
|
|
118
|
+
` Underlying load error: ${err instanceof Error ? err.message : String(err)}`);
|
|
119
|
+
}
|
|
85
120
|
betterSqlite3Ctor = mod.default ?? mod;
|
|
86
121
|
}
|
|
87
122
|
return betterSqlite3Ctor;
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
* Managed-database seam — the single home for the SQLite open/lifecycle recipe.
|
|
6
|
+
*
|
|
7
|
+
* Before this module, two idioms were copy-pasted across state.db / logs.db /
|
|
8
|
+
* workflow.db / index.db and their consumers:
|
|
9
|
+
*
|
|
10
|
+
* 1. The open recipe: `mkdir(dir) → openDatabase(path) → applyStandardPragmas
|
|
11
|
+
* → migrate`.
|
|
12
|
+
* 2. The borrow-or-own lifecycle: `const db = ctx?.db ?? open(); const owns =
|
|
13
|
+
* !ctx?.db; try { … } finally { if (owns) db.close(); }`.
|
|
14
|
+
*
|
|
15
|
+
* {@link openManagedDatabase} owns (1); {@link withManagedDb} owns (2). Each DB
|
|
16
|
+
* module supplies only a path + initializer and gets a `withXDb` loan helper
|
|
17
|
+
* (see `withIndexDb`, `withStateDb`, etc.) so callers never hand-roll the
|
|
18
|
+
* ownership flag or the finally/close again. This is also the one place to add
|
|
19
|
+
* busy-timeout tuning, integrity checks, or test-isolation injection.
|
|
20
|
+
*/
|
|
21
|
+
import fs from "node:fs";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import { openDatabase } from "./database.js";
|
|
24
|
+
import { applyStandardPragmas } from "./sqlite-pragmas.js";
|
|
25
|
+
/**
|
|
26
|
+
* Open a managed SQLite database: ensure the parent dir exists, open the handle,
|
|
27
|
+
* apply standard pragmas, then run the schema initializer. The single home for
|
|
28
|
+
* the open→pragmas→migrate recipe.
|
|
29
|
+
*/
|
|
30
|
+
export function openManagedDatabase(spec) {
|
|
31
|
+
const dir = path.dirname(spec.path);
|
|
32
|
+
if (!fs.existsSync(dir)) {
|
|
33
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
const db = openDatabase(spec.path);
|
|
36
|
+
applyStandardPragmas(db, spec.pragmas ?? { dataDir: dir });
|
|
37
|
+
spec.init?.(db);
|
|
38
|
+
return db;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Run `fn` against a managed database, owning its lifecycle.
|
|
42
|
+
*
|
|
43
|
+
* When `opts.borrowed` is supplied the caller already owns an open handle: it is
|
|
44
|
+
* passed straight through and NOT closed (borrow). Otherwise a fresh handle is
|
|
45
|
+
* opened via `open` and closed in a `finally` (own). This replaces the
|
|
46
|
+
* hand-rolled `ctx?.db ?? open()` + `ownsDb` flag + `finally`/close idiom — the
|
|
47
|
+
* ownership decision and the close live here, once.
|
|
48
|
+
*
|
|
49
|
+
* Synchronous by design: the DB consumers (telemetry writers, planners) finish
|
|
50
|
+
* all work within the tick, matching the inline blocks this replaces.
|
|
51
|
+
*/
|
|
52
|
+
export function withManagedDb(open, fn, opts) {
|
|
53
|
+
if (opts?.borrowed) {
|
|
54
|
+
return fn(opts.borrowed);
|
|
55
|
+
}
|
|
56
|
+
const db = open();
|
|
57
|
+
try {
|
|
58
|
+
return fn(db);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
db.close();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Async sibling of {@link withManagedDb}. Use this — NOT `withManagedDb` — when
|
|
66
|
+
* `fn` holds the handle across an `await`: the sync version closes in its
|
|
67
|
+
* `finally` before the awaited work resolves (use-after-close). Here the handle
|
|
68
|
+
* is closed only after `fn`'s promise settles. Borrowed handles pass straight
|
|
69
|
+
* through and are not closed, as in the sync version.
|
|
70
|
+
*/
|
|
71
|
+
export async function withManagedDbAsync(open, fn, opts) {
|
|
72
|
+
if (opts?.borrowed) {
|
|
73
|
+
return fn(opts.borrowed);
|
|
74
|
+
}
|
|
75
|
+
const db = open();
|
|
76
|
+
try {
|
|
77
|
+
return await fn(db);
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
db.close();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
/** Insert a freshly minted canary set (all rows active, one shared set id). */
|
|
5
|
+
export function insertCanaries(db, canarySetId, canaries, now) {
|
|
6
|
+
if (canaries.length === 0)
|
|
7
|
+
return;
|
|
8
|
+
const ts = now ?? new Date().toISOString();
|
|
9
|
+
const stmt = db.prepare(`
|
|
10
|
+
INSERT INTO canary_queries (canary_set_id, anchor_ref, query, source, active, created_at)
|
|
11
|
+
VALUES (?, ?, ?, ?, 1, ?)
|
|
12
|
+
`);
|
|
13
|
+
db.transaction(() => {
|
|
14
|
+
for (const c of canaries) {
|
|
15
|
+
stmt.run(canarySetId, c.anchorRef, c.query, c.source ?? "auto", ts);
|
|
16
|
+
}
|
|
17
|
+
})();
|
|
18
|
+
}
|
|
19
|
+
/** Load the active canary set (empty array = never minted). */
|
|
20
|
+
export function getActiveCanaries(db) {
|
|
21
|
+
// Scope to the NEWEST active set: if an interrupted refresh (or a bug) ever
|
|
22
|
+
// leaves two sets active, mixing their rows would silently corrupt the
|
|
23
|
+
// recall/entropy trend baselines. The newest set wins; stale-active rows are
|
|
24
|
+
// simply never returned.
|
|
25
|
+
return db
|
|
26
|
+
.prepare(`SELECT * FROM canary_queries
|
|
27
|
+
WHERE active = 1 AND canary_set_id = (
|
|
28
|
+
SELECT canary_set_id FROM canary_queries WHERE active = 1
|
|
29
|
+
ORDER BY created_at DESC, id DESC LIMIT 1
|
|
30
|
+
)
|
|
31
|
+
ORDER BY id`)
|
|
32
|
+
.all();
|
|
33
|
+
}
|
|
34
|
+
/** Load one canary set's rows by its exact set id (any active state), insertion order. */
|
|
35
|
+
export function getCanariesBySetId(db, canarySetId) {
|
|
36
|
+
return db
|
|
37
|
+
.prepare(`SELECT * FROM canary_queries WHERE canary_set_id = ? ORDER BY id`)
|
|
38
|
+
.all(canarySetId);
|
|
39
|
+
}
|
|
40
|
+
/** List every distinct canary_set_id that still has active rows. */
|
|
41
|
+
export function listActiveCanarySetIds(db) {
|
|
42
|
+
const rows = db.prepare(`SELECT DISTINCT canary_set_id FROM canary_queries WHERE active = 1`).all();
|
|
43
|
+
return rows.map((r) => r.canary_set_id);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Deactivate every canary row in a set. Rows are RETAINED (active = 0) so
|
|
47
|
+
* historical improve_cycle_metrics rows keyed on the old canary_set_id stay
|
|
48
|
+
* interpretable; only `akm improve canary --refresh` calls this.
|
|
49
|
+
*/
|
|
50
|
+
export function deactivateCanarySet(db, canarySetId) {
|
|
51
|
+
const result = db
|
|
52
|
+
.prepare(`UPDATE canary_queries SET active = 0 WHERE canary_set_id = ? AND active = 1`)
|
|
53
|
+
.run(canarySetId);
|
|
54
|
+
const changes = result.changes ?? 0;
|
|
55
|
+
return typeof changes === "bigint" ? Number(changes) : changes;
|
|
56
|
+
}
|
|
57
|
+
/** Persist one qualifying cycle's store-health snapshot. */
|
|
58
|
+
export function insertCycleMetrics(db, row) {
|
|
59
|
+
db.prepare(`
|
|
60
|
+
INSERT INTO improve_cycle_metrics
|
|
61
|
+
(run_id, ts, pass, canary_set_id, mean_recall, mean_ndcg, mean_mrr,
|
|
62
|
+
canary_ranks_json, store_total, store_by_type_json, distinct_content_ratio,
|
|
63
|
+
mean_bigram_diversity, over_generation_count, accepted_actions,
|
|
64
|
+
merge_floor_violations, alerts_json)
|
|
65
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
66
|
+
`).run(row.run_id, row.ts, row.pass, row.canary_set_id, row.mean_recall, row.mean_ndcg, row.mean_mrr, row.canary_ranks_json, row.store_total, row.store_by_type_json, row.distinct_content_ratio, row.mean_bigram_diversity, row.over_generation_count, row.accepted_actions, row.merge_floor_violations, row.alerts_json);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Load the most recent cycle rows for one canary set, OLDEST-first (the alert
|
|
70
|
+
* evaluator's window order). Scoped by canary_set_id so trends never compare
|
|
71
|
+
* across canary re-mints.
|
|
72
|
+
*/
|
|
73
|
+
export function queryRecentCycleMetrics(db, canarySetId, limit) {
|
|
74
|
+
const rows = db
|
|
75
|
+
.prepare(`SELECT run_id, ts, pass, canary_set_id, mean_recall, mean_ndcg, mean_mrr,
|
|
76
|
+
canary_ranks_json, store_total, store_by_type_json, distinct_content_ratio,
|
|
77
|
+
mean_bigram_diversity, over_generation_count, accepted_actions,
|
|
78
|
+
merge_floor_violations, alerts_json
|
|
79
|
+
FROM improve_cycle_metrics WHERE canary_set_id = ?
|
|
80
|
+
ORDER BY ts DESC, id DESC LIMIT ?`)
|
|
81
|
+
.all(canarySetId, Math.max(0, limit));
|
|
82
|
+
return rows.reverse();
|
|
83
|
+
}
|
|
84
|
+
/** Load the single most recent cycle row across all canary sets (health surface). */
|
|
85
|
+
export function getLatestCycleMetrics(db) {
|
|
86
|
+
const row = db
|
|
87
|
+
.prepare(`SELECT run_id, ts, pass, canary_set_id, mean_recall, mean_ndcg, mean_mrr,
|
|
88
|
+
canary_ranks_json, store_total, store_by_type_json, distinct_content_ratio,
|
|
89
|
+
mean_bigram_diversity, over_generation_count, accepted_actions,
|
|
90
|
+
merge_floor_violations, alerts_json
|
|
91
|
+
FROM improve_cycle_metrics ORDER BY ts DESC, id DESC LIMIT 1`)
|
|
92
|
+
.get();
|
|
93
|
+
return row == null ? undefined : row;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Delete cycle rows older than `retentionDays` (default 365 — owner-approved;
|
|
97
|
+
* a slow collapse needs a longer trend window than the 90-day events log).
|
|
98
|
+
* Returns the purged row count. canary_queries rows are never purged.
|
|
99
|
+
*/
|
|
100
|
+
export function purgeOldCycleMetrics(db, retentionDays = 365) {
|
|
101
|
+
if (!Number.isFinite(retentionDays) || retentionDays <= 0)
|
|
102
|
+
return 0;
|
|
103
|
+
const cutoff = new Date(Date.now() - retentionDays * 86_400_000).toISOString();
|
|
104
|
+
const result = db.prepare("DELETE FROM improve_cycle_metrics WHERE ts < ?").run(cutoff);
|
|
105
|
+
const changes = result.changes ?? 0;
|
|
106
|
+
return typeof changes === "bigint" ? Number(changes) : changes;
|
|
107
|
+
}
|
|
@@ -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
|
+
* Bulk-fetch the judged-state cache for a set of entry keys in one query.
|
|
6
|
+
* Returns a Map keyed by entry_key so the consolidate pool-selection loop can
|
|
7
|
+
* do O(1) "has this memory been judged at this content hash?" lookups.
|
|
8
|
+
* Empty input → empty map (no query issued).
|
|
9
|
+
*/
|
|
10
|
+
export function getConsolidationJudgedMap(db, entryKeys) {
|
|
11
|
+
const out = new Map();
|
|
12
|
+
if (entryKeys.length === 0)
|
|
13
|
+
return out;
|
|
14
|
+
// SQLite has a ~999 param ceiling; chunk if a caller ever exceeds that.
|
|
15
|
+
const CHUNK = 500;
|
|
16
|
+
for (let i = 0; i < entryKeys.length; i += CHUNK) {
|
|
17
|
+
const chunk = entryKeys.slice(i, i + CHUNK);
|
|
18
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
19
|
+
const rows = db
|
|
20
|
+
.prepare(`SELECT * FROM consolidation_judged WHERE entry_key IN (${placeholders})`)
|
|
21
|
+
.all(...chunk);
|
|
22
|
+
for (const row of rows)
|
|
23
|
+
out.set(row.entry_key, row);
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Record (or update) the judged state for one memory. INSERT-OR-REPLACE so the
|
|
29
|
+
* row always reflects the most recent judge of that entry_key. Called once per
|
|
30
|
+
* memory the consolidate LLM saw in a successfully-judged chunk.
|
|
31
|
+
*/
|
|
32
|
+
export function upsertConsolidationJudged(db, input) {
|
|
33
|
+
db.prepare(`
|
|
34
|
+
INSERT OR REPLACE INTO consolidation_judged
|
|
35
|
+
(entry_key, content_hash, judged_at, outcome)
|
|
36
|
+
VALUES (?, ?, ?, ?)
|
|
37
|
+
`).run(input.entryKey, input.contentHash, input.judgedAt, input.outcome);
|
|
38
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
* Convert a `number[]` embedding vector to the `Float32Array` byte
|
|
6
|
+
* representation stored in the `body_embeddings.embedding` BLOB column.
|
|
7
|
+
*/
|
|
8
|
+
export function embeddingToBlob(vec) {
|
|
9
|
+
const f32 = new Float32Array(vec);
|
|
10
|
+
return new Uint8Array(f32.buffer);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Convert the raw `Uint8Array` bytes from the `body_embeddings.embedding`
|
|
14
|
+
* BLOB column back to a `number[]` embedding vector.
|
|
15
|
+
*/
|
|
16
|
+
export function blobToEmbedding(blob) {
|
|
17
|
+
// SQLite BLOB columns are returned as Uint8Array; re-interpret as Float32.
|
|
18
|
+
const f32 = new Float32Array(blob.buffer, blob.byteOffset, blob.byteLength / 4);
|
|
19
|
+
return Array.from(f32);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Bulk-fetch cached body embeddings for a set of content hashes.
|
|
23
|
+
* Returns a Map keyed by `content_hash` (embedding decoded to `number[]`).
|
|
24
|
+
* Empty input → empty map (no query issued).
|
|
25
|
+
*
|
|
26
|
+
* If the stored `model_id` does not match `expectedModelId` the entire table
|
|
27
|
+
* is cleared (drop-all on model mismatch) and an empty map is returned so
|
|
28
|
+
* callers re-embed everything on this run.
|
|
29
|
+
*/
|
|
30
|
+
export function getBodyEmbeddings(db, contentHashes, expectedModelId) {
|
|
31
|
+
const out = new Map();
|
|
32
|
+
if (contentHashes.length === 0)
|
|
33
|
+
return out;
|
|
34
|
+
// Model-id mismatch: vectors are in the wrong metric space — drop all rows.
|
|
35
|
+
const firstRow = db.prepare("SELECT model_id FROM body_embeddings LIMIT 1").get();
|
|
36
|
+
if (firstRow && firstRow.model_id !== expectedModelId) {
|
|
37
|
+
db.exec("DELETE FROM body_embeddings");
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
// SQLite has a ~999 param ceiling; chunk if needed.
|
|
41
|
+
const CHUNK = 500;
|
|
42
|
+
for (let i = 0; i < contentHashes.length; i += CHUNK) {
|
|
43
|
+
const chunk = contentHashes.slice(i, i + CHUNK);
|
|
44
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
45
|
+
const rows = db
|
|
46
|
+
.prepare(`SELECT content_hash, embedding FROM body_embeddings WHERE content_hash IN (${placeholders})`)
|
|
47
|
+
.all(...chunk);
|
|
48
|
+
for (const row of rows) {
|
|
49
|
+
out.set(row.content_hash, blobToEmbedding(row.embedding));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Upsert body-embedding rows in a single transaction.
|
|
56
|
+
* Each entry maps a `cacheHash` → `number[]` vector. `model_id` is stored
|
|
57
|
+
* so a future model change can trigger a drop-all purge.
|
|
58
|
+
*/
|
|
59
|
+
export function upsertBodyEmbeddings(db, entries) {
|
|
60
|
+
if (entries.length === 0)
|
|
61
|
+
return;
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
const stmt = db.prepare(`
|
|
64
|
+
INSERT OR REPLACE INTO body_embeddings (content_hash, embedding, model_id, created_at)
|
|
65
|
+
VALUES (?, ?, ?, ?)
|
|
66
|
+
`);
|
|
67
|
+
db.transaction(() => {
|
|
68
|
+
for (const { contentHash, embedding, modelId } of entries) {
|
|
69
|
+
stmt.run(contentHash, embeddingToBlob(embedding), modelId, now);
|
|
70
|
+
}
|
|
71
|
+
})();
|
|
72
|
+
}
|