akm-cli 0.9.26 → 0.9.27-alpha.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 +125 -0
- package/dist/assets/prompts/consolidate-system.md +2 -2
- package/dist/assets/prompts/extract-session.md +2 -2
- package/dist/commands/improve/consolidate/pair-pass.js +1 -1
- package/dist/commands/improve/consolidate.js +14 -4
- package/dist/commands/improve/distill.js +9 -5
- package/dist/commands/improve/extract-prompt.js +61 -39
- package/dist/commands/improve/extract.js +2 -1
- package/dist/commands/improve/reflect.js +36 -4
- package/dist/commands/improve/retrieval-gate.js +1 -1
- package/dist/commands/improve/session-asset.js +3 -2
- package/dist/commands/improve/stage.js +1 -1
- package/dist/indexer/indexer.js +35 -19
- package/dist/integrations/harnesses/codex/agent-builder.js +23 -15
- package/dist/llm/client.js +44 -14
- package/dist/llm/memory-infer.js +1 -1
- package/dist/scripts/akm-migrate-node.js +25 -20
- package/dist/scripts/akm-migrate.js +25 -20
- package/dist/storage/repositories/index-entry-schema.js +20 -4
- package/dist/storage/repositories/index-fts-repository.js +44 -3
- package/dist/storage/repositories/index-schema.js +14 -8
- package/docs/reference/cli.md +1 -0
- package/docs/reference/configuration.md +5 -2
- package/package.json +1 -1
|
@@ -19,9 +19,27 @@ import { splitMarkdownFragments } from "../../core/asset/markdown-fragments.js";
|
|
|
19
19
|
import { warn } from "../../core/warn.js";
|
|
20
20
|
import { ftsOrMatch, ftsQueryTokens } from "../../indexer/search/fts-query.js";
|
|
21
21
|
import { buildSearchFields } from "../../indexer/search/search-fields.js";
|
|
22
|
+
import { isContentlessFtsDdl, readTableSql } from "./index-entry-schema.js";
|
|
23
|
+
import { deleteMeta, getMeta } from "./index-meta-repository.js";
|
|
22
24
|
import { SQLITE_CHUNK_SIZE } from "./index-sql.js";
|
|
23
25
|
// `entries_fts.rowid = entries.id`, so a per-entry delete is a rowid lookup.
|
|
24
26
|
const INSERT_FTS_SQL = "INSERT INTO entries_fts (rowid, entry_id, name, description, tags, hints, content) VALUES (?, ?, ?, ?, ?, ?, ?)";
|
|
27
|
+
/**
|
|
28
|
+
* `index_meta` key stamped when a delete took a row out of `entries_fts`.
|
|
29
|
+
*
|
|
30
|
+
* FTS5 cannot take a deleted row out of a contentless table's BM25 totals (its
|
|
31
|
+
* row count, and the token counts the average document length comes from), so
|
|
32
|
+
* every removal or replacement leaves them one row too high and an updated
|
|
33
|
+
* index scores differently from a fresh one (#1048). SQLite has no command that
|
|
34
|
+
* recomputes them (`delete` and `rebuild` are refused on a contentless table),
|
|
35
|
+
* so the next `akm index` rebuilds the table from `entries`
|
|
36
|
+
* ({@link rebuildFtsIfTotalsStale}).
|
|
37
|
+
*/
|
|
38
|
+
const FTS_TOTALS_STALE_META = "ftsTotalsStale";
|
|
39
|
+
/** The content-bearing table older SQLite gets subtracts a deleted row itself. */
|
|
40
|
+
function isContentlessFts(db) {
|
|
41
|
+
return isContentlessFtsDdl(readTableSql(db, "entries_fts"));
|
|
42
|
+
}
|
|
25
43
|
const ftsMutationStatementsByDb = new WeakMap();
|
|
26
44
|
function getFtsMutationStatements(db) {
|
|
27
45
|
const existing = ftsMutationStatementsByDb.get(db);
|
|
@@ -29,6 +47,7 @@ function getFtsMutationStatements(db) {
|
|
|
29
47
|
return existing;
|
|
30
48
|
const statements = {
|
|
31
49
|
deleteOne: db.prepare("DELETE FROM entries_fts WHERE rowid = ?"),
|
|
50
|
+
markTotalsStale: db.prepare(`INSERT OR IGNORE INTO index_meta (key, value) VALUES ('${FTS_TOTALS_STALE_META}', '1')`),
|
|
32
51
|
insert: db.prepare(INSERT_FTS_SQL),
|
|
33
52
|
upsertFragmentSource: db.prepare("INSERT INTO entry_fragments (entry_id, safe_markdown) VALUES (?, ?) ON CONFLICT(entry_id) DO UPDATE SET safe_markdown = excluded.safe_markdown"),
|
|
34
53
|
deleteFragmentSource: db.prepare("DELETE FROM entry_fragments WHERE entry_id = ?"),
|
|
@@ -36,6 +55,11 @@ function getFtsMutationStatements(db) {
|
|
|
36
55
|
ftsMutationStatementsByDb.set(db, statements);
|
|
37
56
|
return statements;
|
|
38
57
|
}
|
|
58
|
+
/** A delete that removed a row leaves the table's BM25 totals too high until it is rebuilt. */
|
|
59
|
+
function noteRemoval(statements, removed) {
|
|
60
|
+
if (removed.changes > 0)
|
|
61
|
+
statements.markTotalsStale.run();
|
|
62
|
+
}
|
|
39
63
|
/**
|
|
40
64
|
* Replace one entry's derived FTS row, and its fragment source when the scan
|
|
41
65
|
* read Markdown, inside the caller's transaction.
|
|
@@ -43,7 +67,7 @@ function getFtsMutationStatements(db) {
|
|
|
43
67
|
export function replaceFtsEntry(db, entryId, entry, fragmentContent) {
|
|
44
68
|
const fields = buildSearchFields(entry);
|
|
45
69
|
const statements = getFtsMutationStatements(db);
|
|
46
|
-
statements.deleteOne.run(entryId);
|
|
70
|
+
noteRemoval(statements, statements.deleteOne.run(entryId));
|
|
47
71
|
statements.insert.run(entryId, entryId, fields.name, fields.description, fields.tags, fields.hints, fields.content);
|
|
48
72
|
if (fragmentContent === undefined) {
|
|
49
73
|
// Metadata-only re-upserts and re-keys deserialize the public document
|
|
@@ -61,7 +85,7 @@ export function deleteFtsEntries(db, entryIds) {
|
|
|
61
85
|
for (let i = 0; i < entryIds.length; i += SQLITE_CHUNK_SIZE) {
|
|
62
86
|
const chunk = entryIds.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
63
87
|
const placeholders = chunk.map(() => "?").join(",");
|
|
64
|
-
db.prepare(`DELETE FROM entries_fts WHERE rowid IN (${placeholders})`).run(...chunk);
|
|
88
|
+
noteRemoval(getFtsMutationStatements(db), db.prepare(`DELETE FROM entries_fts WHERE rowid IN (${placeholders})`).run(...chunk));
|
|
65
89
|
db.prepare(`DELETE FROM entry_fragments WHERE entry_id IN (${placeholders})`).run(...chunk);
|
|
66
90
|
}
|
|
67
91
|
}
|
|
@@ -143,7 +167,9 @@ function materializeIndexedMarkdownFragment(fragment, fragments, parentChars) {
|
|
|
143
167
|
*/
|
|
144
168
|
export function rebuildFts(db) {
|
|
145
169
|
db.transaction(() => {
|
|
146
|
-
|
|
170
|
+
// `delete-all` also resets the BM25 totals, which a plain DELETE leaves as they were (#1048).
|
|
171
|
+
db.exec(isContentlessFts(db) ? "INSERT INTO entries_fts(entries_fts) VALUES('delete-all')" : "DELETE FROM entries_fts");
|
|
172
|
+
deleteMeta(db, FTS_TOTALS_STALE_META);
|
|
147
173
|
// Keyset pages, so a large index is never held in memory at once.
|
|
148
174
|
const page = db.prepare("SELECT id, document_json FROM entries WHERE id > ? ORDER BY id LIMIT 500");
|
|
149
175
|
const insertStmt = db.prepare(INSERT_FTS_SQL);
|
|
@@ -171,3 +197,18 @@ export function rebuildFts(db) {
|
|
|
171
197
|
}
|
|
172
198
|
})();
|
|
173
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Rebuild `entries_fts` from `entries` when rows have left it since its BM25
|
|
202
|
+
* totals were last taken (#1048): about a second at 25,000 entries. Returns
|
|
203
|
+
* whether it did.
|
|
204
|
+
*/
|
|
205
|
+
export function rebuildFtsIfTotalsStale(db) {
|
|
206
|
+
if (getMeta(db, FTS_TOTALS_STALE_META) === undefined)
|
|
207
|
+
return false;
|
|
208
|
+
if (!isContentlessFts(db)) {
|
|
209
|
+
deleteMeta(db, FTS_TOTALS_STALE_META);
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
rebuildFts(db);
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
@@ -25,7 +25,7 @@ import path from "node:path";
|
|
|
25
25
|
import { ConfigError } from "../../core/errors.js";
|
|
26
26
|
import { warn } from "../../core/warn.js";
|
|
27
27
|
import { sha256Hex } from "../../runtime.js";
|
|
28
|
-
import { CANONICAL_ENTRY_SCHEMA_SQL, CANONICAL_INDEX_DB_VERSION, entriesFtsDdl, isContentlessFtsDdl, missingEntryColumns, readTableSql, supportsContentlessDelete, tableExists, } from "./index-entry-schema.js";
|
|
28
|
+
import { CANONICAL_ENTRY_SCHEMA_SQL, CANONICAL_INDEX_DB_VERSION, entriesFtsDdl, isContentlessFtsDdl, missingEntryColumns, readTableSql, retiredEntryColumns, supportsContentlessDelete, tableExists, } from "./index-entry-schema.js";
|
|
29
29
|
import { rebuildFts } from "./index-fts-repository.js";
|
|
30
30
|
import { rebuildAllEntryLinks } from "./index-links-repository.js";
|
|
31
31
|
import { getMeta, setMeta } from "./index-meta-repository.js";
|
|
@@ -67,11 +67,13 @@ const REGISTRY_INDEX_CACHE_DDL = `
|
|
|
67
67
|
ON registry_index_cache(fetched_at);
|
|
68
68
|
`;
|
|
69
69
|
/**
|
|
70
|
-
* An `entries` table missing a required column
|
|
71
|
-
* this release (the last such change was
|
|
72
|
-
* transitional `entry_key`/`dir_path`/... columns
|
|
73
|
-
*
|
|
74
|
-
*
|
|
70
|
+
* An `entries` table missing a required column, or still carrying a retired
|
|
71
|
+
* one, cannot be read or written by this release (the last such change was
|
|
72
|
+
* v20→v21, which removed the transitional `entry_key`/`dir_path`/... columns
|
|
73
|
+
* and made `item_ref` the key). Layouts 18–20 carried the current columns
|
|
74
|
+
* beside the retired ones, so only the retired ones give them away.
|
|
75
|
+
* Recreate only the tables keyed by `entries.id` — their ids are about to be
|
|
76
|
+
* re-minted, so the rows would dangle anyway. The LLM enrichment cache
|
|
75
77
|
* (keyed by ref) is kept. The LLM entity-graph tables are unconditionally
|
|
76
78
|
* dropped elsewhere in this file regardless of this recreation (retired
|
|
77
79
|
* 0.9.17-alpha.9), not kept. The next index run re-walks every source.
|
|
@@ -80,9 +82,13 @@ function ensureEntriesLayout(db) {
|
|
|
80
82
|
if (!tableExists(db, "entries"))
|
|
81
83
|
return;
|
|
82
84
|
const missing = missingEntryColumns(db);
|
|
83
|
-
|
|
85
|
+
const retired = retiredEntryColumns(db);
|
|
86
|
+
if (missing.length === 0 && retired.length === 0)
|
|
84
87
|
return;
|
|
85
|
-
|
|
88
|
+
const why = missing.length > 0
|
|
89
|
+
? `predates the ${missing.join(", ")} column${missing.length === 1 ? "" : "s"}`
|
|
90
|
+
: `still has the retired ${retired.join(", ")} column${retired.length === 1 ? "" : "s"}`;
|
|
91
|
+
warn(`Index database entries table ${why} — ` +
|
|
86
92
|
"recreating the entries-keyed tables (entries, full-text, embeddings, utility scores); the " +
|
|
87
93
|
"LLM enrichment cache is kept. The next index run re-walks every source.");
|
|
88
94
|
db.transaction(() => {
|
package/docs/reference/cli.md
CHANGED
|
@@ -2780,6 +2780,7 @@ harvest" branch on `skipReasons`, `warnings`, or `sessionsProcessed` /
|
|
|
2780
2780
|
| `engineKind` | `"llm"`, `"sdk"`, or `"agent"` — the kind of runner `engine` resolved to. Same absence condition as `engine`. |
|
|
2781
2781
|
| `skipReasons` | Per-`skipReason` count across `sessions[]` (e.g. `{ "llm_unavailable": 25 }`). Present only when `sessionsSkipped > 0`. |
|
|
2782
2782
|
| `warnings` | Includes one aggregate line per infrastructure skip reason that fired (`llm_unavailable`, `read_failed`, `exception`, `locked_concurrent`) — e.g. `25 of 25 sessions skipped: llm_unavailable (engine "default")` — so an engine outage is visible without inspecting `sessions[]`. Session-content skips (`already_extracted`, `too_short`, `triaged_out`) are counted in `skipReasons` but never produce a warning line. |
|
|
2783
|
+
| `sessions[].warnings` | One line per candidate the model wrote that the output contract refuses, as `<type>:<name> dropped: <reason>` — a lesson without a `when_to_use` of 15 characters, a description under 20, a name that is not a kebab-case slug — and one per candidate held back by the improve ledger. A session whose every candidate was dropped has `candidateCount: 0` and no `rationaleIfEmpty`, so this is where the loss shows. |
|
|
2783
2784
|
|
|
2784
2785
|
#### proposal new
|
|
2785
2786
|
|
|
@@ -110,8 +110,11 @@ distill quality-gate judges do too unless the judge's engine sets
|
|
|
110
110
|
`reasoning_effort` when set. Backend support: llama.cpp direct honors both forms
|
|
111
111
|
(`reasoning_effort` from build ≥ b10644); vLLM honors
|
|
112
112
|
`chat_template_kwargs`; Bifrost drops `chat_template_kwargs` and passes
|
|
113
|
-
`reasoning_effort` through, so also set `reasoningEffort: "none"` behind it
|
|
114
|
-
strict hosted API
|
|
113
|
+
`reasoning_effort` through, so also set `reasoningEffort: "none"` behind it. A
|
|
114
|
+
strict hosted API that rejects the two fields (OpenAI answers 400 `Unknown
|
|
115
|
+
parameter: 'chat_template_kwargs'`) gets one retry without both, and AKM stops
|
|
116
|
+
sending them to that endpoint and model for the rest of the process, as it
|
|
117
|
+
does for `response_format` below. Both fields are AKM-owned, not
|
|
115
118
|
settable via `extraParams`. A response with reasoning tokens despite
|
|
116
119
|
`enableThinking: false` triggers a runtime warning and the `akm health`
|
|
117
120
|
`thinking-control` advisory.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.27-alpha.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cursor.",
|
|
6
6
|
"keywords": [
|