akm-cli 0.9.7 → 0.9.8-beta.1
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 +218 -0
- package/dist/commands/env/env.js +2 -21
- package/dist/commands/health/checks.js +0 -25
- package/dist/commands/health/improve-metrics.js +8 -34
- package/dist/commands/health/windows.js +0 -4
- package/dist/commands/health.js +1 -35
- package/dist/commands/improve/consolidate/eligibility.js +11 -5
- package/dist/commands/improve/extract.js +36 -32
- package/dist/commands/improve/memory/memory-belief.js +15 -5
- package/dist/commands/improve/memory/memory-contradiction-detect.js +16 -25
- package/dist/commands/improve/memory/memory-improve.js +7 -19
- package/dist/commands/improve/preparation.js +52 -36
- package/dist/commands/improve/reflect.js +13 -17
- package/dist/commands/lint/base-linter.js +42 -2
- package/dist/commands/lint/index.js +4 -5
- package/dist/commands/migrate/config-extra-params.js +61 -0
- package/dist/commands/migrate/dead-residue.js +113 -0
- package/dist/commands/migrate/stale-txn.js +49 -0
- package/dist/commands/migrate-cli.js +42 -1
- package/dist/commands/proposal/proposal.js +1 -21
- package/dist/commands/proposal/repository.js +0 -4
- package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -6
- package/dist/commands/read/curate.js +4 -18
- package/dist/commands/read/show.js +2 -1
- package/dist/commands/sources/installed-stashes.js +5 -1
- package/dist/commands/sources/self-update.js +38 -1
- package/dist/commands/sources/sources-cli.js +17 -1
- package/dist/commands/tasks/tasks.js +0 -8
- package/dist/commands/url-checker.js +20 -28
- package/dist/core/adapter/adapters/agent-skills-adapter.js +2 -6
- package/dist/core/adapter/adapters/akm-adapter.js +2 -3
- package/dist/core/adapter/adapters/akm-lint.js +2 -20
- package/dist/core/adapter/adapters/akm-task-adapter.js +2 -6
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +2 -6
- package/dist/core/adapter/adapters/dotenv-adapter.js +2 -22
- package/dist/core/adapter/adapters/generic-files-adapter.js +3 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +2 -6
- package/dist/core/adapter/adapters/okf-adapter.js +7 -18
- package/dist/core/adapter/adapters/shared.js +2 -0
- package/dist/core/adapter/adapters/tool-dir-shared.js +3 -9
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +2 -6
- package/dist/core/adapter/validate-context.js +1 -3
- package/dist/core/asset/asset-placement.js +1 -3
- package/dist/core/asset/frontmatter.js +96 -0
- package/dist/core/common.js +66 -2
- package/dist/core/config/config-walker.js +6 -10
- package/dist/core/config/config.js +11 -8
- package/dist/core/extra-params.js +17 -13
- package/dist/core/improve-result.js +1 -3
- package/dist/core/json-schema.js +9 -11
- package/dist/core/state/migrations.js +52 -2
- package/dist/core/state-db.js +2 -1
- package/dist/execution/executable-identity.js +1 -3
- package/dist/execution/guarded-source.js +1 -6
- package/dist/indexer/bundle-identity-guard.js +6 -1
- package/dist/indexer/db/graph-db.js +139 -154
- package/dist/indexer/ensure-index.js +11 -19
- package/dist/indexer/graph/graph-boost.js +23 -34
- package/dist/indexer/graph/graph-extraction.js +12 -2
- package/dist/indexer/indexer.js +1 -1
- package/dist/indexer/lookup/adapter-concept-owner.js +12 -9
- package/dist/indexer/passes/memory-inference.js +7 -2
- package/dist/indexer/scan/drain-dir.js +2 -4
- package/dist/indexer/search/db-search.js +3 -3
- package/dist/indexer/search/fts-query.js +10 -15
- package/dist/indexer/search/search-source.js +0 -13
- package/dist/indexer/usage/usage-events.js +9 -1
- package/dist/indexer/walk/walker.js +11 -6
- package/dist/integrations/harnesses/claude/result-extractor.js +30 -15
- package/dist/integrations/harnesses/codex/result-extractor.js +43 -6
- package/dist/integrations/harnesses/copilot/result-extractor.js +39 -12
- package/dist/integrations/harnesses/gemini/result-extractor.js +40 -12
- package/dist/integrations/harnesses/index.js +0 -4
- package/dist/integrations/harnesses/openhands/result-extractor.js +51 -19
- package/dist/integrations/harnesses/pi/result-extractor.js +45 -12
- package/dist/integrations/lockfile.js +0 -14
- package/dist/integrations/session-logs/index.js +0 -81
- package/dist/llm/client.js +0 -12
- package/dist/llm/memory-infer.js +1 -3
- package/dist/llm/usage-telemetry.js +1 -3
- package/dist/registry/create-provider-registry.js +4 -0
- package/dist/registry/factory.js +4 -0
- package/dist/registry/resolve.js +23 -8
- package/dist/runtime.js +0 -12
- package/dist/scripts/akm-migrate-node.js +142 -244
- package/dist/scripts/akm-migrate.js +142 -244
- package/dist/setup/setup.js +6 -12
- package/dist/sources/providers/git-install.js +7 -2
- package/dist/sources/providers/tar-utils.js +1 -7
- package/dist/sources/snapshot-fetchers/content-extract.js +0 -17
- package/dist/storage/managed-db.js +20 -7
- package/dist/storage/repositories/events-repository.js +0 -81
- package/dist/storage/repositories/index-entries-repository.js +0 -17
- package/dist/storage/repositories/index-fts-repository.js +24 -30
- package/dist/storage/repositories/index-utility-repository.js +0 -57
- package/dist/storage/repositories/index-vec-repository.js +25 -27
- package/dist/storage/repositories/task-history-repository.js +9 -3
- package/dist/tasks/backends/launchd.js +8 -18
- package/dist/tasks/run/task-history.js +21 -31
- package/dist/tasks/scheduler-sync.js +1 -6
- package/dist/tasks/source/bounded-document.js +1 -14
- package/dist/workflows/exec/child-workflow.js +1 -1
- package/dist/workflows/exec/native-executor.js +2 -2
- package/dist/workflows/exec/step-work.js +5 -17
- package/dist/workflows/exec/worktree.js +40 -6
- package/dist/workflows/freeze/task-bindings.js +2 -4
- package/dist/workflows/ir/compile.js +3 -14
- package/dist/workflows/ir/schema-v4.js +4 -6
- package/dist/workflows/ir/schema.js +2 -5
- package/dist/workflows/parser.js +23 -26
- package/dist/workflows/source-files.js +8 -13
- package/dist/workflows/source-ir/schema.js +1 -19
- package/docs/migration/v0.9.0-troubleshooting.md +12 -2
- package/docs/reference/cli.md +2 -0
- package/docs/reference/workflow-schema.md +24 -0
- package/package.json +2 -5
- package/dist/workflows/source-ir/compare.js +0 -17
package/dist/setup/setup.js
CHANGED
|
@@ -24,7 +24,7 @@ import { deriveBundleIds } from "../core/bundle-id.js";
|
|
|
24
24
|
import { bundleEntryToSourceEntry, DEFAULT_CONFIG, loadUserConfig, mutateConfigWithPrecommit, parseAndValidateConfigText, primaryBundlePath, validateCompleteConfig, } from "../core/config/config.js";
|
|
25
25
|
import { readConfigText } from "../core/config/config-io.js";
|
|
26
26
|
import { listTopLevelConfigKeys } from "../core/config/config-schema.js";
|
|
27
|
-
import { deepMergeConfig } from "../core/config/deep-merge.js";
|
|
27
|
+
import { deepMergeConfig, isPlainObject } from "../core/config/deep-merge.js";
|
|
28
28
|
import { ConfigError, UsageError } from "../core/errors.js";
|
|
29
29
|
import { getConfigPath, getDefaultStashDir, isTransientStashPath } from "../core/paths.js";
|
|
30
30
|
import { warn } from "../core/warn.js";
|
|
@@ -131,17 +131,11 @@ export function assertSetupConfigPreflight() {
|
|
|
131
131
|
`(e.g. \`mv ${configPath} ${configPath}.bak\`) and re-run \`akm setup\`.`);
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
-
function isPlainRecord(value) {
|
|
135
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
136
|
-
return false;
|
|
137
|
-
const prototype = Object.getPrototypeOf(value);
|
|
138
|
-
return prototype === Object.prototype || prototype === null;
|
|
139
|
-
}
|
|
140
134
|
function sameConfigValue(left, right) {
|
|
141
135
|
return isDeepStrictEqual(left, right);
|
|
142
136
|
}
|
|
143
137
|
function configArrayItemKey(value) {
|
|
144
|
-
if (!
|
|
138
|
+
if (!isPlainObject(value))
|
|
145
139
|
return `value:${JSON.stringify(value)}`;
|
|
146
140
|
if (typeof value.id === "string")
|
|
147
141
|
return `id:${value.id}`;
|
|
@@ -199,14 +193,14 @@ export function rebaseSetupChanges(original, desired, latest, pathParts = []) {
|
|
|
199
193
|
}
|
|
200
194
|
return result;
|
|
201
195
|
}
|
|
202
|
-
if (!
|
|
196
|
+
if (!isPlainObject(original) || !isPlainObject(desired)) {
|
|
203
197
|
if (!sameConfigValue(latest, original) && !sameConfigValue(latest, desired))
|
|
204
198
|
setupConflict(pathParts);
|
|
205
199
|
return desired;
|
|
206
200
|
}
|
|
207
|
-
if (latest !== undefined && !
|
|
201
|
+
if (latest !== undefined && !isPlainObject(latest))
|
|
208
202
|
setupConflict(pathParts);
|
|
209
|
-
const result =
|
|
203
|
+
const result = isPlainObject(latest) ? { ...latest } : {};
|
|
210
204
|
for (const key of new Set([...Object.keys(original), ...Object.keys(desired)])) {
|
|
211
205
|
if (!Object.hasOwn(desired, key)) {
|
|
212
206
|
if (Object.hasOwn(result, key) && !sameConfigValue(result[key], original[key]))
|
|
@@ -855,7 +849,7 @@ export async function runSetupFromConfig(opts) {
|
|
|
855
849
|
catch (e) {
|
|
856
850
|
throw new UsageError(`Invalid JSON in --config: ${e.message}`, "INVALID_FLAG_VALUE");
|
|
857
851
|
}
|
|
858
|
-
if (!
|
|
852
|
+
if (!isPlainObject(parsed)) {
|
|
859
853
|
throw new ConfigError("Setup config must contain a top-level object.", "INVALID_CONFIG_FILE");
|
|
860
854
|
}
|
|
861
855
|
const incoming = parsed;
|
|
@@ -48,8 +48,13 @@ export function inspectGitUpstream(repoDir) {
|
|
|
48
48
|
* commit SHA peels to itself.
|
|
49
49
|
*/
|
|
50
50
|
export function verifyClonedRevision(cloneDir, url, expectedRevision) {
|
|
51
|
-
if (!expectedRevision)
|
|
52
|
-
|
|
51
|
+
if (!expectedRevision) {
|
|
52
|
+
// `resolveGitArtifact` (registry/resolve.ts) throws when it cannot resolve
|
|
53
|
+
// a revision via `git ls-remote`, so a plain git install never reaches
|
|
54
|
+
// this call with `undefined` — treat it as a bug, not a silently-skipped
|
|
55
|
+
// check that would defeat the R-011 post-clone integrity verification.
|
|
56
|
+
throw new UsageError(`No revision was resolved from ${url}; refusing to install without a verifiable checkout.`);
|
|
57
|
+
}
|
|
53
58
|
const head = runGit(["-C", cloneDir, "rev-parse", "HEAD"]);
|
|
54
59
|
if (head.status !== 0 || !head.stdout.trim()) {
|
|
55
60
|
throw new UsageError(`Failed to read cloned HEAD at ${cloneDir}: ${head.stderr.trim() || "rev-parse failed"}`);
|
|
@@ -91,13 +91,7 @@ export function extractTarGzSecure(archivePath, destinationDir) {
|
|
|
91
91
|
scanExtractedFiles(destinationDir, destinationDir);
|
|
92
92
|
}
|
|
93
93
|
function scanExtractedFiles(dir, root) {
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
94
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
101
95
|
for (const entry of entries) {
|
|
102
96
|
const fullPath = path.join(dir, entry.name);
|
|
103
97
|
// Reject only entries whose name is exactly the parent-traversal segment
|
|
@@ -609,20 +609,3 @@ export function htmlToMarkdownAndLinks(html, pageUrl) {
|
|
|
609
609
|
const markdown = root ? markdownFromRoot(root, html, pageUrl) : plainTextFallback(html);
|
|
610
610
|
return { markdown: finalizeMarkdown(markdown), links };
|
|
611
611
|
}
|
|
612
|
-
/**
|
|
613
|
-
* Collect links from the WHOLE document, not the extracted content region.
|
|
614
|
-
*
|
|
615
|
-
* Deliberate and load-bearing: nav/header/footer links are how a crawl
|
|
616
|
-
* discovers pages. Narrowing this to the content region would silently shrink
|
|
617
|
-
* every crawl to whatever the first page happens to link inline.
|
|
618
|
-
*/
|
|
619
|
-
export function extractDocumentLinks(html, pageUrl) {
|
|
620
|
-
let root;
|
|
621
|
-
try {
|
|
622
|
-
root = parse(scrubDangerousMarkup(html), { comment: false });
|
|
623
|
-
}
|
|
624
|
-
catch {
|
|
625
|
-
return [];
|
|
626
|
-
}
|
|
627
|
-
return collectLinksFromRoot(root, pageUrl);
|
|
628
|
-
}
|
|
@@ -61,13 +61,19 @@ export function openManagedDatabase(spec) {
|
|
|
61
61
|
// Initializers may open a transaction (source update does so before index
|
|
62
62
|
// schema work). Never strand that transaction/handle when later setup
|
|
63
63
|
// fails; closing rolls it back and releases its writer lock.
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
//
|
|
65
|
+
// `db.inTransaction` is INSIDE the try: reading it on an already-closed
|
|
66
|
+
// handle throws "Database has closed", and it used to sit outside, so that
|
|
67
|
+
// throw escaped and replaced `error` — masking the real initializer failure
|
|
68
|
+
// this whole block exists to preserve. Every other step here was already
|
|
69
|
+
// guarded for exactly that reason; this one was not, and a concurrent
|
|
70
|
+
// close (WAL contention between two writers) reached it.
|
|
71
|
+
try {
|
|
72
|
+
if (db.inTransaction)
|
|
66
73
|
db.exec("ROLLBACK");
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Closing remains the final rollback backstop.
|
|
71
77
|
}
|
|
72
78
|
try {
|
|
73
79
|
db.close();
|
|
@@ -99,6 +105,13 @@ export function withManagedDb(open, fn, opts) {
|
|
|
99
105
|
return fn(db);
|
|
100
106
|
}
|
|
101
107
|
finally {
|
|
102
|
-
|
|
108
|
+
try {
|
|
109
|
+
db.close();
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// A close() throw here would REPLACE fn's in-flight exception — the same
|
|
113
|
+
// masking openManagedDatabase above was hardened against. The handle is
|
|
114
|
+
// being discarded either way; fn's error is the one that matters.
|
|
115
|
+
}
|
|
103
116
|
}
|
|
104
117
|
}
|
|
@@ -147,84 +147,3 @@ export function purgeOldEvents(db, retentionDays = 90) {
|
|
|
147
147
|
const changes = result.changes ?? 0;
|
|
148
148
|
return typeof changes === "bigint" ? Number(changes) : changes;
|
|
149
149
|
}
|
|
150
|
-
/**
|
|
151
|
-
* Import all events from an `events.jsonl` file into the `events` table.
|
|
152
|
-
*
|
|
153
|
-
* The old byte-offset `id` is NOT preserved — the database assigns new
|
|
154
|
-
* monotonic integer ids. Callers that persisted a byte-offset cursor must
|
|
155
|
-
* discard it after migration and use the returned `maxId` as the new cursor.
|
|
156
|
-
*
|
|
157
|
-
* **Idempotency**: each line is pre-checked against the `events` table using
|
|
158
|
-
* `(event_type, ts, ref, metadata_json)` as the duplicate key. Lines whose
|
|
159
|
-
* exact tuple is already present are skipped and reported as `skipped` in the
|
|
160
|
-
* return value. This makes the migration safe to re-run (the v0.7→v0.8
|
|
161
|
-
* migration guide recommends re-running the script as a recovery path; without
|
|
162
|
-
* this guard, every re-run would double-import the entire event log).
|
|
163
|
-
*
|
|
164
|
-
* Duplicate detection is per-import-tuple, not a table-wide UNIQUE constraint:
|
|
165
|
-
* the events table has no UNIQUE constraint at runtime so that
|
|
166
|
-
* `appendEvent` can write multiple events with the same ts (sub-millisecond
|
|
167
|
-
* bursts produce identical `(event_type, ts, ref)` triples in practice). The
|
|
168
|
-
* SELECT-first check is scoped to the import path only.
|
|
169
|
-
*
|
|
170
|
-
* The import is wrapped in a single transaction for atomicity.
|
|
171
|
-
*
|
|
172
|
-
* @param db - Open state.db connection.
|
|
173
|
-
* @param jsonlPath - Absolute path to the events.jsonl file to import.
|
|
174
|
-
* @returns Number of rows inserted, the max id assigned, and the
|
|
175
|
-
* count of rows skipped because an identical event already
|
|
176
|
-
* existed in the table.
|
|
177
|
-
*/
|
|
178
|
-
export async function importEventsJsonl(db, jsonlPath) {
|
|
179
|
-
const { readFileSync, existsSync } = await import("node:fs");
|
|
180
|
-
if (!existsSync(jsonlPath)) {
|
|
181
|
-
return { imported: 0, maxId: 0, skipped: 0 };
|
|
182
|
-
}
|
|
183
|
-
const text = readFileSync(jsonlPath, "utf8");
|
|
184
|
-
const lines = text.split("\n").filter((l) => l.trim().length > 0);
|
|
185
|
-
let imported = 0;
|
|
186
|
-
let maxId = 0;
|
|
187
|
-
let skipped = 0;
|
|
188
|
-
const insertStmt = db.prepare(`INSERT INTO events (event_type, ts, ref, metadata_json)
|
|
189
|
-
VALUES (?, ?, ?, ?)
|
|
190
|
-
RETURNING id`);
|
|
191
|
-
// Dedup pre-check: matches by the full tuple including metadata_json so an
|
|
192
|
-
// import is idempotent over identical rows but does not collide with two
|
|
193
|
-
// genuinely different events that happen to share (event_type, ts, ref).
|
|
194
|
-
//
|
|
195
|
-
// Uses IS for ref so two NULL refs compare equal (a plain `=` would treat
|
|
196
|
-
// NULL = NULL as NULL and the row would be re-inserted on every run).
|
|
197
|
-
const existsStmt = db.prepare(`SELECT 1 FROM events
|
|
198
|
-
WHERE event_type = ?
|
|
199
|
-
AND ts = ?
|
|
200
|
-
AND ref IS ?
|
|
201
|
-
AND metadata_json = ?
|
|
202
|
-
LIMIT 1`);
|
|
203
|
-
db.transaction(() => {
|
|
204
|
-
for (const line of lines) {
|
|
205
|
-
let parsed;
|
|
206
|
-
try {
|
|
207
|
-
parsed = JSON.parse(line);
|
|
208
|
-
}
|
|
209
|
-
catch {
|
|
210
|
-
continue; // skip malformed lines — same behaviour as readEvents()
|
|
211
|
-
}
|
|
212
|
-
const eventType = typeof parsed.eventType === "string" ? parsed.eventType : "unknown";
|
|
213
|
-
const ts = typeof parsed.ts === "string" ? parsed.ts : new Date().toISOString();
|
|
214
|
-
const ref = typeof parsed.ref === "string" ? parsed.ref : null;
|
|
215
|
-
const metadata = parsed.metadata !== undefined && typeof parsed.metadata === "object" ? JSON.stringify(parsed.metadata) : "{}";
|
|
216
|
-
const duplicate = existsStmt.get(eventType, ts, ref, metadata);
|
|
217
|
-
if (duplicate) {
|
|
218
|
-
skipped++;
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
const result = insertStmt.get(eventType, ts, ref, metadata);
|
|
222
|
-
if (result) {
|
|
223
|
-
imported++;
|
|
224
|
-
if (result.id > maxId)
|
|
225
|
-
maxId = result.id;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
})();
|
|
229
|
-
return { imported, maxId, skipped };
|
|
230
|
-
}
|
|
@@ -683,23 +683,6 @@ export function getEntryFilePathById(db, id) {
|
|
|
683
683
|
const row = db.prepare("SELECT file_path FROM entries WHERE id = ?").get(id);
|
|
684
684
|
return row?.file_path;
|
|
685
685
|
}
|
|
686
|
-
/**
|
|
687
|
-
* Fetch every `(file_path, document_json)` row whose path is under a source
|
|
688
|
-
* root. The path containment filter is applied in JS so SQLite LIKE wildcard
|
|
689
|
-
* characters in filesystem paths cannot widen ownership.
|
|
690
|
-
*
|
|
691
|
-
* Lifted verbatim (WS5) from the inline query in commands/graph.ts'
|
|
692
|
-
* `buildRefByPath`. The full result set is materialised with `.all()` before
|
|
693
|
-
* return so callers can iterate it after the connection closes (WS5
|
|
694
|
-
* connection-lifetime rule). JSON parsing stays with the caller, unchanged.
|
|
695
|
-
*/
|
|
696
|
-
export function getEntryRefRowsForStashRoot(db, stashRoot) {
|
|
697
|
-
const root = path.resolve(stashRoot);
|
|
698
|
-
return db.prepare("SELECT file_path, document_json FROM entries").all().filter((row) => {
|
|
699
|
-
const file = path.resolve(row.file_path);
|
|
700
|
-
return file === root || file.startsWith(`${root}${path.sep}`);
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
686
|
// ── Indexer-phase helpers (moved from indexer.ts) ────────────────────────────
|
|
704
687
|
/**
|
|
705
688
|
* Return distinct zero-result search queries from the `usage_events` table
|
|
@@ -91,38 +91,32 @@ function runFtsQuery(db, ftsQuery, lexicalMatch, limit, entryType, excludeTypes)
|
|
|
91
91
|
ORDER BY bm25Score, e.id ASC
|
|
92
92
|
LIMIT ?
|
|
93
93
|
`;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
entry = JSON.parse(row.documentJson);
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
warn(`[db] searchFts: skipping entry id=${row.id} — corrupt document_json`);
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
results.push({
|
|
108
|
-
id: row.id,
|
|
109
|
-
filePath: row.filePath,
|
|
110
|
-
entry,
|
|
111
|
-
searchText: row.searchText,
|
|
112
|
-
bm25Score: row.bm25Score,
|
|
113
|
-
itemRef: row.itemRef,
|
|
114
|
-
bundleId: row.bundleId,
|
|
115
|
-
conceptId: row.conceptId,
|
|
116
|
-
adapterId: row.adapterId,
|
|
117
|
-
lexicalMatch,
|
|
118
|
-
});
|
|
94
|
+
const rows = db.prepare(sql).all(...params);
|
|
95
|
+
// Guard against corrupt JSON — skip the row rather than crashing
|
|
96
|
+
const results = [];
|
|
97
|
+
for (const row of rows) {
|
|
98
|
+
let entry;
|
|
99
|
+
try {
|
|
100
|
+
entry = JSON.parse(row.documentJson);
|
|
119
101
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
102
|
+
catch {
|
|
103
|
+
warn(`[db] searchFts: skipping entry id=${row.id} — corrupt document_json`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
results.push({
|
|
107
|
+
id: row.id,
|
|
108
|
+
filePath: row.filePath,
|
|
109
|
+
entry,
|
|
110
|
+
searchText: row.searchText,
|
|
111
|
+
bm25Score: row.bm25Score,
|
|
112
|
+
itemRef: row.itemRef,
|
|
113
|
+
bundleId: row.bundleId,
|
|
114
|
+
conceptId: row.conceptId,
|
|
115
|
+
adapterId: row.adapterId,
|
|
116
|
+
lexicalMatch,
|
|
117
|
+
});
|
|
125
118
|
}
|
|
119
|
+
return results;
|
|
126
120
|
}
|
|
127
121
|
/**
|
|
128
122
|
* Explicitly rebuild the complete FTS5 projection from canonical entries.
|
|
@@ -263,63 +263,6 @@ function getSourceScopedRetrievalCounts(indexDb, stateDb, refs, options) {
|
|
|
263
263
|
}
|
|
264
264
|
return result;
|
|
265
265
|
}
|
|
266
|
-
/**
|
|
267
|
-
* Apply a MemRL reward signal to a batch of entries via exponential moving
|
|
268
|
-
* average (EMA): next = clamp(current + lr * (reward - current), 0, 1).
|
|
269
|
-
*
|
|
270
|
-
* Wrapped in a single transaction so all bumps succeed or fail together.
|
|
271
|
-
* The indexer (`akm index`) will overwrite these values at next reindex run;
|
|
272
|
-
* bumps are intentionally temporary hints between index runs, not permanent
|
|
273
|
-
* overrides.
|
|
274
|
-
*
|
|
275
|
-
* When `scopeKey` is provided, also writes a scoped bump to
|
|
276
|
-
* `utility_scores_scoped` so per-project usage signals accumulate alongside
|
|
277
|
-
* the global ones. The global table is always updated regardless.
|
|
278
|
-
*/
|
|
279
|
-
export function bumpUtilityScoresBatch(db, entryIds, reward, lr = 0.1, scopeKey) {
|
|
280
|
-
if (entryIds.length === 0)
|
|
281
|
-
return;
|
|
282
|
-
db.transaction(() => {
|
|
283
|
-
const { global: scoreMap } = getUtilityScoresByIds(db, entryIds);
|
|
284
|
-
const now = new Date().toISOString();
|
|
285
|
-
const nowMs = Date.now();
|
|
286
|
-
const stmt = db.prepare(`INSERT INTO utility_scores (entry_id, utility, show_count, search_count, select_rate, last_used_at, updated_at)
|
|
287
|
-
VALUES (?, ?, 0, 0, 0, ?, ?)
|
|
288
|
-
ON CONFLICT(entry_id) DO UPDATE SET
|
|
289
|
-
utility = excluded.utility,
|
|
290
|
-
updated_at = excluded.updated_at`);
|
|
291
|
-
// Prepare scoped upsert once outside the loop when scopeKey is present.
|
|
292
|
-
const scopedStmt = scopeKey
|
|
293
|
-
? db.prepare(`INSERT INTO utility_scores_scoped (entry_id, scope_key, utility, last_used_at)
|
|
294
|
-
VALUES (?, ?, ?, ?)
|
|
295
|
-
ON CONFLICT(entry_id, scope_key) DO UPDATE SET
|
|
296
|
-
utility = excluded.utility,
|
|
297
|
-
last_used_at = excluded.last_used_at`)
|
|
298
|
-
: null;
|
|
299
|
-
for (const entryId of entryIds) {
|
|
300
|
-
const existing = scoreMap.get(entryId);
|
|
301
|
-
const current = existing?.utility ?? 0;
|
|
302
|
-
const next = Math.max(0, Math.min(1, current + lr * (reward - current)));
|
|
303
|
-
stmt.run(entryId, next, now, now);
|
|
304
|
-
if (scopedStmt && scopeKey) {
|
|
305
|
-
// Retrieve the current scoped utility so we can apply the same EMA.
|
|
306
|
-
const scopedCurrent = getScopedUtility(db, entryId, scopeKey);
|
|
307
|
-
const scopedNext = Math.max(0, Math.min(1, scopedCurrent + lr * (reward - scopedCurrent)));
|
|
308
|
-
scopedStmt.run(entryId, scopeKey, scopedNext, nowMs);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
})();
|
|
312
|
-
}
|
|
313
|
-
/**
|
|
314
|
-
* Return the current utility value for a single (entry_id, scope_key) pair.
|
|
315
|
-
* Returns 0 when no row exists yet.
|
|
316
|
-
*/
|
|
317
|
-
function getScopedUtility(db, entryId, scopeKey) {
|
|
318
|
-
const row = db
|
|
319
|
-
.prepare("SELECT utility FROM utility_scores_scoped WHERE entry_id = ? AND scope_key = ?")
|
|
320
|
-
.get(entryId, scopeKey);
|
|
321
|
-
return row?.utility ?? 0;
|
|
322
|
-
}
|
|
323
266
|
/**
|
|
324
267
|
* Apply accumulated feedback counts to the utility score of an entry, persisting
|
|
325
268
|
* the result. The bounded-step EMA policy itself (MemRL, F-5 / #386,
|
|
@@ -229,9 +229,14 @@ export function searchVec(db, queryEmbedding, k) {
|
|
|
229
229
|
.all(buf, k);
|
|
230
230
|
}
|
|
231
231
|
catch (err) {
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
232
|
+
// A dimension mismatch (e.g. the embedding provider/model changed since
|
|
233
|
+
// the fast-path table was built) is a real, expected reason this query
|
|
234
|
+
// specifically cannot use the vec table — the complete BLOB table below
|
|
235
|
+
// is unaffected, so fall back to it rather than either silently
|
|
236
|
+
// returning [] (masking a genuinely corrupt index) or failing the whole
|
|
237
|
+
// search over one degraded index.
|
|
238
|
+
warn("[db] searchVec (sqlite-vec path) failed, falling back to JS-cosine scan:", err instanceof Error ? err.message : String(err));
|
|
239
|
+
return searchBlobVec(db, queryEmbedding, k);
|
|
235
240
|
}
|
|
236
241
|
}
|
|
237
242
|
// Fallback: JS-based cosine similarity over BLOB table
|
|
@@ -281,32 +286,25 @@ function bufferToFloat32(buf, expectedDim) {
|
|
|
281
286
|
return Array.from(f32);
|
|
282
287
|
}
|
|
283
288
|
function searchBlobVec(db, queryEmbedding, k) {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (rows.length === 0)
|
|
287
|
-
return [];
|
|
288
|
-
const expectedDim = queryEmbedding.length;
|
|
289
|
-
const scored = [];
|
|
290
|
-
for (const row of rows) {
|
|
291
|
-
const embedding = bufferToFloat32(row.embedding, expectedDim);
|
|
292
|
-
if (embedding === null)
|
|
293
|
-
continue;
|
|
294
|
-
const similarity = cosineSimilarity(queryEmbedding, embedding);
|
|
295
|
-
scored.push({ id: row.id, similarity });
|
|
296
|
-
}
|
|
297
|
-
scored.sort((a, b) => b.similarity - a.similarity);
|
|
298
|
-
// Convert cosine similarity to L2 distance for compatibility with sqlite-vec interface
|
|
299
|
-
// For normalized vectors: L2² = 2(1 - cos_sim)
|
|
300
|
-
return scored.slice(0, k).map(({ id, similarity }) => ({
|
|
301
|
-
id,
|
|
302
|
-
distance: Math.sqrt(2 * Math.max(0, 1 - similarity)),
|
|
303
|
-
}));
|
|
304
|
-
}
|
|
305
|
-
catch (err) {
|
|
306
|
-
// MD-5: Log the failure so it's visible in diagnostics
|
|
307
|
-
warn("[db] searchBlobVec (JS fallback) failed:", err instanceof Error ? err.message : String(err));
|
|
289
|
+
const rows = db.prepare("SELECT id, embedding FROM embeddings").all();
|
|
290
|
+
if (rows.length === 0)
|
|
308
291
|
return [];
|
|
292
|
+
const expectedDim = queryEmbedding.length;
|
|
293
|
+
const scored = [];
|
|
294
|
+
for (const row of rows) {
|
|
295
|
+
const embedding = bufferToFloat32(row.embedding, expectedDim);
|
|
296
|
+
if (embedding === null)
|
|
297
|
+
continue;
|
|
298
|
+
const similarity = cosineSimilarity(queryEmbedding, embedding);
|
|
299
|
+
scored.push({ id: row.id, similarity });
|
|
309
300
|
}
|
|
301
|
+
scored.sort((a, b) => b.similarity - a.similarity);
|
|
302
|
+
// Convert cosine similarity to L2 distance for compatibility with sqlite-vec interface
|
|
303
|
+
// For normalized vectors: L2² = 2(1 - cos_sim)
|
|
304
|
+
return scored.slice(0, k).map(({ id, similarity }) => ({
|
|
305
|
+
id,
|
|
306
|
+
distance: Math.sqrt(2 * Math.max(0, 1 - similarity)),
|
|
307
|
+
}));
|
|
310
308
|
}
|
|
311
309
|
/**
|
|
312
310
|
* Return all entries that do not yet have an embedding row.
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Repository for the state.db `task_history` table (per-task run ledger,
|
|
6
|
+
* replaces the old per-task JSONL files). Extracted verbatim from
|
|
7
|
+
* core/state-db.ts — queries unchanged, only relocated behind the repository
|
|
8
|
+
* boundary. Re-exported by core/state-db.ts so existing importers resolve.
|
|
9
|
+
*
|
|
10
|
+
* @module task-history-repository
|
|
11
|
+
*/
|
|
12
|
+
import { isRecord } from "../../core/common.js";
|
|
4
13
|
function metadataError(message) {
|
|
5
14
|
throw new Error(`invalid task_history metadata_json: ${message}`);
|
|
6
15
|
}
|
|
7
|
-
function isRecord(value) {
|
|
8
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
|
-
}
|
|
10
16
|
function validateDetail(value) {
|
|
11
17
|
if (value === undefined || value === null)
|
|
12
18
|
return;
|
|
@@ -25,6 +25,7 @@ import fs from "node:fs";
|
|
|
25
25
|
import os from "node:os";
|
|
26
26
|
import path from "node:path";
|
|
27
27
|
import launchdTemplate from "../../assets/backends/launchd-template.xml" with { type: "text" };
|
|
28
|
+
import { hasErrnoCode } from "../../core/common.js";
|
|
28
29
|
import { ConfigError } from "../../core/errors.js";
|
|
29
30
|
import { getTaskLogDir } from "../../core/paths.js";
|
|
30
31
|
import { resolveAkmInvocation } from "../resolve-akm-bin.js";
|
|
@@ -851,22 +852,6 @@ export function parseLaunchdLoadedLabels(output) {
|
|
|
851
852
|
}
|
|
852
853
|
return labels;
|
|
853
854
|
}
|
|
854
|
-
function countCharacter(value, needle) {
|
|
855
|
-
let count = 0;
|
|
856
|
-
for (const character of value)
|
|
857
|
-
if (character === needle)
|
|
858
|
-
count += 1;
|
|
859
|
-
return count;
|
|
860
|
-
}
|
|
861
|
-
function hasUnsafeLaunchdControlCharacter(output) {
|
|
862
|
-
for (let index = 0; index < output.length; index += 1) {
|
|
863
|
-
const code = output.charCodeAt(index);
|
|
864
|
-
if (code <= 8 || code === 11 || code === 12 || (code >= 14 && code <= 31) || (code >= 127 && code <= 159)) {
|
|
865
|
-
return true;
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
return false;
|
|
869
|
-
}
|
|
870
855
|
function readDisabledLabels(exec) {
|
|
871
856
|
try {
|
|
872
857
|
const result = exec.run(["launchctl", "print-disabled", `gui/${exec.uid()}`]);
|
|
@@ -933,8 +918,13 @@ function defaultLaunchdFs() {
|
|
|
933
918
|
try {
|
|
934
919
|
return fs.readdirSync(dir);
|
|
935
920
|
}
|
|
936
|
-
catch {
|
|
937
|
-
|
|
921
|
+
catch (error) {
|
|
922
|
+
// Genuinely absent is an empty listing; anything else (e.g. EACCES) is
|
|
923
|
+
// not "no plists" — surfacing it as such could make inspectBindings
|
|
924
|
+
// think there is nothing to reconcile.
|
|
925
|
+
if (hasErrnoCode(error, "ENOENT"))
|
|
926
|
+
return [];
|
|
927
|
+
throw new ConfigError(`Unable to read LaunchAgents directory at "${dir}".`, "INVALID_CONFIG_FILE");
|
|
938
928
|
}
|
|
939
929
|
},
|
|
940
930
|
exists(file) {
|
|
@@ -14,20 +14,15 @@
|
|
|
14
14
|
*
|
|
15
15
|
* D8 result-vocabulary re-code (why, and the WRITE side's exact shape): see
|
|
16
16
|
* docs/architecture/decisions/0005-task-result-vocabulary-and-legacy-read-mapping.md.
|
|
17
|
-
* The READ side
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* "shell"/"script"/"prompt" written WITHOUT a marker, which no production
|
|
27
|
-
* writer ever does) -> `"unknown"`. The P0-pinned null fallbacks survive:
|
|
28
|
-
* workflow `ref` falls back to `""`, the command/prompt arm's `engine`
|
|
29
|
-
* falls back to `null`. A row carrying `targetVocab: 2` reads `target_kind`
|
|
30
|
-
* directly in the current vocabulary — no mapping needed.
|
|
17
|
+
* The READ side used to carry a permanent legacy-vocabulary mapping here
|
|
18
|
+
* (row B-51 of docs/plans/specs/p4-deletions-closeout.md called deleting it
|
|
19
|
+
* "review-blocking") — SUPERSEDED: `task_history` is DB-owned data, so the
|
|
20
|
+
* remap is now a one-time schema migration
|
|
21
|
+
* (`025-task-history-vocabulary-backfill` in src/core/state/migrations.ts)
|
|
22
|
+
* instead of a read-side shim run forever. Every row this function sees now
|
|
23
|
+
* carries the current vocabulary's `target_kind` strings; the P0-pinned null
|
|
24
|
+
* fallbacks still apply (workflow `ref` falls back to `""`, the command
|
|
25
|
+
* arm's `engine` falls back to `null`).
|
|
31
26
|
*
|
|
32
27
|
* A DAG leaf with respect to the rest of src/tasks/run/**: this module
|
|
33
28
|
* imports TaskRunResult/TaskRunStatus's TYPE from ./task-result but no VALUE
|
|
@@ -37,6 +32,7 @@
|
|
|
37
32
|
*/
|
|
38
33
|
import { rethrowIfTestIsolationError } from "../../core/errors.js";
|
|
39
34
|
import { withStateDb } from "../../core/state-db.js";
|
|
35
|
+
import { warn } from "../../core/warn.js";
|
|
40
36
|
import { decodeTaskHistoryMetadata, finalizeTaskHistoryAttempt, getTaskHistory, getTaskHistoryRuns, queryTaskHistory, upsertTaskHistory, } from "../../storage/repositories/task-history-repository.js";
|
|
41
37
|
/** Append (or finalize a reserved attempt into) one task_history row. */
|
|
42
38
|
export function appendHistory(result, historyReserved = false) {
|
|
@@ -67,7 +63,11 @@ export function appendHistory(result, historyReserved = false) {
|
|
|
67
63
|
}
|
|
68
64
|
catch (error) {
|
|
69
65
|
rethrowIfTestIsolationError(error);
|
|
70
|
-
// History recording
|
|
66
|
+
// History recording must not alter the task's own result/exit status —
|
|
67
|
+
// the task already ran and its outcome is independent of whether we can
|
|
68
|
+
// persist a history row. But silence would make a failed write
|
|
69
|
+
// indistinguishable from the task never having run, so warn.
|
|
70
|
+
warn(`task history: failed to record history for task ${result.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
export function readTaskHistory(options = {}) {
|
|
@@ -111,32 +111,22 @@ function decodeTaskHistoryRows(rows) {
|
|
|
111
111
|
* Convert a `TaskHistoryRow` from state.db back to a `TaskRunResult` shape
|
|
112
112
|
* that callers of `readTaskHistory()` expect.
|
|
113
113
|
*
|
|
114
|
-
*
|
|
115
|
-
* `
|
|
114
|
+
* Reads `target_kind` directly in the current (post-D8) vocabulary — the
|
|
115
|
+
* `025-task-history-vocabulary-backfill` state migration rewrites every
|
|
116
|
+
* legacy-vocabulary row before this ever runs against it.
|
|
116
117
|
*/
|
|
117
118
|
function taskHistoryRowToResult(row) {
|
|
118
119
|
const meta = decodeTaskHistoryMetadata(row.metadata_json);
|
|
119
|
-
const marked = meta.targetVocab === 2;
|
|
120
120
|
const target = (() => {
|
|
121
121
|
switch (row.target_kind) {
|
|
122
122
|
case "workflow":
|
|
123
|
-
// PRESERVED for both vintages (incl. the null-ref fallback).
|
|
124
123
|
return { kind: "workflow", ref: row.target_ref ?? "" };
|
|
125
124
|
case "command":
|
|
126
|
-
|
|
127
|
-
// LEGACY vocabulary: the native shell/script arm's shared string.
|
|
128
|
-
return marked ? { kind: "command", engine: meta.engine ?? null } : { kind: "shell" };
|
|
125
|
+
return { kind: "command", engine: meta.engine ?? null };
|
|
129
126
|
case "shell":
|
|
130
|
-
|
|
131
|
-
// "shell" row is unreachable from any production writer.
|
|
132
|
-
return marked ? { kind: "shell" } : { kind: "unknown" };
|
|
127
|
+
return { kind: "shell" };
|
|
133
128
|
case "script":
|
|
134
|
-
|
|
135
|
-
// "script" row is unreachable from any production writer.
|
|
136
|
-
return marked ? { kind: "script" } : { kind: "unknown" };
|
|
137
|
-
case "prompt":
|
|
138
|
-
// Only LEGACY rows (pre-P1b) ever wrote this string.
|
|
139
|
-
return marked ? { kind: "unknown" } : { kind: "command", engine: meta.engine ?? null };
|
|
129
|
+
return { kind: "script" };
|
|
140
130
|
default:
|
|
141
131
|
return { kind: "unknown" };
|
|
142
132
|
}
|
|
@@ -6,6 +6,7 @@ import { createHash } from "node:crypto";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { loadAdapterExecutionSource } from "../commands/command/execution-source-loader.js";
|
|
8
8
|
import { makeBundleRef } from "../core/asset/asset-ref.js";
|
|
9
|
+
import { compareCodePoints, toPosix } from "../core/common.js";
|
|
9
10
|
import { UsageError } from "../core/errors.js";
|
|
10
11
|
import { canonicalizeWorkflowName, WORKFLOW_EXTENSIONS } from "../core/recognition-util.js";
|
|
11
12
|
import { captureGuardedDirectoryManifest, captureGuardedExecutionSource, GuardedExecutionSourceCollector, } from "../execution/guarded-source.js";
|
|
@@ -742,12 +743,6 @@ function freezeOperation(operation) {
|
|
|
742
743
|
function sameInvocation(left, right) {
|
|
743
744
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
744
745
|
}
|
|
745
|
-
function compareCodePoints(left, right) {
|
|
746
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
747
|
-
}
|
|
748
|
-
function toPosix(value) {
|
|
749
|
-
return value.replaceAll("\\", "/");
|
|
750
|
-
}
|
|
751
746
|
// Retain the concrete error in this module's public dependency graph so callers
|
|
752
747
|
// can continue to identify ownership failures without importing an adapter.
|
|
753
748
|
export { WorkflowSourceRejectionError };
|