backpass 0.1.8 → 0.1.9
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/README.md +19 -5
- package/package.json +1 -1
- package/src/analyze.js +24 -1
- package/src/apply/writer.js +4 -2
- package/src/commands/bootstrap.js +20 -8
- package/src/commands/propose.js +14 -4
package/README.md
CHANGED
|
@@ -148,9 +148,14 @@ instruction covers.
|
|
|
148
148
|
most important defence against a model confabulating influence. Negative evidence (a
|
|
149
149
|
visible violation) is weighted highest.
|
|
150
150
|
|
|
151
|
-
Results are cached per transcript, keyed to both the transcript's content _and_ the
|
|
152
|
-
file
|
|
153
|
-
the next run is free.
|
|
151
|
+
Results are cached per transcript, keyed to both the transcript's content _and_ the effective
|
|
152
|
+
memory-file set hash: edit the weights and the evidence correctly re-computes; change nothing
|
|
153
|
+
and the next run is free. A memory-file edit therefore reanalyzes without `--force` - that is
|
|
154
|
+
not a cache miss, it is the cache doing its job - and the run says so on stderr, naming the
|
|
155
|
+
old and new hash, so a "0 reused" line reads as "the file changed" rather than "reuse is
|
|
156
|
+
broken." Evidence files that are not refreshed remain on disk but are excluded while their
|
|
157
|
+
hash is stale. They become eligible again if the memory-file set returns to that hash;
|
|
158
|
+
evidence for transcripts included in the new analysis is replaced with fresh judgments.
|
|
154
159
|
|
|
155
160
|
### 4. Aggregate gradients - deterministic, no model
|
|
156
161
|
|
|
@@ -159,6 +164,13 @@ Evidence is grouped by instruction, giving each one a positive/negative count an
|
|
|
159
164
|
gaps across sessions are clustered, and clusters seen in fewer than `minGapEvidence`
|
|
160
165
|
sessions (default 2) are dropped. One bad session never rewrites the weights.
|
|
161
166
|
|
|
167
|
+
Only evidence judged against the _current_ memory-file set hash is folded into a proposal. A
|
|
168
|
+
transcript that fell out of this run's sample - the time window, `maxTranscripts`, or the
|
|
169
|
+
transcript itself being gone - can leave an older evidence file on disk under a hash the
|
|
170
|
+
memory-file set no longer has; that file is left untouched, but it does not count toward this
|
|
171
|
+
run's session total or instruction scores, or add a gap observation, until it is current
|
|
172
|
+
again.
|
|
173
|
+
|
|
162
174
|
Those sessions are counted across runs, not per run: every gap sighting is kept in
|
|
163
175
|
`.backpass/gap-ledger.json` by gap and session, so a gap seen in one session today and in
|
|
164
176
|
another session next week graduates on the later run. The same session never counts twice,
|
|
@@ -277,8 +289,10 @@ Apply preflights every accepted edit before writing. The proposal was measured a
|
|
|
277
289
|
exact version of your memory file, so apply first checks the file still exists and is still
|
|
278
290
|
that version. If it was removed or changed since - you pulled, edited it by hand, or another
|
|
279
291
|
agent did - the edits no longer describe what is on disk, so nothing is written and you are
|
|
280
|
-
told to run `backpass` again to re-propose against the current file.
|
|
281
|
-
|
|
292
|
+
told to run `backpass` again to re-propose against the current file. That rerun reanalyzes
|
|
293
|
+
transcripts against the file that exists now; it does not reuse the stale judgments behind
|
|
294
|
+
the refused proposal. Within a run every file is composed from one version: it takes every
|
|
295
|
+
accepted edit or none of them. Apply also
|
|
282
296
|
refuses the whole write if any created skill target already exists or two accepted paths
|
|
283
297
|
resolve to the same file.
|
|
284
298
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backpass",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"packageManager": "pnpm@11.5.0",
|
|
5
5
|
"description": "Gradient descent for your agent memory - analyzes past agent session transcripts and proposes evidence-backed edits to AGENTS.md / CLAUDE.md",
|
|
6
6
|
"type": "module",
|
package/src/analyze.js
CHANGED
|
@@ -185,7 +185,16 @@ async function pool(items, limit, worker) {
|
|
|
185
185
|
export async function analyzeTranscripts({ transcripts, memoryFile, config, repo, memoryHash, force = false }) {
|
|
186
186
|
const state = config.state;
|
|
187
187
|
const pending = [];
|
|
188
|
-
const summary = {
|
|
188
|
+
const summary = {
|
|
189
|
+
total: transcripts.length,
|
|
190
|
+
cached: 0,
|
|
191
|
+
analyzed: 0,
|
|
192
|
+
skipped: 0,
|
|
193
|
+
failed: 0,
|
|
194
|
+
usage: [],
|
|
195
|
+
staleMemoryHash: 0,
|
|
196
|
+
};
|
|
197
|
+
const priorHashes = new Set();
|
|
189
198
|
|
|
190
199
|
for (const transcript of transcripts) {
|
|
191
200
|
const existing = state.readEvidence(transcript.id);
|
|
@@ -193,9 +202,23 @@ export async function analyzeTranscripts({ transcripts, memoryFile, config, repo
|
|
|
193
202
|
summary.cached += 1;
|
|
194
203
|
continue;
|
|
195
204
|
}
|
|
205
|
+
// Distinguish "no prior evidence" from "prior evidence exists, but it was judged
|
|
206
|
+
// against a memory-file set that no longer matches" - a re-analysis here, not a miss.
|
|
207
|
+
if (existing?.status === "ok" && existing.memoryHash && existing.memoryHash !== memoryHash) {
|
|
208
|
+
summary.staleMemoryHash += 1;
|
|
209
|
+
priorHashes.add(existing.memoryHash);
|
|
210
|
+
}
|
|
196
211
|
pending.push(transcript);
|
|
197
212
|
}
|
|
198
213
|
|
|
214
|
+
if (summary.staleMemoryHash) {
|
|
215
|
+
info(
|
|
216
|
+
`${color.yellow("·")} ${summary.staleMemoryHash} transcript(s) have evidence from a previous ` +
|
|
217
|
+
`memory-file set (${[...priorHashes].join(", ")} -> ${memoryHash}); that evidence is stale, not ` +
|
|
218
|
+
`missing, and reuse resumes once this pass re-judges it against the current memory-file set`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
199
222
|
if (!pending.length) {
|
|
200
223
|
emitProgress("analyze:start", { pending: 0, cached: summary.cached, total: transcripts.length, jobs: config.jobs });
|
|
201
224
|
emitProgress("analyze:done", summary);
|
package/src/apply/writer.js
CHANGED
|
@@ -76,7 +76,8 @@ function memoryFileSnapshot(proposal, repo) {
|
|
|
76
76
|
file: relative,
|
|
77
77
|
error:
|
|
78
78
|
`${relative} no longer exists, so its edits no longer describe the file on disk; nothing was written. ` +
|
|
79
|
-
`Run \`backpass\` to re-propose against the current repository
|
|
79
|
+
`Run \`backpass\` to re-propose against the current repository - that pass reanalyzes transcripts ` +
|
|
80
|
+
`against the file that exists now, it does not reuse the judgments behind this proposal.`,
|
|
80
81
|
},
|
|
81
82
|
};
|
|
82
83
|
}
|
|
@@ -94,7 +95,8 @@ function memoryFileSnapshot(proposal, repo) {
|
|
|
94
95
|
error:
|
|
95
96
|
`${relative} changed after this proposal was made (${expected} -> ${observed}), so its edits ` +
|
|
96
97
|
`no longer describe the file on disk; nothing was written. Run \`backpass\` to re-propose ` +
|
|
97
|
-
`against the current ${relative}
|
|
98
|
+
`against the current ${relative} - that pass reanalyzes transcripts against the new file, it does ` +
|
|
99
|
+
`not reuse the stale judgments behind this proposal.`,
|
|
98
100
|
},
|
|
99
101
|
};
|
|
100
102
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { analyzeTranscripts } from "../analyze.js";
|
|
2
2
|
import { applyDecisions, writeBootstrapFiles } from "../apply/writer.js";
|
|
3
3
|
import { bootstrapTargets, renderPointer, starterMemoryFile } from "../bootstrap.js";
|
|
4
|
-
import { color, info, json, out, warn } from "../logger.js";
|
|
4
|
+
import { UserError, color, info, json, out, warn } from "../logger.js";
|
|
5
|
+
import { resolveMemoryFiles } from "../memory.js";
|
|
5
6
|
import { emitProgress } from "../progress.js";
|
|
6
7
|
import { ProposalViolation } from "../proposal.js";
|
|
7
8
|
import { synthesizeProposal } from "../synthesize.js";
|
|
@@ -52,14 +53,25 @@ export async function bootstrapRun(ctx, deps = {}) {
|
|
|
52
53
|
const seed = [{ path: canonical, text: starter.text }];
|
|
53
54
|
if (pointer) seed.push({ path: pointer, text: renderPointer(canonical) });
|
|
54
55
|
const seeded = writeBootstrapFiles(repo.root, seed);
|
|
56
|
+
const resolved = resolveMemoryFiles(repo.root, config.memoryFiles);
|
|
55
57
|
for (const w of seeded.written) info(`${color.green("·")} wrote ${w.file}`);
|
|
56
58
|
for (const s of seeded.skipped) warn(`${s.file} ${s.reason} - left untouched`);
|
|
57
59
|
|
|
60
|
+
const canonicalSkipped = seeded.skipped.some((entry) => entry.file === canonical);
|
|
61
|
+
if (canonicalSkipped || resolved.primary?.path !== starter.path || resolved.primary?.text !== starter.text) {
|
|
62
|
+
throw new UserError(
|
|
63
|
+
`${canonical} changed while backpass was bootstrapping it`,
|
|
64
|
+
"run `backpass` again to analyze the memory file that now exists",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const memoryFile = resolved.primary;
|
|
68
|
+
const memoryHash = resolved.hash;
|
|
69
|
+
|
|
58
70
|
emitProgress("memory", {
|
|
59
|
-
path:
|
|
60
|
-
tokens:
|
|
71
|
+
path: memoryFile.path,
|
|
72
|
+
tokens: memoryFile.tokens,
|
|
61
73
|
budget: config.budgetTokens,
|
|
62
|
-
units:
|
|
74
|
+
units: memoryFile.units.length,
|
|
63
75
|
});
|
|
64
76
|
|
|
65
77
|
const result = {
|
|
@@ -78,10 +90,10 @@ export async function bootstrapRun(ctx, deps = {}) {
|
|
|
78
90
|
|
|
79
91
|
result.summary = await analyze({
|
|
80
92
|
transcripts,
|
|
81
|
-
memoryFile
|
|
93
|
+
memoryFile,
|
|
82
94
|
config,
|
|
83
95
|
repo,
|
|
84
|
-
memoryHash
|
|
96
|
+
memoryHash,
|
|
85
97
|
force: Boolean(ctx.flags.force),
|
|
86
98
|
});
|
|
87
99
|
info(
|
|
@@ -89,7 +101,7 @@ export async function bootstrapRun(ctx, deps = {}) {
|
|
|
89
101
|
`${result.summary.skipped} too short · ${result.summary.failed} failed`,
|
|
90
102
|
);
|
|
91
103
|
|
|
92
|
-
const folded = await foldForRun(ctx,
|
|
104
|
+
const folded = await foldForRun(ctx, memoryFile, memoryHash);
|
|
93
105
|
config.state.writeSummary(folded);
|
|
94
106
|
emitProgress("fold:done", {
|
|
95
107
|
instructions: folded.instructions.length,
|
|
@@ -102,7 +114,7 @@ export async function bootstrapRun(ctx, deps = {}) {
|
|
|
102
114
|
|
|
103
115
|
try {
|
|
104
116
|
const { proposal } = await synthesize({
|
|
105
|
-
memoryFile
|
|
117
|
+
memoryFile,
|
|
106
118
|
summary: folded,
|
|
107
119
|
config,
|
|
108
120
|
repo,
|
package/src/commands/propose.js
CHANGED
|
@@ -15,11 +15,21 @@ import { discoverForRun } from "./scan.js";
|
|
|
15
15
|
* prune what the current file now covers or what aged out (after recording, because the
|
|
16
16
|
* evidence files that fed an expired sighting are still on disk and would re-add it),
|
|
17
17
|
* then cluster from the ledger.
|
|
18
|
+
*
|
|
19
|
+
* Evidence is also filtered to `memoryHash`: a transcript's evidence file is rewritten
|
|
20
|
+
* every time it is re-analyzed against a changed memory file, but a transcript that fell
|
|
21
|
+
* out of this run's sample (window, cap, or discovery drift) leaves its last evidence file
|
|
22
|
+
* on disk under whatever hash it was last judged against. That leftover file is real and
|
|
23
|
+
* reusable the moment its transcript is re-analyzed - or immediately, if the memory file's
|
|
24
|
+
* bytes return to that hash - but folding it into *this* proposal would score it against
|
|
25
|
+
* an instruction index it was never judged against (aliases are positional) and inflate
|
|
26
|
+
* `analyzedSessions` with a session this run never touched. Nothing is migrated, rewritten,
|
|
27
|
+
* or deleted here - only excluded from this run's fold.
|
|
18
28
|
*/
|
|
19
|
-
export async function foldForRun(ctx, memoryFile) {
|
|
29
|
+
export async function foldForRun(ctx, memoryFile, memoryHash) {
|
|
20
30
|
const { state, minGapEvidence, gapLedgerMaxAge } = ctx.config;
|
|
21
31
|
const evidence = state.listEvidence();
|
|
22
|
-
const relevant = evidence.filter((e) => e.memoryPath === memoryFile.path);
|
|
32
|
+
const relevant = evidence.filter((e) => e.memoryPath === memoryFile.path && e.memoryHash === memoryHash);
|
|
23
33
|
|
|
24
34
|
const ledger = state.readGapLedger();
|
|
25
35
|
recordGapObservations(ledger, relevant);
|
|
@@ -39,11 +49,11 @@ export async function runProposal(ctx, precomputed = null) {
|
|
|
39
49
|
// folding, and agent resolution can all fail before synthesis starts; none of those
|
|
40
50
|
// failures may leave an older proposal available to apply as if it came from this run.
|
|
41
51
|
config.state.clearProposal();
|
|
42
|
-
const { file } = precomputed || primaryMemoryFile(repo, config);
|
|
52
|
+
const { file, hash } = precomputed || primaryMemoryFile(repo, config);
|
|
43
53
|
const transcripts = precomputed?.transcripts || (await discoverForRun(ctx)).transcripts;
|
|
44
54
|
|
|
45
55
|
const foldStarted = Date.now();
|
|
46
|
-
const summary = await foldForRun(ctx, file);
|
|
56
|
+
const summary = await foldForRun(ctx, file, hash);
|
|
47
57
|
config.state.writeSummary(summary);
|
|
48
58
|
emitProgress("fold:done", {
|
|
49
59
|
instructions: summary.instructions.length,
|