akm-cli 0.10.26101002-alpha → 0.10.26101201-alpha
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 +108 -0
- package/dist/assets/hints/cli-hints-full.md +2 -2
- package/dist/assets/hints/cli-hints-short.md +1 -1
- package/dist/assets/stash-skeleton/README.md +1 -1
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +1 -1
- package/dist/commands/feedback-cli.js +75 -49
- package/dist/commands/improve/consolidate/chunking.js +3 -10
- package/dist/commands/improve/consolidate/continuity-check.js +4 -4
- package/dist/commands/improve/consolidate/pair-pass.js +22 -9
- package/dist/commands/improve/consolidate.js +4 -30
- package/dist/commands/improve/distill.js +8 -1
- package/dist/commands/improve/extract-prompt.js +17 -7
- package/dist/commands/improve/extract.js +5 -7
- package/dist/commands/improve/improve-cli.js +5 -4
- package/dist/commands/improve/improve.js +132 -50
- package/dist/commands/improve/loop-stages.js +18 -4
- package/dist/commands/improve/reflect.js +26 -26
- package/dist/commands/improve/retrieval-queries.js +37 -0
- package/dist/commands/improve/session-asset.js +4 -1
- package/dist/commands/improve/stage.js +9 -3
- package/dist/commands/proposal/drain.js +4 -0
- package/dist/commands/proposal/repository.js +17 -11
- package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -3
- package/dist/commands/sources/stash-cli.js +2 -2
- package/dist/core/config/schema/improve-processes.js +2 -2
- package/dist/core/file-lock.js +2 -6
- package/dist/core/improve-result.js +2 -0
- package/dist/core/paths.js +4 -0
- package/dist/core/run-lock.js +1 -1
- package/dist/indexer/indexer.js +15 -6
- package/dist/indexer/passes/memory-inference.js +15 -6
- package/dist/indexer/search/db-search.js +45 -17
- package/dist/indexer/search/ranking.js +29 -1
- package/dist/indexer/usage/usage-events.js +21 -16
- package/dist/scripts/akm-migrate-node.js +2 -4
- package/dist/scripts/akm-migrate.js +2 -4
- package/dist/sources/providers/git-stash.js +94 -13
- package/dist/storage/repositories/index-utility-repository.js +2 -32
- package/dist/storage/repositories/index-vec-repository.js +4 -0
- package/dist/tasks/run/run-command-task.js +9 -1
- package/dist/tasks/run/run-workflow-task.js +4 -3
- package/dist/tasks/run/task-log.js +38 -1
- package/docs/architecture/0.10-process-rules.md +827 -0
- package/docs/architecture/improvement.md +66 -58
- package/docs/architecture/internals/improve-workflow.md +8 -9
- package/docs/architecture/internals/storage-locations.md +1 -1
- package/docs/architecture/testing/manual-testing-checklist.md +1 -1
- package/docs/guides/bundles.md +3 -1
- package/docs/guides/improve-the-library.md +3 -2
- package/docs/guides/scheduling.md +9 -1
- package/docs/reference/cli.md +32 -18
- package/docs/reference/configuration.md +2 -2
- package/docs/reference/data-and-telemetry.md +1 -1
- package/package.json +1 -1
- package/dist/assets/prompts/retrieval-relevance-judge.md +0 -6
- package/dist/commands/improve/retrieval-gate.js +0 -132
- package/dist/indexer/feedback/utility-policy.js +0 -75
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,114 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.10.26101201] - 2026-10-12
|
|
10
|
+
|
|
11
|
+
### Removed
|
|
12
|
+
|
|
13
|
+
- **Reflect's retrieval regression gate (#1171).** After the quality judge passed a revision, reflect graded the old and the new text against up to five of the asset's own retrieval queries and refused the revision (`quality_rejected`, a 14-day window) when the mean grade fell at all. Re-measured on 0.10 it refused 12-17% of the revisions the judge had passed and lowered defects fixed from 84 to 77 (35B) and from 92 to 77 (27B) of 96, now that reflect edits frontmatter only. A judge-passed revision is now minted as a proposal with no further check. This also removes the `retrieval-ungraded` review reason, the `retrievalRegression`/`retrievalUngraded` event metadata and the `retrieval-relevance-judge.md` prompt. No config key existed for the gate, so no config changes.
|
|
14
|
+
|
|
15
|
+
## [0.10.26101101] - 2026-10-11
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **A manual fix to an asset applies at once.** `akm feedback --negative ... --replace/--with/--source`, `--superseded-by` and `--outdated` used to queue a `feedback` proposal that waited for `akm proposal accept` and expired after 90 days. The command now files the proposal and accepts it in the same run, through the ordinary accept (a git-backed bundle gets the usual commit; `akm proposal revert` undoes it), with gate decision `manual-fix` and no judge. If the accept refuses, the proposal is rejected, the file is untouched and the command fails. The edited asset stays eligible for every improve process.
|
|
20
|
+
- **`akm improve` syncs before it runs, not only after (#1091).** A live run on a git-backed bundle now does the same
|
|
21
|
+
fetch / fast-forward-or-merge / commit / push as `akm sync` right after taking its lock and before the index and plan
|
|
22
|
+
stages, so it plans from what other hosts pushed. It honours `--no-sync` and `--no-push`. A conflict or fetch failure
|
|
23
|
+
warns and the run continues on the local state; the outcome is `preRunSync` on the result and a `stash_synced` event
|
|
24
|
+
with `phase: "pre-run"`. Pending hand edits in the managed paths are committed by it, as `akm sync` would.
|
|
25
|
+
- **Reflect no longer locks an asset for 14 days over an uncertain verdict; it asks a person.** A reflect revision the
|
|
26
|
+
quality judge does not pass but scores 3.5 or more (the judge's own review label begins at 2.5) is now queued as a
|
|
27
|
+
pending proposal for a person (`judge-uncertain`, with the per-criterion scores and the judge's reason) and records
|
|
28
|
+
no `quality_rejected` window; a lower score is refused as before. Likewise a retrieval check that cannot grade a
|
|
29
|
+
revision (transport error, timeout, unreadable reply) queues it for a person (`retrieval-ungraded`) instead of
|
|
30
|
+
refusing it as a regression, and a distill lesson or promotion whose judge is missing at judge time is queued for
|
|
31
|
+
review instead of recording a 30-day `quality_rejected` window. Triage never accepts any of these. `akm proposal
|
|
32
|
+
list` shows the reason as `gate=deferred:<reason>`.
|
|
33
|
+
- **Feedback now changes `akm search` and `akm curate` ranking, as `akm feedback` always said it did.** The fused
|
|
34
|
+
score is scaled by `1 + 0.03 x (ln(1 + positives) - ln(1 + negatives))`, counting the user's recorded feedback
|
|
35
|
+
events for the asset (improve, task and audit feedback does not count). The rule is the same for every asset, rises
|
|
36
|
+
with each positive and falls with each negative, and gains less per vote as votes pile up: about +2% / +3% / +5% /
|
|
37
|
+
+9% for 1 / 2 / 5 / 20 positives, so an asset with 5 positives outranks one with 2, and the effect reorders close
|
|
38
|
+
matches without overriding relevance. The counts are read from `state.db` in one query per search. `utility_scores`
|
|
39
|
+
is still written and read by improve and metrics, but search does not use it.
|
|
40
|
+
- **`akm feedback` reports the real ranking effect.** `rankingUpdate.applied` in its output used to mean the utility
|
|
41
|
+
EMA write succeeded, which no longer has anything to do with ranking. It is now `true` exactly when the recorded
|
|
42
|
+
event is user-attributed (the events search counts) and `false` with a reason for improve, task, audit or unknown
|
|
43
|
+
attribution; a utility-write failure only warns and no longer flips it.
|
|
44
|
+
- **Utility has one formula and one implementation.** `akm feedback` used to move utility with its own bounded step
|
|
45
|
+
toward the cumulative positive share while `akm index` recomputed the same table with a different formula, so the
|
|
46
|
+
stored value depended on which ran last. `akm feedback` now calls the indexer's `recomputeUtilityScores` for the one
|
|
47
|
+
entry (user-attributed events only), so the value it stores is the one `akm index` would compute at that moment; the
|
|
48
|
+
bounded-step code (`utility-policy.ts`) is removed. A first positive on an entry with no utility row now gives 0.3
|
|
49
|
+
(the old start was 0.5). Search still does not read utility.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- **The 02:00 nightly `akm improve` can no longer lose its night to the consolidate task's lock.** The shipped
|
|
54
|
+
`akm-improve-consolidate` task (`20 */4 * * *`) kept the default 2-hour budget, so a 00:20 run could still hold
|
|
55
|
+
`improve.lock` at 02:00 and the nightly (deliberately without `--skip-if-locked`) failed with `IMPROVE_LOCK_HELD`.
|
|
56
|
+
The consolidate task now passes `--timeout-ms 3600000`; the nightly is unchanged.
|
|
57
|
+
- **Scheduled command tasks (`uses: commands/…`) get the 6-hour unattended default timeout.** Only workflow and native
|
|
58
|
+
shell/script tasks had it, so a wedged engine call could hold a command task forever. An explicit task `timeout:`,
|
|
59
|
+
an engine timeout, or `timeout: null` still wins.
|
|
60
|
+
- **`akm search` no longer warns "no embedding provider is configured" on a default install.** With
|
|
61
|
+
`semanticSearchMode: auto` and no embedding settings, search uses the built-in local model, so the warning was
|
|
62
|
+
false on every query. It now fires only for a remote `embedding.endpoint` set without `embedding.model` (the one
|
|
63
|
+
config that cannot work), and the remedy names that field.
|
|
64
|
+
- **`akm sync` fetches the remote and brings other hosts' changes in before it pushes.** It used to check the stale
|
|
65
|
+
local tracking ref, so once another host had pushed, the lease push failed and `akm sync` exited 70 instead of
|
|
66
|
+
committing, and a two-host filesystem bundle never converged. A writable bundle with a remote now fetches, then
|
|
67
|
+
fast-forwards (or merges with git's default merge) the upstream, then pushes; a host with nothing to commit still
|
|
68
|
+
receives the upstream. A merge that conflicts is aborted, your commit stays, the push is skipped, and
|
|
69
|
+
`reason` reads `not pushed: merging upstream conflicts in <files> ...`. A failed fetch likewise only skips the push.
|
|
70
|
+
`--no-push` and read-only bundles never touch the network.
|
|
71
|
+
- **The end-of-run improve sync reports `committed: true` when only the push failed.** The commit had landed, but the
|
|
72
|
+
result and the `stash_synced` event said `committed: false`.
|
|
73
|
+
- **The extract per-session lock no longer reclaims a live holder.** It was reclaimed once its file was five minutes
|
|
74
|
+
old, but one session can hold it across two extraction attempts and the summary call, so a session-end hook racing
|
|
75
|
+
the improve run processed the session twice. Only a lock whose holder process is dead is reclaimed, as with the
|
|
76
|
+
improve run lock.
|
|
77
|
+
- **Memory inference writes to the default bundle, not the first bundle in the list.** The pass took `sources[0]`, so
|
|
78
|
+
it relied on list order and did nothing, silently, when that entry was not an akm bundle. It now selects the source
|
|
79
|
+
flagged as the default bundle and warns when there is none or when it is not an akm bundle.
|
|
80
|
+
- **Memory inference no longer reverts a parent memory's frontmatter edit.** The `inferenceProcessed` stamp rewrote
|
|
81
|
+
frontmatter parsed before the model call, so an edit made to the memory in the meantime (accepted proposal,
|
|
82
|
+
feedback hash) was lost. The stamp now adds its key to the file's current frontmatter.
|
|
83
|
+
- **The session summary prompt fences the transcript as untrusted.** Extract's summary call sent the raw, unfiltered
|
|
84
|
+
event stream with no boundary, so a crafted session could steer the text written into the session asset. It now
|
|
85
|
+
uses the same pre-filter and the same `UNTRUSTED SESSION TRANSCRIPT` fence, with forged fence markers neutralised,
|
|
86
|
+
as the extraction call.
|
|
87
|
+
- **`akm proposal revert` of a retirement still works after the 30-day archive purge.** When the graced purge has
|
|
88
|
+
deleted the archived copy, the primary is restored byte for byte from the proposal's recorded `backupContent`
|
|
89
|
+
instead of the revert refusing. A file that now occupies the path is still never overwritten.
|
|
90
|
+
- **Retire proposals can no longer become permanent.** They now expire by age like every other proposal (`akm proposal
|
|
91
|
+
reopen` brings one back, so the old "never expires" rationale is gone), a staged one whose file or successor is gone
|
|
92
|
+
or changed is auto-rejected once as `stale-target` instead of failing every drain run, and an expired or stale-target
|
|
93
|
+
retire proposal no longer counts as a person's rejection that settles its pair.
|
|
94
|
+
- **The consolidate pair pass no longer marks assets judged when they have no stored embedding.** With
|
|
95
|
+
`semanticSearchMode: off` (or before an asset is embedded) the neighbour query could not run, yet the pass wrote a
|
|
96
|
+
`judged_no_action` row that held the asset until its body changed. It now writes no row for such an asset, so
|
|
97
|
+
enabling embeddings reconsiders it, and warns once per run that the pair pass needs stored embeddings.
|
|
98
|
+
- **A transient reflect failure no longer consumes the feedback that planned it.** A transport error, parse error,
|
|
99
|
+
timeout, budget abort or crash wrote a `failed` ledger row that advanced the signal cursor, so the ref was not
|
|
100
|
+
retried until new feedback arrived. Like distill, these now write no row; guard rejections and quality rejections
|
|
101
|
+
are unchanged.
|
|
102
|
+
- **The per-run `akm improve` and `akm index` log files are now purged by the retention pass.**
|
|
103
|
+
`$CACHE/logs/improve/<ISO>.log` and `$CACHE/logs/index/<ISO>.log` were outside the purge and grew without bound;
|
|
104
|
+
they now follow `improve.eventRetentionDays` like the task log files.
|
|
105
|
+
- **A run hard-killed by the budget watchdog now leaves a record and banks its writes.** The forced `process.exit(0)`
|
|
106
|
+
(exit code unchanged) skipped every finally block, so the run left no `improve_runs` row and the files it had
|
|
107
|
+
written were never committed. Before exiting, the watchdog now records the run as terminated
|
|
108
|
+
(`budget_exhausted`) and makes the same provenance-scoped auto-sync commit, without pushing.
|
|
109
|
+
|
|
110
|
+
### Changed
|
|
111
|
+
|
|
112
|
+
- **`captureMode: hot` memories are consolidated like any other memory.** The pair pass may now retire one, the promote
|
|
113
|
+
pass may promote one (accepting archives the source, as for any memory), and a chunk of only hot memories goes to the
|
|
114
|
+
model instead of being skipped as judged no-action. The hot-specific retire guard, chunk skip and promote-prompt
|
|
115
|
+
annotation are gone; `akm remember` still writes `captureMode: hot`.
|
|
116
|
+
|
|
9
117
|
## [0.10.26101002] - 2026-10-10
|
|
10
118
|
|
|
11
119
|
### Changed
|
|
@@ -95,7 +95,7 @@ akm lint --type workflows # Parse and compile every workflo
|
|
|
95
95
|
akm workflow run workflows/ship-release # Start or resume and execute the workflow
|
|
96
96
|
akm feedback skills/code-review --positive # Record that an asset helped (ranks it higher; no rewrite)
|
|
97
97
|
akm feedback agents/reviewer --negative --reason "says to run jest; the suite runs on vitest" # Content wrong or stale: lowers its ranking; improve may repair its frontmatter
|
|
98
|
-
akm feedback knowledge/opencode-server --negative --reason "the default port is 4096, not 8000" --replace "port 8000" --with "port 4096" --source "https://opencode.ai/docs/server/" #
|
|
98
|
+
akm feedback knowledge/opencode-server --negative --reason "the default port is 4096, not 8000" --replace "port 8000" --with "port 4096" --source "https://opencode.ai/docs/server/" # Apply an exact fix
|
|
99
99
|
akm feedback memories/deployment-notes --positive # Works for memories too
|
|
100
100
|
akm feedback env/prod --positive # Records env feedback without surfacing values
|
|
101
101
|
```
|
|
@@ -110,7 +110,7 @@ its description, title or `when_to_use` from your reason. A note that simply did
|
|
|
110
110
|
not fit your task is not negative feedback: record nothing for it. Improve does
|
|
111
111
|
not rewrite an asset's text: once you have verified the correct fact, attach the
|
|
112
112
|
exact fix with `--replace "<exact current text>" --with "<corrected text>"
|
|
113
|
-
--source "<URL, command or file>"`, which akm checks and
|
|
113
|
+
--source "<URL, command or file>"`, which akm checks and applies at once.
|
|
114
114
|
A note that is outdated can be marked with `--outdated`, or
|
|
115
115
|
`--superseded-by <ref>` when another note replaces it.
|
|
116
116
|
`--positive` records that an asset helped (it raises its ranking) and does not
|
|
@@ -41,7 +41,7 @@ akm proposal accept 7c115132 # Accept by UUID prefix
|
|
|
41
41
|
akm proposal reject skills/my-skill --reason "..." # Reject by ref
|
|
42
42
|
akm feedback <ref> --positive # Record that an asset helped (ranks it higher; no rewrite)
|
|
43
43
|
akm feedback <ref> --negative --reason "..." # Content wrong or stale: lowers its ranking; improve may repair its frontmatter
|
|
44
|
-
akm feedback <ref> --negative --reason "..." --replace "<exact text>" --with "<fix>" --source "<url|cmd|file>" #
|
|
44
|
+
akm feedback <ref> --negative --reason "..." --replace "<exact text>" --with "<fix>" --source "<url|cmd|file>" # Apply an exact fix to its text
|
|
45
45
|
akm bundle add <ref> # Add a source (npm, GitHub, git, local dir)
|
|
46
46
|
akm clone <ref> # Copy an asset to the working bundle (optional --dest arg to clone to specific location)
|
|
47
47
|
akm sync # Commit (and push if writable remote) changes in the primary bundle (--no-push to commit only)
|
|
@@ -85,7 +85,7 @@ akm feedback <ref> --positive
|
|
|
85
85
|
# A note that simply did not fit your task gets no negative feedback.
|
|
86
86
|
akm feedback <ref> --negative --reason "<what is wrong and what should change>"
|
|
87
87
|
|
|
88
|
-
# Once you have verified the correct fact, attach the exact fix: it is checked and
|
|
88
|
+
# Once you have verified the correct fact, attach the exact fix: it is checked and applied at once
|
|
89
89
|
akm feedback <ref> --negative --reason "<what is wrong>" --replace "<exact current text>" --with "<corrected text>" --source "<URL, command or file>"
|
|
90
90
|
|
|
91
91
|
# Capture a durable lesson or memory from the current session
|
|
@@ -21,15 +21,17 @@ import { getDbPath } from "../core/paths.js";
|
|
|
21
21
|
import { withStateDb } from "../core/state-db.js";
|
|
22
22
|
import { warn } from "../core/warn.js";
|
|
23
23
|
import { withWriteTargetMutation } from "../core/write-source.js";
|
|
24
|
+
import { recomputeUtilityScores } from "../indexer/indexer.js";
|
|
24
25
|
import { resolveSourceEntries } from "../indexer/search/search-source.js";
|
|
25
|
-
import {
|
|
26
|
+
import { insertUsageEvent, resolveUsageEventSource } from "../indexer/usage/usage-events.js";
|
|
26
27
|
import { resolveSourcesForOrigin } from "../registry/origin-resolve.js";
|
|
27
28
|
import { closeDatabase, openExistingDatabase } from "../storage/repositories/index-connection.js";
|
|
28
29
|
import { findEntryIdByRef, getEntryFilePathById, getItemRefById, } from "../storage/repositories/index-entries-repository.js";
|
|
29
|
-
import {
|
|
30
|
+
import { getUtilityScore } from "../storage/repositories/index-utility-repository.js";
|
|
30
31
|
import { contentHash } from "./improve/content-hash.js";
|
|
31
32
|
import { readEdgeList } from "./improve/memory/memory-belief.js";
|
|
32
|
-
import {
|
|
33
|
+
import { akmProposalAccept } from "./proposal/proposal.js";
|
|
34
|
+
import { createProposal, recordGateDecision, rejectProposalDurably } from "./proposal/repository.js";
|
|
33
35
|
// ── Tag validation ────────────────────────────────────────────────────────────
|
|
34
36
|
const TAG_KEY_RE = /^[a-z_][a-z0-9_]*$/;
|
|
35
37
|
function validateFeedbackTags(raw) {
|
|
@@ -144,26 +146,25 @@ function buildLessonStrengthUpdate(rawBytes, feedbackRef) {
|
|
|
144
146
|
const nextBytes = Buffer.from(next);
|
|
145
147
|
return rawBytes.equals(nextBytes) ? { strength: strengthList.length } : { strength: strengthList.length, nextBytes };
|
|
146
148
|
}
|
|
149
|
+
/** Utility at or above this is "high"; falling below it after feedback emits `improve_review_needed`. */
|
|
150
|
+
const UTILITY_REVIEW_THRESHOLD = 0.5;
|
|
147
151
|
/**
|
|
148
|
-
* Persist the feedback usage-event (state.db) and
|
|
149
|
-
* entry's
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
* and
|
|
160
|
-
* caller rather than passing silently. Best-effort: a utility-update failure
|
|
161
|
-
* never fails the feedback record.
|
|
152
|
+
* Persist the feedback usage-event (state.db) and, for a user-attributed event,
|
|
153
|
+
* recompute the entry's utility score (index.db) with the same function the
|
|
154
|
+
* `akm index` finalize pass uses, scoped to this one entry, so there is one
|
|
155
|
+
* formula and the stored value is what the next index would compute. Chunk-8
|
|
156
|
+
* WI-8.3: usage_events lives in state.db; entries + utility_scores stay in `indexDb`.
|
|
157
|
+
* Search ranking does not read the utility score: it counts the recorded
|
|
158
|
+
* `source='user'` feedback events itself, so a user-attributed event affects
|
|
159
|
+
* ranking at once and an `improve`/`task`/`audit`/`unknown` one never does — an
|
|
160
|
+
* anti-self-reinforcement guard that stops an agent or automated pipeline from
|
|
161
|
+
* boosting its own picks. This is by design and must not be removed. The
|
|
162
|
+
* utility score is read by improve and metrics only; its update is best-effort
|
|
163
|
+
* and its failure never fails the feedback record or changes the ranking effect.
|
|
162
164
|
*/
|
|
163
165
|
function recordFeedbackUsage(indexDb, entryId, durableEntryRef, signal, metadataStr) {
|
|
164
166
|
let utilityResult;
|
|
165
|
-
let
|
|
166
|
-
let rankingUpdateSkippedReason;
|
|
167
|
+
let rankingSkippedReason;
|
|
167
168
|
const eventSource = resolveUsageEventSource();
|
|
168
169
|
withStateDb((stateDb) => {
|
|
169
170
|
insertUsageEvent(stateDb, {
|
|
@@ -175,22 +176,22 @@ function recordFeedbackUsage(indexDb, entryId, durableEntryRef, signal, metadata
|
|
|
175
176
|
source: eventSource,
|
|
176
177
|
});
|
|
177
178
|
if (eventSource !== "user") {
|
|
178
|
-
|
|
179
|
-
`feedback source is "${eventSource}", not "user" —
|
|
180
|
-
"feedback (anti-self-reinforcement guard; set AKM_EVENT_SOURCE=user to record as user demand).";
|
|
179
|
+
rankingSkippedReason =
|
|
180
|
+
`feedback source is "${eventSource}", not "user" — it does not count toward search ranking, which counts ` +
|
|
181
|
+
"user-attributed feedback only (anti-self-reinforcement guard; set AKM_EVENT_SOURCE=user to record as user demand).";
|
|
181
182
|
return;
|
|
182
183
|
}
|
|
183
184
|
try {
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
185
|
+
const previousUtility = getUtilityScore(indexDb, entryId)?.utility ?? 0;
|
|
186
|
+
recomputeUtilityScores(indexDb, stateDb, entryId);
|
|
187
|
+
utilityResult = { previousUtility, nextUtility: getUtilityScore(indexDb, entryId)?.utility ?? previousUtility };
|
|
187
188
|
}
|
|
188
189
|
catch (err) {
|
|
189
|
-
// best-effort —
|
|
190
|
-
|
|
190
|
+
// best-effort — the event is recorded (and ranked) even if the utility update fails
|
|
191
|
+
warn(`[feedback] utility update failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
191
192
|
}
|
|
192
193
|
});
|
|
193
|
-
return { utilityResult,
|
|
194
|
+
return { utilityResult, countsTowardRanking: eventSource === "user", rankingSkippedReason };
|
|
194
195
|
}
|
|
195
196
|
// ── Judged text ──────────────────────────────────────────────────────────────
|
|
196
197
|
/**
|
|
@@ -416,15 +417,20 @@ export const feedbackCommand = defineJsonCommand({
|
|
|
416
417
|
'the exact fix: --replace "<exact current text>" --with "<corrected text>" --source "<URL,\n' +
|
|
417
418
|
'command or file that shows it>" (repeat --replace/--with for several edits; use --with=...\n' +
|
|
418
419
|
"for a value that starts with -). akm checks that each --replace text appears exactly once\n" +
|
|
419
|
-
"and
|
|
420
|
+
"and applies the edit at once, with no review (`akm proposal revert` undoes it). To mark\n" +
|
|
421
|
+
"the asset's history instead, add\n" +
|
|
420
422
|
"--superseded-by <ref> (the asset that replaces it) or --outdated (it describes a past\n" +
|
|
421
|
-
"state and nothing replaces it), with --reason and --source: the same
|
|
423
|
+
"state and nothing replaces it), with --reason and --source: the same edit sets its\n" +
|
|
422
424
|
"beliefState and, for --superseded-by, adds the ref to supersededBy. `--positive` records\n" +
|
|
423
425
|
"that an asset helped (it raises its ranking) and does not trigger a rewrite.\n\n" +
|
|
424
|
-
"
|
|
425
|
-
"
|
|
426
|
-
"
|
|
427
|
-
"
|
|
426
|
+
"Feedback changes `akm search` and `akm curate` ranking starting with the very\n" +
|
|
427
|
+
"next search; no reindex is needed. Each recorded positive raises the asset's\n" +
|
|
428
|
+
"score and each negative lowers it, the same way for every asset: the more\n" +
|
|
429
|
+
"positives, the higher it ranks (5 outrank 2), and each further vote counts\n" +
|
|
430
|
+
"for a little less. The effect is small, so it reorders close matches and\n" +
|
|
431
|
+
"does not override relevance. Only feedback recorded as the user counts, not\n" +
|
|
432
|
+
"feedback from improve, task or audit runs. Feedback also moves the utility\n" +
|
|
433
|
+
"score that improve and `akm metrics` read.",
|
|
428
434
|
},
|
|
429
435
|
args: {
|
|
430
436
|
// Optional in citty so run() is invoked even when omitted; we re-validate
|
|
@@ -565,8 +571,8 @@ export const feedbackCommand = defineJsonCommand({
|
|
|
565
571
|
// background reindex it spawned — now that ensureIndex is removed, holding
|
|
566
572
|
// the lock only causes feedback to block for the full improve run duration.
|
|
567
573
|
let utilityResult;
|
|
568
|
-
let
|
|
569
|
-
let
|
|
574
|
+
let countsTowardRanking = false;
|
|
575
|
+
let rankingSkippedReason;
|
|
570
576
|
let durableRef = ref;
|
|
571
577
|
let fix;
|
|
572
578
|
const db = openExistingDatabase();
|
|
@@ -586,10 +592,10 @@ export const feedbackCommand = defineJsonCommand({
|
|
|
586
592
|
throw new NotFoundError(`Ref "${ref}" is not in the index. ` +
|
|
587
593
|
"Run 'akm search' to verify the asset exists, then 'akm index' if it was recently added.", "ASSET_NOT_FOUND");
|
|
588
594
|
}
|
|
589
|
-
// Persist the feedback signal into usage_events.
|
|
590
|
-
//
|
|
591
|
-
//
|
|
592
|
-
//
|
|
595
|
+
// Persist the feedback signal into usage_events. A user-attributed signal
|
|
596
|
+
// also recomputes the entry's utility at once (see recordFeedbackUsage).
|
|
597
|
+
// Search ranking reads the recorded events directly, so no `akm index`
|
|
598
|
+
// run is needed.
|
|
593
599
|
// WI-8.5b: the `feedback` / `improve_review_needed` events key on the
|
|
594
600
|
// resolved entry's fully-qualified item_ref — the SAME durable key the
|
|
595
601
|
// usage_events row carries and the SAME spelling the signal-delta
|
|
@@ -645,8 +651,8 @@ export const feedbackCommand = defineJsonCommand({
|
|
|
645
651
|
const metadataStr = Object.keys(metadataObj).length > 1 ? JSON.stringify(metadataObj) : undefined;
|
|
646
652
|
const recordResult = recordFeedbackUsage(db, entryId, itemRef, signal, metadataStr);
|
|
647
653
|
utilityResult = recordResult.utilityResult;
|
|
648
|
-
|
|
649
|
-
|
|
654
|
+
countsTowardRanking = recordResult.countsTowardRanking;
|
|
655
|
+
rankingSkippedReason = recordResult.rankingSkippedReason;
|
|
650
656
|
}
|
|
651
657
|
finally {
|
|
652
658
|
closeDatabase(db);
|
|
@@ -656,16 +662,34 @@ export const feedbackCommand = defineJsonCommand({
|
|
|
656
662
|
ref: durableRef,
|
|
657
663
|
metadata: metadataObj,
|
|
658
664
|
});
|
|
659
|
-
|
|
660
|
-
|
|
665
|
+
// A person typed the fix, so it is applied now: the proposal is minted for the audit trail, `akm proposal
|
|
666
|
+
// revert` and reflect's "already acted on" rule, then accepted through the ordinary accept path (the same
|
|
667
|
+
// checks and git commit). If that refuses, the proposal is rejected and the command fails.
|
|
668
|
+
let fixProposal;
|
|
669
|
+
if (fix && reason?.trim() && fixSource) {
|
|
670
|
+
const stashDir = resolveStashDir();
|
|
671
|
+
const minted = createProposal(stashDir, {
|
|
661
672
|
ref: durableRef,
|
|
662
673
|
itemRef: durableRef,
|
|
663
674
|
target: fix.target,
|
|
664
675
|
source: "feedback",
|
|
665
676
|
payload: { content: fix.content },
|
|
666
677
|
feedback: { reason: reason.trim(), source: fixSource },
|
|
667
|
-
})
|
|
668
|
-
|
|
678
|
+
});
|
|
679
|
+
try {
|
|
680
|
+
recordGateDecision(stashDir, minted.id, { outcome: "auto-accepted", reason: "manual-fix", gate: "feedback" });
|
|
681
|
+
fixProposal = (await akmProposalAccept({ stashDir, id: minted.id })).proposal;
|
|
682
|
+
}
|
|
683
|
+
catch (err) {
|
|
684
|
+
try {
|
|
685
|
+
rejectProposalDurably(stashDir, minted.id, `manual fix not applied: ${err instanceof Error ? err.message : String(err)}`);
|
|
686
|
+
}
|
|
687
|
+
catch (rejectErr) {
|
|
688
|
+
warn(`[feedback] could not reject proposal ${minted.id}: ${rejectErr instanceof Error ? rejectErr.message : String(rejectErr)}`);
|
|
689
|
+
}
|
|
690
|
+
throw err;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
669
693
|
// F-5 / #386: When a high-utility asset crosses below the review threshold,
|
|
670
694
|
// auto-create a review-needed escalation proposal so a human can confirm
|
|
671
695
|
// whether the negative feedback is valid before the asset falls out of
|
|
@@ -674,7 +698,9 @@ export const feedbackCommand = defineJsonCommand({
|
|
|
674
698
|
// Emit a structured event rather than a proposal so the review-needed
|
|
675
699
|
// signal doesn't risk an accidental asset overwrite if the proposal is
|
|
676
700
|
// accepted.
|
|
677
|
-
if (utilityResult
|
|
701
|
+
if (utilityResult &&
|
|
702
|
+
utilityResult.previousUtility >= UTILITY_REVIEW_THRESHOLD &&
|
|
703
|
+
utilityResult.nextUtility < UTILITY_REVIEW_THRESHOLD) {
|
|
678
704
|
try {
|
|
679
705
|
appendEvent({
|
|
680
706
|
eventType: "improve_review_needed",
|
|
@@ -729,9 +755,9 @@ export const feedbackCommand = defineJsonCommand({
|
|
|
729
755
|
signal,
|
|
730
756
|
reason: reason?.trim() ?? null,
|
|
731
757
|
tags: validatedTags,
|
|
732
|
-
rankingUpdate:
|
|
758
|
+
rankingUpdate: countsTowardRanking
|
|
733
759
|
? { applied: true }
|
|
734
|
-
: { applied: false, reason:
|
|
760
|
+
: { applied: false, reason: rankingSkippedReason ?? "unknown" },
|
|
735
761
|
...(fixProposal
|
|
736
762
|
? {
|
|
737
763
|
fix: {
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
/** Consolidate's chunk sizing and per-chunk prompt (pure token math and text). */
|
|
5
5
|
import fs from "node:fs";
|
|
6
|
-
import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
|
|
7
6
|
import { contentHash, stripFrontmatterBody } from "../content-hash.js";
|
|
8
7
|
/** A conservative chars-per-token estimate, shared by every context-length budget. */
|
|
9
8
|
export const CHARS_PER_TOKEN = 3;
|
|
@@ -24,9 +23,8 @@ export function computeSafeChunkSize(contextLength, bodyTruncation, maxChunkSize
|
|
|
24
23
|
/**
|
|
25
24
|
* One chunk's user prompt. Each memory shows its body only (frontmatter can
|
|
26
25
|
* outrun the truncation and leave the model judging metadata), marked
|
|
27
|
-
* `
|
|
28
|
-
*
|
|
29
|
-
* prompt forbids promoting those.
|
|
26
|
+
* `already queued` when a pending consolidate proposal carries its body hash —
|
|
27
|
+
* the system prompt forbids promoting those.
|
|
30
28
|
*/
|
|
31
29
|
export function buildChunkPrompt(sourceName, memories, chunkIndex, totalChunks, bodyTruncation, pendingProposalBodyHashes = new Set()) {
|
|
32
30
|
const lines = [
|
|
@@ -42,12 +40,7 @@ export function buildChunkPrompt(sourceName, memories, chunkIndex, totalChunks,
|
|
|
42
40
|
catch {
|
|
43
41
|
body = "(unreadable)";
|
|
44
42
|
}
|
|
45
|
-
const
|
|
46
|
-
if (parseFrontmatter(body).data.captureMode === "hot")
|
|
47
|
-
annotations.push("captureMode: hot");
|
|
48
|
-
if (pendingProposalBodyHashes.has(contentHash(body, "body")))
|
|
49
|
-
annotations.push("already queued");
|
|
50
|
-
const annotationSuffix = annotations.length > 0 ? ` (${annotations.join("; ")})` : "";
|
|
43
|
+
const annotationSuffix = pendingProposalBodyHashes.has(contentHash(body, "body")) ? " (already queued)" : "";
|
|
51
44
|
lines.push(`[${i + 1}] memories/${m.name}${annotationSuffix}`, `Description: ${m.description || "(none)"}`, `Tags: ${m.tags.length > 0 ? m.tags.join(", ") : "(none)"}`, "---", stripFrontmatterBody(body).slice(0, bodyTruncation), "");
|
|
52
45
|
});
|
|
53
46
|
return lines.join("\n");
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { searchLocal } from "../../../indexer/search/db-search.js";
|
|
5
5
|
import { stripFrontmatterBody } from "../content-hash.js";
|
|
6
6
|
import { stripBundle } from "../ledger.js";
|
|
7
|
-
import { loadRetrievalQueries } from "../retrieval-
|
|
7
|
+
import { loadRetrievalQueries } from "../retrieval-queries.js";
|
|
8
8
|
/** At most this many of the retired asset's most recent queries are replayed (plan §5.4, §7). */
|
|
9
9
|
export const CONTINUITY_MAX_QUERIES = 5;
|
|
10
10
|
/** "Top 10" per the plan's rule — both the pass/fail cutoff and the search `limit`. */
|
|
@@ -63,8 +63,8 @@ function normalizedBody(raw) {
|
|
|
63
63
|
*
|
|
64
64
|
* S3: two fixes against false flags measured on a real night-1 admission
|
|
65
65
|
* (300 pairs, 6 flags, half spurious):
|
|
66
|
-
* - queries are the
|
|
67
|
-
*
|
|
66
|
+
* - queries are the cleaned set `loadRetrievalQueries` returns
|
|
67
|
+
* (`../retrieval-queries.ts`) — stash-README
|
|
68
68
|
* boilerplate, harness/tool envelopes, pastes over 2,000 characters, and
|
|
69
69
|
* duplicates are dropped before replay, not just capped at 5 raw entries;
|
|
70
70
|
* - when the retired and successor bodies normalize identical, the check
|
|
@@ -92,7 +92,7 @@ export async function checkRetirementContinuity(args) {
|
|
|
92
92
|
// no real risk here to check for.
|
|
93
93
|
if (normalizedBody(args.retiredRaw) === normalizedBody(args.successorRaw))
|
|
94
94
|
return undefined;
|
|
95
|
-
// S3a: the
|
|
95
|
+
// S3a: the cleaned queries from `loadRetrievalQueries` —
|
|
96
96
|
// boilerplate, envelopes, pastes and duplicates dropped before replay.
|
|
97
97
|
const queries = loadRetrievalQueries(args.ledgerAccess, args.retiredRef).slice(0, CONTINUITY_MAX_QUERIES);
|
|
98
98
|
if (queries.length === 0)
|
|
@@ -42,10 +42,9 @@ import { assertRunnerCredentials } from "../../../integrations/agent/runner-disp
|
|
|
42
42
|
import { runGit } from "../../../sources/providers/git-install.js";
|
|
43
43
|
import { closeDatabase, openExistingDatabase, openReadonlyExistingDatabase, } from "../../../storage/repositories/index-connection.js";
|
|
44
44
|
import { getAllEntries, getEntryById } from "../../../storage/repositories/index-entries-repository.js";
|
|
45
|
-
import { getNeighborsByEntryId } from "../../../storage/repositories/index-vec-repository.js";
|
|
46
|
-
import { isRetireProposal, PAIR_PASS_GATE, } from "../../proposal/proposal-types.js";
|
|
45
|
+
import { getNeighborsByEntryId, hasStoredEmbedding } from "../../../storage/repositories/index-vec-repository.js";
|
|
46
|
+
import { EXPIRED_GATE_REASON, isRetireProposal, PAIR_PASS_GATE, STALE_TARGET_GATE_REASON, } from "../../proposal/proposal-types.js";
|
|
47
47
|
import { createRetireProposal, listProposalsReadOnly, proposalContentHash, recordGateDecision, } from "../../proposal/repository.js";
|
|
48
|
-
import { isHotCapturedMemory } from "../consolidate.js";
|
|
49
48
|
import { contentHash, stripFrontmatterBody } from "../content-hash.js";
|
|
50
49
|
import { loadLedgerSnapshot, PAIR_PASS_LEDGER_SOURCE, recordLedgerAttempt, stripBundle } from "../ledger.js";
|
|
51
50
|
import { isInRetrievalScope, loadRetrievalScope } from "../retrieval-scope.js";
|
|
@@ -313,6 +312,10 @@ export function loadRejectedPairKeys(stashDir, proposalsCtx) {
|
|
|
313
312
|
for (const p of listProposalsReadOnly(stashDir, { status, includeArchive: true }, proposalsCtx)) {
|
|
314
313
|
if (!isRetireProposal(p) || !p.retirement)
|
|
315
314
|
continue;
|
|
315
|
+
// Expiry and a stale target are procedural, not a person saying no: the pair may be judged again.
|
|
316
|
+
const autoReason = p.gateDecision?.outcome === "auto-rejected" ? p.gateDecision.reason : undefined;
|
|
317
|
+
if (autoReason === EXPIRED_GATE_REASON || autoReason === STALE_TARGET_GATE_REASON)
|
|
318
|
+
continue;
|
|
316
319
|
keys.add(rejectedPairKey(p.retirement.retiredRef, p.retirement.successorRef, p.retirement.retiredContentHash, p.retirement.successorContentHash));
|
|
317
320
|
}
|
|
318
321
|
}
|
|
@@ -364,12 +367,18 @@ export function selectInitiators(pool, opts, stashDir, gitFirstAdded) {
|
|
|
364
367
|
* apart from a fully-judged one). S2: fetches `PAIR_NEIGHBOR_FETCH_K` (20)
|
|
365
368
|
* raw neighbours and keeps the first `PAIR_NEIGHBOR_K` (5) that clear every
|
|
366
369
|
* filter, so a few self/twin/bundle/tier misses in the raw top-5 no longer
|
|
367
|
-
* starve an initiator down to zero real candidates.
|
|
370
|
+
* starve an initiator down to zero real candidates. An initiator with no
|
|
371
|
+
* stored vector (embeddings off, or not embedded yet) cannot be queried at
|
|
372
|
+
* all; its ref is added to `unvectored` so the caller does not settle it.
|
|
368
373
|
*/
|
|
369
|
-
export function selectCandidates(db, initiators, bundleId) {
|
|
374
|
+
export function selectCandidates(db, initiators, bundleId, unvectored) {
|
|
370
375
|
const candidates = [];
|
|
371
376
|
const seenPairs = new Set();
|
|
372
377
|
for (const initiator of initiators) {
|
|
378
|
+
if (!hasStoredEmbedding(db, initiator.entryId)) {
|
|
379
|
+
unvectored?.add(initiator.ref);
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
373
382
|
// S1: a changed-content or new-material initiator judges at T_PAIR; the
|
|
374
383
|
// rest of the backlog (no row, not recently git-added) needs the higher
|
|
375
384
|
// BACKFILL_FLOOR.
|
|
@@ -597,9 +606,6 @@ async function judgeOne(ctx, candidate) {
|
|
|
597
606
|
[retired, successor] = [successor, retired];
|
|
598
607
|
}
|
|
599
608
|
// Guards (plan §5.2 step 4 / brief §A "Guards").
|
|
600
|
-
if (retired.asset.type === "memory" && isHotCapturedMemory(retired.asset.filePath)) {
|
|
601
|
-
return { failed: false }; // never propose retiring a captureMode: hot memory — leave the pair alone
|
|
602
|
-
}
|
|
603
609
|
if (retired.asset.type === "memory" && retired.asset.name.endsWith(DERIVED_SUFFIX)) {
|
|
604
610
|
// S3: derive the parent path from the FULL file path, not from name +
|
|
605
611
|
// dirname — for a subfolder memory (e.g. memories/sub/foo.derived) the
|
|
@@ -736,6 +742,7 @@ seams = {}) {
|
|
|
736
742
|
let candidates;
|
|
737
743
|
let gitFirstAdded;
|
|
738
744
|
let db;
|
|
745
|
+
const unvectored = new Set();
|
|
739
746
|
try {
|
|
740
747
|
db = opts.dryRun ? openReadonlyExistingDatabase(undefined, { isolatedSnapshot: true }) : openExistingDatabase();
|
|
741
748
|
if (!db)
|
|
@@ -743,7 +750,7 @@ seams = {}) {
|
|
|
743
750
|
gitFirstAdded = loadGitFirstAddedMap(stashDir);
|
|
744
751
|
const pool = loadPairPassPool(db, bundleId);
|
|
745
752
|
initiators = selectInitiators(pool, opts, stashDir, gitFirstAdded).initiators;
|
|
746
|
-
candidates = selectCandidates(db, initiators, bundleId);
|
|
753
|
+
candidates = selectCandidates(db, initiators, bundleId, unvectored);
|
|
747
754
|
}
|
|
748
755
|
catch (error) {
|
|
749
756
|
warnings.push(`Pair pass: index unavailable — skipped (${error instanceof Error ? error.message : String(error)}).`);
|
|
@@ -754,6 +761,9 @@ seams = {}) {
|
|
|
754
761
|
closeDatabase(db);
|
|
755
762
|
}
|
|
756
763
|
const initiatorsBacklog = initiators.filter((i) => i.backlog).length;
|
|
764
|
+
if (unvectored.size > 0) {
|
|
765
|
+
warnings.push(`Pair pass: ${unvectored.size} asset(s) have no stored embedding, so their neighbours cannot be found and they were not judged. The pair pass needs stored embeddings (set semanticSearchMode to "auto" in the config, then run akm index).`);
|
|
766
|
+
}
|
|
757
767
|
// Never judge a pair when either side already has a pending retire
|
|
758
768
|
// proposal, as the retired ref OR its successor (B2 widens this from the
|
|
759
769
|
// retired ref alone): an asset spoken for by one pending decision cannot
|
|
@@ -867,6 +877,9 @@ seams = {}) {
|
|
|
867
877
|
const admittedRefs = new Set(judgeable.map((c) => c.initiator.ref));
|
|
868
878
|
const ledgerInputs = initiators
|
|
869
879
|
.filter((i) => {
|
|
880
|
+
// No stored vector: the neighbour query never ran, so nothing was judged. Leave it unsettled.
|
|
881
|
+
if (unvectored.has(i.ref))
|
|
882
|
+
return false;
|
|
870
883
|
const hasCandidates = byInitiator.has(i.ref);
|
|
871
884
|
if (!hasCandidates)
|
|
872
885
|
return true;
|
|
@@ -43,7 +43,7 @@ import { getBodyEmbeddings, upsertBodyEmbeddings } from "../../storage/repositor
|
|
|
43
43
|
import { closeDatabase, openExistingDatabase, openReadonlyExistingDatabase, } from "../../storage/repositories/index-connection.js";
|
|
44
44
|
import { getAllEntries } from "../../storage/repositories/index-entries-repository.js";
|
|
45
45
|
import { listProposals, listProposalsReadOnly, proposalContent } from "../proposal/repository.js";
|
|
46
|
-
import {
|
|
46
|
+
import { hasSupersededStatus, validateProposalFrontmatter } from "../proposal/validators/proposal-quality-validators.js";
|
|
47
47
|
import { buildChunkPrompt, computeSafeChunkSize, DEFAULT_CONTEXT_LENGTH_TOKENS } from "./consolidate/chunking.js";
|
|
48
48
|
import { openKnowledgeCoverage } from "./consolidate/coverage.js";
|
|
49
49
|
import { runConsolidatePairPass } from "./consolidate/pair-pass.js";
|
|
@@ -75,21 +75,6 @@ export function mergePlans(chunks) {
|
|
|
75
75
|
export function isConsolidationEligibleMemoryName(name) {
|
|
76
76
|
return !name.endsWith(".derived");
|
|
77
77
|
}
|
|
78
|
-
/**
|
|
79
|
-
* A `captureMode: hot` memory (written deliberately with `akm remember`). A
|
|
80
|
-
* missing file is not hot; an unreadable one is treated as hot — the check is
|
|
81
|
-
* a protection and must not fail open.
|
|
82
|
-
*/
|
|
83
|
-
export function isHotCapturedMemory(filePath) {
|
|
84
|
-
if (!fs.existsSync(filePath))
|
|
85
|
-
return false;
|
|
86
|
-
try {
|
|
87
|
-
return hasHotCaptureMode(parseFrontmatter(fs.readFileSync(filePath, "utf8")).data);
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
return true;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
78
|
/**
|
|
94
79
|
* Structured-output schema for a plan. Promote-only: merge/delete/contradict
|
|
95
80
|
* were removed in 0.9.17-alpha.1 (`e82eec811`) after running in production —
|
|
@@ -531,9 +516,8 @@ const ABORT_MIN_CHUNKS = 4;
|
|
|
531
516
|
const ABORT_FAILURE_RATE = 0.5;
|
|
532
517
|
/**
|
|
533
518
|
* The chunk loop: stop cleanly on the budget signal, abort once ≥50% of at
|
|
534
|
-
* least 4 chunks failed (the model is likely down),
|
|
535
|
-
*
|
|
536
|
-
* count every memory into exactly one accounting bucket.
|
|
519
|
+
* least 4 chunks failed (the model is likely down), and count every memory
|
|
520
|
+
* into exactly one accounting bucket.
|
|
537
521
|
*/
|
|
538
522
|
async function judgeConsolidationChunks(args) {
|
|
539
523
|
const { chunks, opts, config, warnings, acc } = args;
|
|
@@ -571,15 +555,6 @@ async function judgeConsolidationChunks(args) {
|
|
|
571
555
|
}
|
|
572
556
|
}
|
|
573
557
|
const chunk = chunks[chunkIdx];
|
|
574
|
-
if (chunk.length > 0 && chunk.every((m) => isHotCapturedMemory(m.filePath))) {
|
|
575
|
-
for (const m of chunk) {
|
|
576
|
-
acc.judgedNoActionRefs.add(memRef(m));
|
|
577
|
-
acc.judgedRefs.add(memRef(m));
|
|
578
|
-
}
|
|
579
|
-
acc.judgedNoAction += chunk.length;
|
|
580
|
-
warn(`[consolidate] chunk ${chunkIdx + 1}/${chunks.length}: all ${chunk.length} memories are captureMode: hot — skipping LLM (judged no-action).`);
|
|
581
|
-
continue;
|
|
582
|
-
}
|
|
583
558
|
warn(`[consolidate] chunk ${chunkIdx + 1}/${chunks.length} (${chunk.length} memories) …`);
|
|
584
559
|
processed++;
|
|
585
560
|
if (!llmRunner) {
|
|
@@ -667,8 +642,7 @@ async function planConsolidation(opts, config, stashDir, memories, warnings, sta
|
|
|
667
642
|
}
|
|
668
643
|
}
|
|
669
644
|
const slice = (list) => Array.from({ length: Math.ceil(list.length / chunkSize) }, (_, i) => list.slice(i * chunkSize, (i + 1) * chunkSize));
|
|
670
|
-
|
|
671
|
-
if (llmRunner && willDispatch)
|
|
645
|
+
if (llmRunner && budgeted.length > 0)
|
|
672
646
|
assertRunnerCredentials(llmRunner);
|
|
673
647
|
const { ordered, embedTelemetry } = await clusterMemoriesBySimilarity(budgeted, config, stateDb, opts.signal);
|
|
674
648
|
const chunks = slice(ordered);
|