akm-cli 0.9.18 → 0.9.19-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 +135 -0
- package/STABILITY.md +2 -1
- package/dist/assets/hints/cli-hints-full.md +4 -2
- package/dist/assets/hints/cli-hints-short.md +5 -3
- package/dist/assets/prompts/reflect-feedback-framing.md +1 -1
- package/dist/commands/feedback-cli.js +1 -1
- package/dist/commands/improve/consolidate/coverage.js +132 -0
- package/dist/commands/improve/consolidate/pair-pass.js +30 -20
- package/dist/commands/improve/consolidate.js +43 -10
- package/dist/commands/improve/distill.js +7 -3
- package/dist/commands/improve/eligibility.js +39 -9
- package/dist/commands/improve/improve-cli.js +9 -6
- package/dist/commands/improve/improve.js +13 -4
- package/dist/commands/improve/ledger.js +2 -2
- package/dist/commands/improve/loop-stages.js +2 -0
- package/dist/commands/improve/preparation.js +1 -1
- package/dist/commands/improve/reflect.js +1 -1
- package/dist/commands/improve/stage.js +38 -9
- package/dist/commands/proposal/diff-format.js +21 -0
- package/dist/commands/proposal/proposal-cli.js +48 -10
- package/dist/commands/proposal/proposal-types.js +11 -0
- package/dist/commands/proposal/proposal.js +60 -5
- package/dist/commands/proposal/repository.js +245 -17
- package/dist/commands/read/knowledge.js +13 -11
- package/dist/commands/read/remember-cli.js +7 -3
- package/dist/commands/sources/source-clone.js +1 -1
- package/dist/commands/tasks/tasks-cli.js +1 -1
- package/dist/commands/tasks/tasks.js +10 -3
- package/dist/core/mutation-target.js +8 -3
- package/dist/core/write-source.js +3 -2
- package/dist/indexer/usage/usage-events.js +2 -1
- package/dist/output/shapes/helpers.js +7 -0
- package/dist/output/shapes/passthrough.js +1 -0
- package/dist/output/shapes/proposal/reopen.js +14 -0
- package/dist/output/shapes.js +2 -0
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/proposal/proposal.js +3 -1
- package/dist/output/text/proposal-format.js +87 -32
- package/dist/scripts/akm-migrate-node.js +79 -23
- package/dist/scripts/akm-migrate.js +79 -23
- package/dist/storage/repositories/improve-ledger-repository.js +65 -6
- package/dist/storage/repositories/index-vec-repository.js +13 -8
- package/dist/storage/repositories/proposals-repository.js +23 -0
- package/dist/tasks/run/load-task.js +5 -1
- package/docs/migration/README.md +1 -0
- package/docs/migration/release-notes/0.9.19.md +134 -0
- package/docs/migration/release-notes/README.md +5 -0
- package/docs/migration/v0.8-to-v0.9.md +5 -1
- package/docs/reference/cli.md +189 -28
- package/docs/reference/configuration.md +9 -8
- package/docs/reference/data-and-telemetry.md +19 -14
- package/package.json +1 -1
|
@@ -38,6 +38,33 @@ export const LEDGER_REVISIT_CADENCE_DAYS = 7;
|
|
|
38
38
|
* eligible again.
|
|
39
39
|
*/
|
|
40
40
|
export const PAIR_PASS_LEDGER_SOURCE = "consolidate-pair";
|
|
41
|
+
/**
|
|
42
|
+
* The consolidate promote pass's ledger source: one row per source memory,
|
|
43
|
+
* keyed by the memory (`memories/<name>`), not by the knowledge ref the
|
|
44
|
+
* promotion would create.
|
|
45
|
+
*/
|
|
46
|
+
export const CONSOLIDATE_LEDGER_SOURCE = "consolidate";
|
|
47
|
+
/**
|
|
48
|
+
* Whether a decision on `(source, outcome)` leaves the ref's next attempt to
|
|
49
|
+
* its content instead of a clock (#998). An accepted or rejected consolidate
|
|
50
|
+
* promotion is a verdict on that memory's text; asking the model about the
|
|
51
|
+
* same text again can only reproduce the proposal (accepted used to be
|
|
52
|
+
* eligible at once, rejected after 7 days), so the memory waits for an edit.
|
|
53
|
+
* The clock stays for a row with no recorded hash — one decided before the
|
|
54
|
+
* hash was recorded — see {@link nextEligibleAt} and {@link isContentDrivenRow}.
|
|
55
|
+
*/
|
|
56
|
+
export function isContentDrivenDecision(source, outcome) {
|
|
57
|
+
return source === CONSOLIDATE_LEDGER_SOURCE && (outcome === "accepted" || outcome === "rejected");
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Whether this row is held by its content hash: a decided consolidate
|
|
61
|
+
* promotion that recorded the body it was decided against. Such a row has no
|
|
62
|
+
* `next_eligible_at`; the caller compares `contentHash` with the asset's
|
|
63
|
+
* current body hash (the pair pass does the same in `selectInitiators`).
|
|
64
|
+
*/
|
|
65
|
+
export function isContentDrivenRow(row) {
|
|
66
|
+
return row.contentHash !== null && isContentDrivenDecision(row.source, row.outcome);
|
|
67
|
+
}
|
|
41
68
|
/**
|
|
42
69
|
* Outcomes whose window a fresh signal on the asset (new feedback, a content
|
|
43
70
|
* change) cannot lift. Every other window is a revisit cadence that a signal
|
|
@@ -73,12 +100,17 @@ function windowDays(source, outcome) {
|
|
|
73
100
|
}
|
|
74
101
|
/**
|
|
75
102
|
* The single cadence function: when a `(source, outcome)` recorded at
|
|
76
|
-
* `fromIso` becomes eligible again, or `null` for "immediately".
|
|
103
|
+
* `fromIso` becomes eligible again, or `null` for "immediately". A decision
|
|
104
|
+
* that {@link isContentDrivenDecision} holds by content, recorded together
|
|
105
|
+
* with the body hash it was decided against, starts no clock at all: `null`
|
|
106
|
+
* here means the hash is the whole test, not that the ref is free to retry.
|
|
77
107
|
*/
|
|
78
|
-
export function nextEligibleAt(source, outcome, fromIso) {
|
|
108
|
+
export function nextEligibleAt(source, outcome, fromIso, contentHash) {
|
|
79
109
|
const from = Date.parse(fromIso);
|
|
80
110
|
if (!Number.isFinite(from))
|
|
81
111
|
return null;
|
|
112
|
+
if (contentHash && isContentDrivenDecision(source, outcome))
|
|
113
|
+
return null;
|
|
82
114
|
const days = windowDays(source, outcome);
|
|
83
115
|
return days === null ? null : new Date(from + days * MS_PER_DAY).toISOString();
|
|
84
116
|
}
|
|
@@ -127,7 +159,7 @@ export function recordImproveLedger(db, input) {
|
|
|
127
159
|
source: input.source,
|
|
128
160
|
lastAttemptAt: input.at,
|
|
129
161
|
outcome: input.outcome,
|
|
130
|
-
nextEligibleAt: nextEligibleAt(input.source, input.outcome, input.at),
|
|
162
|
+
nextEligibleAt: nextEligibleAt(input.source, input.outcome, input.at, input.contentHash),
|
|
131
163
|
proposalId: input.proposalId ?? null,
|
|
132
164
|
detail: trimDetail(input.detail),
|
|
133
165
|
contentHash: input.contentHash ?? null,
|
|
@@ -150,14 +182,16 @@ export function recordImproveLedger(db, input) {
|
|
|
150
182
|
* differ from the ledger key (a distill proposal for `lessons/x` is keyed by
|
|
151
183
|
* its input `memories/x`) — and `last_attempt_at` is kept: the window starts
|
|
152
184
|
* at the decision, the attempt happened when it happened. A proposal no row
|
|
153
|
-
* knows (minted before the ledger existed
|
|
185
|
+
* knows (minted before the ledger existed, or whose row a later `judged_no_action`
|
|
186
|
+
* overwrote) gets a row keyed by `input.ref`.
|
|
154
187
|
*/
|
|
155
188
|
export function recordImproveLedgerDecision(db, input) {
|
|
189
|
+
const hash = isContentDrivenDecision(input.source, input.outcome) ? input.contentHash : undefined;
|
|
156
190
|
const changes = db
|
|
157
191
|
.prepare(`UPDATE improve_ledger
|
|
158
|
-
SET outcome = ?, next_eligible_at = ?, detail =
|
|
192
|
+
SET outcome = ?, next_eligible_at = ?, detail = ?, content_hash = COALESCE(?, content_hash)
|
|
159
193
|
WHERE stash_dir = ? AND proposal_id = ?`)
|
|
160
|
-
.run(input.outcome, nextEligibleAt(input.source, input.outcome, input.at), trimDetail(input.detail), input.stashDir, input.proposalId).changes;
|
|
194
|
+
.run(input.outcome, nextEligibleAt(input.source, input.outcome, input.at, hash), trimDetail(input.detail), hash ?? null, input.stashDir, input.proposalId).changes;
|
|
161
195
|
if (Number(changes) > 0)
|
|
162
196
|
return;
|
|
163
197
|
recordImproveLedger(db, {
|
|
@@ -168,8 +202,33 @@ export function recordImproveLedgerDecision(db, input) {
|
|
|
168
202
|
at: input.at,
|
|
169
203
|
proposalId: input.proposalId,
|
|
170
204
|
...(input.detail !== undefined ? { detail: input.detail } : {}),
|
|
205
|
+
...(hash !== undefined ? { contentHash: hash } : {}),
|
|
171
206
|
});
|
|
172
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* A rejected proposal was reopened (`akm proposal reopen`): put the rows
|
|
210
|
+
* {@link recordImproveLedgerDecision} found by `proposal_id` back to what the
|
|
211
|
+
* mint wrote — `proposed`, on the revisit cadence from the reopen, and no
|
|
212
|
+
* `content_hash` (a mint records none) — so the rejection's hard window stops
|
|
213
|
+
* reporting (and blocking) a proposal that is pending again.
|
|
214
|
+
* `last_attempt_at` is kept, as a decision keeps it. A proposal with no such
|
|
215
|
+
* row (its mint wrote none, or a later attempt took the key over) changes
|
|
216
|
+
* nothing.
|
|
217
|
+
*/
|
|
218
|
+
export function reopenImproveLedgerDecision(db, input) {
|
|
219
|
+
db.prepare(`UPDATE improve_ledger
|
|
220
|
+
SET outcome = 'proposed', next_eligible_at = ?, detail = ?, content_hash = NULL
|
|
221
|
+
WHERE stash_dir = ? AND proposal_id = ?`).run(nextEligibleAt(input.source, "proposed", input.at), trimDetail(input.detail), input.stashDir, input.proposalId);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* The same reopen for a proposal whose mint deliberately wrote no ledger row (a
|
|
225
|
+
* retire proposal — see `createRetireProposal`): the row its rejection created
|
|
226
|
+
* is dropped, returning the ledger to what the mint left. Found by
|
|
227
|
+
* `proposal_id`, like {@link recordImproveLedgerDecision}.
|
|
228
|
+
*/
|
|
229
|
+
export function forgetImproveLedgerDecision(db, stashDir, proposalId) {
|
|
230
|
+
db.prepare("DELETE FROM improve_ledger WHERE stash_dir = ? AND proposal_id = ?").run(stashDir, proposalId);
|
|
231
|
+
}
|
|
173
232
|
/**
|
|
174
233
|
* Should-fix 6 (second review round): a read-only or dry-run open never
|
|
175
234
|
* migrates, so it can land on a state.db from before migration 029 added
|
|
@@ -80,9 +80,11 @@ export function upsertEmbedding(db, entryId, embedding, model) {
|
|
|
80
80
|
* (ties by id): an exact scan of the current model's rows. `distance` is
|
|
81
81
|
* `sqrt(2 * (1 - cosine))`, the L2 distance between the unit-normalised
|
|
82
82
|
* vectors. Rows of another width, left by a model that is no longer current,
|
|
83
|
-
* never match.
|
|
83
|
+
* never match. With a `scope`, only that bundle's entries of that type are
|
|
84
|
+
* scanned: the `k` nearest of those, not the `k` nearest of everything filtered
|
|
85
|
+
* afterwards.
|
|
84
86
|
*/
|
|
85
|
-
export function searchVec(db, query, k) {
|
|
87
|
+
export function searchVec(db, query, k, scope) {
|
|
86
88
|
const dim = query.length;
|
|
87
89
|
const q = Float64Array.from(query);
|
|
88
90
|
let queryNorm = 0;
|
|
@@ -92,11 +94,13 @@ export function searchVec(db, query, k) {
|
|
|
92
94
|
return [];
|
|
93
95
|
queryNorm = Math.sqrt(queryNorm);
|
|
94
96
|
const current = modelPredicate(db, currentEmbeddingModel(db));
|
|
97
|
+
const scoped = scope ? " AND embeddings.id IN (SELECT id FROM entries WHERE type = ? AND bundle_id = ?)" : "";
|
|
98
|
+
const params = scope ? [...current.params, scope.type, scope.bundleId] : current.params;
|
|
95
99
|
const ids = [];
|
|
96
100
|
const similarities = [];
|
|
97
101
|
const rows = db
|
|
98
|
-
.prepare(`SELECT id, embedding FROM embeddings WHERE ${current.sql}`)
|
|
99
|
-
.iterate(...
|
|
102
|
+
.prepare(`SELECT id, embedding FROM embeddings WHERE ${current.sql}${scoped}`)
|
|
103
|
+
.iterate(...params);
|
|
100
104
|
for (const { id, embedding } of rows) {
|
|
101
105
|
if (embedding.byteLength !== dim * 4)
|
|
102
106
|
continue;
|
|
@@ -126,14 +130,15 @@ export function searchVec(db, query, k) {
|
|
|
126
130
|
/**
|
|
127
131
|
* The k nearest neighbours of an already-indexed entry, by its stored vector —
|
|
128
132
|
* no re-embedding, no network. Returns [] when the entry has no stored
|
|
129
|
-
* vector.
|
|
130
|
-
* filter it out by id.
|
|
133
|
+
* vector. Unscoped, the entry itself is typically returned with distance ~0;
|
|
134
|
+
* callers filter it out by id. A `scope` confines the neighbours to one
|
|
135
|
+
* bundle's entries of one type (see {@link searchVec}).
|
|
131
136
|
*/
|
|
132
|
-
export function getNeighborsByEntryId(db, id, k) {
|
|
137
|
+
export function getNeighborsByEntryId(db, id, k, scope) {
|
|
133
138
|
const row = db.prepare("SELECT embedding FROM embeddings WHERE id = ?").get(id);
|
|
134
139
|
if (!row || row.embedding.byteLength % 4 !== 0)
|
|
135
140
|
return [];
|
|
136
|
-
return searchVec(db, new Float32Array(row.embedding.slice().buffer), k);
|
|
141
|
+
return searchVec(db, new Float32Array(row.embedding.slice().buffer), k, scope);
|
|
137
142
|
}
|
|
138
143
|
/**
|
|
139
144
|
* Every entry that has no embedding row for `model` (any row when no model is
|
|
@@ -106,6 +106,20 @@ const GATE_OUTCOMES = {
|
|
|
106
106
|
staged: true,
|
|
107
107
|
"auto-rejected": true,
|
|
108
108
|
};
|
|
109
|
+
function isRecord(value) {
|
|
110
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
111
|
+
}
|
|
112
|
+
/** One undone rejection in `metadata_json.reviewHistory` (see `ProposalReviewHistoryEntry`): only the fields readers rely on are checked. */
|
|
113
|
+
function isReviewHistoryEntry(value) {
|
|
114
|
+
if (!isRecord(value))
|
|
115
|
+
return false;
|
|
116
|
+
const { review, gateDecision, reopenedAt, reopenReason } = value;
|
|
117
|
+
return (typeof reopenedAt === "string" &&
|
|
118
|
+
(reopenReason === undefined || typeof reopenReason === "string") &&
|
|
119
|
+
(review === undefined ||
|
|
120
|
+
(isRecord(review) && typeof review.outcome === "string" && typeof review.decidedAt === "string")) &&
|
|
121
|
+
(gateDecision === undefined || isRecord(gateDecision)));
|
|
122
|
+
}
|
|
109
123
|
function validatePresentMetadata(meta) {
|
|
110
124
|
const stringFields = ["sourceRun", "beforeHash", "beforeHashNormalized", "backupContent"];
|
|
111
125
|
for (const field of stringFields) {
|
|
@@ -129,6 +143,12 @@ function validatePresentMetadata(meta) {
|
|
|
129
143
|
invalidPresentField("review");
|
|
130
144
|
}
|
|
131
145
|
}
|
|
146
|
+
if (Object.hasOwn(meta, "reviewHistory")) {
|
|
147
|
+
const history = meta.reviewHistory;
|
|
148
|
+
if (!Array.isArray(history) || history.some((entry) => !isReviewHistoryEntry(entry))) {
|
|
149
|
+
invalidPresentField("reviewHistory");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
132
152
|
if (Object.hasOwn(meta, "gateDecision")) {
|
|
133
153
|
const gate = meta.gateDecision;
|
|
134
154
|
if (typeof gate !== "object" ||
|
|
@@ -270,6 +290,7 @@ export function proposalRowToProposal(row) {
|
|
|
270
290
|
...(typeof meta.beforeHash === "string" ? { beforeHash: meta.beforeHash } : {}),
|
|
271
291
|
...(typeof meta.beforeHashNormalized === "string" ? { beforeHashNormalized: meta.beforeHashNormalized } : {}),
|
|
272
292
|
...(meta.review !== undefined ? { review: meta.review } : {}),
|
|
293
|
+
...(meta.reviewHistory !== undefined ? { reviewHistory: meta.reviewHistory } : {}),
|
|
273
294
|
...(typeof meta.confidence === "number" ? { confidence: meta.confidence } : {}),
|
|
274
295
|
...(meta.gateDecision !== undefined ? { gateDecision: meta.gateDecision } : {}),
|
|
275
296
|
...(typeof meta.backupContent === "string" ? { backupContent: meta.backupContent } : {}),
|
|
@@ -332,6 +353,8 @@ export function proposalToRowValues(proposal, stashDir) {
|
|
|
332
353
|
metaObj.sourceRun = proposal.sourceRun;
|
|
333
354
|
if (proposal.review !== undefined)
|
|
334
355
|
metaObj.review = proposal.review;
|
|
356
|
+
if (proposal.reviewHistory !== undefined)
|
|
357
|
+
metaObj.reviewHistory = proposal.reviewHistory;
|
|
335
358
|
if (proposal.confidence !== undefined)
|
|
336
359
|
metaObj.confidence = proposal.confidence;
|
|
337
360
|
if (proposal.gateDecision !== undefined)
|
|
@@ -107,7 +107,11 @@ export async function loadPreparedTask(id, options) {
|
|
|
107
107
|
return { file: await resolveAssetPath(bundleDir, type, name), bundleRoot: bundleDir };
|
|
108
108
|
}
|
|
109
109
|
const resolutionConfig = requiresCommandConfig ? config : loadConfig();
|
|
110
|
-
|
|
110
|
+
// `bundle` is the qualifier of an asset ref in the task, not a flag.
|
|
111
|
+
const resolvedBundle = resolveWriteTarget(resolutionConfig, bundle, {
|
|
112
|
+
requireWritable: false,
|
|
113
|
+
flag: "The asset ref's bundle",
|
|
114
|
+
});
|
|
111
115
|
return {
|
|
112
116
|
file: await resolveAssetPath(resolvedBundle.source.path, type, name),
|
|
113
117
|
bundleRoot: resolvedBundle.source.path,
|
package/docs/migration/README.md
CHANGED
|
@@ -6,6 +6,7 @@ Upgrade guides and per-release migration notes.
|
|
|
6
6
|
- [v0.9.2 release note](release-notes/0.9.2.md) -- Self-contained terminal upgrade summary shipped for `akm help migrate 0.9.2`
|
|
7
7
|
- [v0.9.16 release note](release-notes/0.9.16.md) -- Source-bound scheduler grants, local execution authority, and split unsafe overrides
|
|
8
8
|
- [v0.9.17 release note](release-notes/0.9.17.md) -- Consolidate's retire proposals and index layout 26, replacing the LLM entity graph with declared links, and `akm improve` scoped to what retrieval actually returns
|
|
9
|
+
- [v0.9.19 release note](release-notes/0.9.19.md) -- `akm improve` scoped to the bundle it writes to (scheduled runs need one `--bundle` run per other bundle), `akm proposal reopen` and the retire-proposal diff, and fewer repeat consolidate promotions
|
|
9
10
|
- [v0.8 -> current v0.9 migration guide](v0.8-to-v0.9.md) -- Package upgrade with fresh current config/state and explicit task conversion
|
|
10
11
|
- [v0.7 -> v0.8 migration guide](v0.7-to-v0.8.md) -- Task schema and 0.8-era changes
|
|
11
12
|
- [v0.5 -> v0.6 migration guide](https://github.com/itlackey/akm/blob/main/docs/migration/v0.5-to-v0.6.md) -- Terminology cut, registry schema v3, publisher changes
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Migration notes for akm v0.9.19
|
|
2
|
+
|
|
3
|
+
Nothing has to run after upgrading from 0.9.18: 0.9.19 adds no config keys,
|
|
4
|
+
changes no index layout, adds no state.db migration and leaves scheduler rows
|
|
5
|
+
alone. (0.9.18 asked nothing either. From 0.9.16 or earlier, also read the
|
|
6
|
+
0.9.17 note: akm help migrate 0.9.17.) What changes is which bundle akm
|
|
7
|
+
improve works on, how a rejected proposal is undone, what consolidation and
|
|
8
|
+
distill queue for review, and what akm proposal diff shows for a retirement.
|
|
9
|
+
|
|
10
|
+
akm improve now improves one bundle: the one it writes to, which is --bundle
|
|
11
|
+
when you pass it, else defaultWriteTarget, else your working bundle
|
|
12
|
+
(AKM_BUNDLE_DIR when set, otherwise defaultBundle). Through 0.9.18 a run also
|
|
13
|
+
picked assets out of your other writable bundles, read each one from the
|
|
14
|
+
bundle that owned it, and filed the proposal in its own write target, so an
|
|
15
|
+
asset that lived elsewhere came back either as a new copy in the write target
|
|
16
|
+
or as an edit of the write target's own copy made from the other bundle's
|
|
17
|
+
text. That no longer happens, and a rewrite filed outside the bundle that
|
|
18
|
+
owns its asset is now refused. If everything you improve lives in one bundle,
|
|
19
|
+
nothing changes for you. If you improve more than one, this is the one action
|
|
20
|
+
in the release: a scheduled akm improve, the shipped akm-improve tasks
|
|
21
|
+
included, now covers only its write target, so add one akm improve --bundle
|
|
22
|
+
run for each other bundle you want improved:
|
|
23
|
+
|
|
24
|
+
akm task add improve-team --schedule "15 3 * * *" \
|
|
25
|
+
--command "akm improve --bundle team --skip-if-locked --require-engines"
|
|
26
|
+
|
|
27
|
+
(The --bundle inside the command string is akm improve's own; the --bundle of
|
|
28
|
+
akm task add itself picks which bundle stores the task.) akm improve skills/x
|
|
29
|
+
for an asset that lives only in another bundle now fails with a not-found
|
|
30
|
+
error whose hint names that bundle, and akm improve team//skills/x works.
|
|
31
|
+
Proposals an older run already queued stay in the queue, so review them with
|
|
32
|
+
akm proposal list: one that creates an asset you already have in another
|
|
33
|
+
bundle, or overwrites this bundle's copy with the other's text, is one of
|
|
34
|
+
those. akm improve --dry-run and --plan now preview the bundle a live run
|
|
35
|
+
improves; with no --bundle or defaultWriteTarget, a dry run used to read
|
|
36
|
+
defaultBundle while a live run started from AKM_BUNDLE_DIR.
|
|
37
|
+
|
|
38
|
+
A rejection is no longer final. akm proposal reopen <id> [<id> ...] puts
|
|
39
|
+
rejected proposals back to pending and keeps each rejection in the proposal's
|
|
40
|
+
reviewHistory, which akm proposal show prints; a proposal_reopened event goes
|
|
41
|
+
into akm log. Reopen refuses a proposal that is not rejected or whose target
|
|
42
|
+
changed since it was created, by the same rule accept applies: an update needs
|
|
43
|
+
its target unchanged, a create needs the target still absent, and a retire
|
|
44
|
+
proposal needs its successor present and both documents unchanged. It also
|
|
45
|
+
refuses a retire proposal while another pending retire proposal involves
|
|
46
|
+
either of its documents. Given several ids it reopens all of them or none and
|
|
47
|
+
lists every refusal. It takes full ids, since a prefix only matches pending
|
|
48
|
+
proposals (an asset ref works too, for the newest proposal on it while none
|
|
49
|
+
is pending, but never for a retire proposal). The age that retention expiry
|
|
50
|
+
and --older-than see starts over at the reopen, so a scheduled sweep does not
|
|
51
|
+
take a proposal you just put back.
|
|
52
|
+
|
|
53
|
+
The case that matters most is consolidate's retire proposals. Through 0.9.18,
|
|
54
|
+
akm proposal diff drew a retirement as the file replaced by one blank line,
|
|
55
|
+
so a correct retirement could pass for data loss. One reviewer rejected all
|
|
56
|
+
65 of a bundle's retire proposals that way, and each rejection also kept the
|
|
57
|
+
pair pass from proposing that retirement again while both documents stayed
|
|
58
|
+
unchanged. To take them back, check each rejection's reason (akm proposal
|
|
59
|
+
list --status rejected --generator consolidate-pair --detail normal --format
|
|
60
|
+
json shows it as review.reason) and reopen only those rejected over the diff,
|
|
61
|
+
one id at a time so a refusal skips only that one (the pattern matches the
|
|
62
|
+
reason given here; change it to yours):
|
|
63
|
+
|
|
64
|
+
akm proposal list --status rejected --generator consolidate-pair \
|
|
65
|
+
--detail normal --format json \
|
|
66
|
+
| jq -r '.proposals[] | select(.review.reason // "" | test("blank line"))
|
|
67
|
+
| .id' \
|
|
68
|
+
| xargs -r -n 1 akm proposal reopen --reason "diff was misrendered"
|
|
69
|
+
|
|
70
|
+
A proposal refused because another pending retire proposal involves the same
|
|
71
|
+
document can be reopened once that one is decided. A pair whose documents
|
|
72
|
+
changed after the rejection needs no recovery, because the old rejection only
|
|
73
|
+
held while both were unchanged and the pair pass may judge it afresh. Reopened
|
|
74
|
+
retire proposals wait for you like any other. drain never accepts a retire
|
|
75
|
+
proposal, so read each with akm proposal diff and accept or reject it.
|
|
76
|
+
|
|
77
|
+
akm proposal diff now draws a retirement as one. The text output has a retire
|
|
78
|
+
header, the pair's verdict (label, reason, and continuity risk when the pair
|
|
79
|
+
was flagged), a note that accept archives the file and revert restores it byte
|
|
80
|
+
for byte, and then only the removed lines under a
|
|
81
|
+
+++ /dev/null (retired: archived; successor <ref>) line. In JSON, a retire
|
|
82
|
+
proposal's diff result gains op ("delete"), retirement (retiredRef,
|
|
83
|
+
successorRef, judgeLabel, judgeReason, cosine, and continuityRisk when flagged)
|
|
84
|
+
and note; the diff result of every other proposal is unchanged. A script that
|
|
85
|
+
matched the old "(update: ...)" header on a retire proposal should expect
|
|
86
|
+
"(retire: ...)". akm proposal show --detail full no longer ends a retire
|
|
87
|
+
proposal with an empty payload heading, and a reopened proposal's JSON gains
|
|
88
|
+
reviewHistory.
|
|
89
|
+
|
|
90
|
+
Consolidation queues fewer repeat promotions. Before it queues a memory as a
|
|
91
|
+
knowledge proposal, it compares the memory with the 20 knowledge docs in its
|
|
92
|
+
bundle nearest to it by stored vector, and skips it, with skip reason
|
|
93
|
+
dedup_covered_by_knowledge, when one of them already holds at least half of
|
|
94
|
+
the memory's distinct 5-word runs. That needs stored vectors: with semantic
|
|
95
|
+
search off the check does nothing and only the exact slug and whole-body
|
|
96
|
+
checks apply. A memory whose promotion was accepted or rejected is now offered
|
|
97
|
+
again only when its body changes (frontmatter edits do not count); it used to
|
|
98
|
+
come back at once after an accept and after 7 days after a rejection. A
|
|
99
|
+
decision an older release recorded has no body hash to compare and keeps
|
|
100
|
+
those old windows. Expect a shorter promotion queue, with the skipped
|
|
101
|
+
memories showing up as skip reasons and warnings in the run's result; to have
|
|
102
|
+
a memory considered again, edit its text.
|
|
103
|
+
|
|
104
|
+
Three changes keep a tool failure recorded as feedback from becoming a lesson
|
|
105
|
+
about the error or a TODO placeholder in a memory. Reflect's feedback caveat no
|
|
106
|
+
longer offers a TODO: verify placeholder: when feedback asks for something the
|
|
107
|
+
asset lacks, reflect is told to leave the section unchanged. TODO lines earlier
|
|
108
|
+
runs already put in your assets stay until you remove them; grep -rniE
|
|
109
|
+
"TODO:? *verify" over the bundle finds them. Distill's quality judge now also
|
|
110
|
+
scores whether a lesson is about what its source is about, and a lesson judged
|
|
111
|
+
off-subject is dropped as quality_rejected (a ledger row and a distill_invoked
|
|
112
|
+
event, no proposal) instead of passing or waiting in the queue as
|
|
113
|
+
review_needed; the judge also reads the same first 3000 characters of the
|
|
114
|
+
source body, without frontmatter, that the generator saw. The shipped agent
|
|
115
|
+
guidance changed with it: akm help agents now says to record feedback about an
|
|
116
|
+
asset's content, that it helped or turned out wrong, stale or unhelpful, and
|
|
117
|
+
not a failed akm command such as akm show erroring, and akm feedback --help
|
|
118
|
+
says the same of --reason. If you pasted that guide into an AGENTS.md or a
|
|
119
|
+
system prompt, regenerate the block: the old text told agents to record
|
|
120
|
+
--negative "when it fails".
|
|
121
|
+
|
|
122
|
+
Downgrading to 0.9.18 needs no data change, since no schema moved, but it
|
|
123
|
+
brings some of this back. 0.9.18 does not know reviewHistory: a proposal
|
|
124
|
+
reopened under 0.9.19 reads as an ordinary pending proposal, and 0.9.18 drops
|
|
125
|
+
its history if it rewrites the row (accepting or rejecting it, say). It also
|
|
126
|
+
counts a pending proposal's age from its creation, so a reopened proposal can
|
|
127
|
+
be expired by the next akm improve run (retire proposals never expire) or
|
|
128
|
+
swept by a scheduled accept, reject or drain with --older-than. It does not
|
|
129
|
+
hold a promoted memory either: 0.9.19 records a decided promotion with the
|
|
130
|
+
memory's body hash and no retry time, which 0.9.18 reads as eligible now, so
|
|
131
|
+
those memories go back to consolidation on the next run, a rejected one
|
|
132
|
+
sooner than the 7 days 0.9.18 would have waited. Its improve plans assets
|
|
133
|
+
from every writable bundle again, and its akm proposal diff draws a
|
|
134
|
+
retirement as a blank replacement again.
|
|
@@ -7,6 +7,11 @@ live one level up in `docs/migration/`.
|
|
|
7
7
|
|
|
8
8
|
## Available notes
|
|
9
9
|
|
|
10
|
+
- [0.9.19](0.9.19.md) — `akm improve` planning only the bundle it writes to
|
|
11
|
+
(a scheduled run needs one `--bundle` run per other bundle),
|
|
12
|
+
`akm proposal reopen` and the retire-proposal diff, consolidate's coverage
|
|
13
|
+
check and content-driven hold on decided promotions, distill's grounding
|
|
14
|
+
check, and what a downgrade to 0.9.18 undoes
|
|
10
15
|
- [0.9.17](0.9.17.md) — consolidate's retire proposals and continuity check,
|
|
11
16
|
promotions archiving their source memory, declared links replacing the LLM
|
|
12
17
|
entity graph, index layout 26, scheduler rows carrying their own context,
|
|
@@ -140,7 +140,11 @@ paths (same-filesystem rename, or copy-then-delete across filesystems). A
|
|
|
140
140
|
lock file is only ever deleted once the same staleness check `akm improve`
|
|
141
141
|
itself uses says its holder is dead; a lock a live run still holds (or one
|
|
142
142
|
this process cannot read) is left in place and reported instead. The whole
|
|
143
|
-
step is idempotent — a second run reports nothing pending.
|
|
143
|
+
step is idempotent — a second run reports nothing pending. 0.9.17-alpha.4
|
|
144
|
+
removed that step: `akm migrate` no longer relocates these files, and one left
|
|
145
|
+
at an old path is inert (nothing reads it). Two of the five writers no longer
|
|
146
|
+
exist either — the improve ledger replaced `distill-rejected/`, and the
|
|
147
|
+
write-only `eval-cases/` path was removed.
|
|
144
148
|
`$STASH/.akm/memory-cleanup/` did not move; it is the one confirmed exception
|
|
145
149
|
to the rule (see Storage locations, above).
|
|
146
150
|
|