@seanhogg/builderforce-memory 2026.10.25 → 2026.10.27
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 +25 -0
- package/dist/working/codemapAreas.d.ts +49 -0
- package/dist/working/codemapAreas.d.ts.map +1 -0
- package/dist/working/codemapAreas.js +139 -0
- package/dist/working/codemapAreas.js.map +1 -0
- package/dist/working/consolidate.d.ts +17 -4
- package/dist/working/consolidate.d.ts.map +1 -1
- package/dist/working/consolidate.js +40 -7
- package/dist/working/consolidate.js.map +1 -1
- package/dist/working/consolidationMarker.d.ts +8 -2
- package/dist/working/consolidationMarker.d.ts.map +1 -1
- package/dist/working/consolidationMarker.js +13 -5
- package/dist/working/consolidationMarker.js.map +1 -1
- package/dist/working/index.d.ts +4 -3
- package/dist/working/index.d.ts.map +1 -1
- package/dist/working/index.js +4 -3
- package/dist/working/index.js.map +1 -1
- package/dist/working/summary.d.ts +18 -4
- package/dist/working/summary.d.ts.map +1 -1
- package/dist/working/summary.js +58 -14
- package/dist/working/summary.js.map +1 -1
- package/dist/working/transcript.d.ts +28 -10
- package/dist/working/transcript.d.ts.map +1 -1
- package/dist/working/transcript.js +72 -40
- package/dist/working/transcript.js.map +1 -1
- package/dist/working/types.d.ts +6 -0
- package/dist/working/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/working/codemap.test.ts +144 -0
- package/src/working/codemapAreas.ts +142 -0
- package/src/working/consolidate.ts +40 -6
- package/src/working/consolidationMarker.test.ts +12 -2
- package/src/working/consolidationMarker.ts +12 -5
- package/src/working/index.ts +19 -8
- package/src/working/summary.test.ts +35 -1
- package/src/working/summary.ts +58 -13
- package/src/working/transcript.ts +69 -38
- package/src/working/transcriptWindow.test.ts +84 -0
- package/src/working/types.ts +6 -0
- package/src/working/working.test.ts +7 -4
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/working/transcriptWindow.test.ts
|
|
3
|
+
* Drop-oldest windowing drops in CHUNKS with hysteresis, so the front of the request —
|
|
4
|
+
* the prefix a provider cache hashes — moves rarely instead of on every turn.
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
windowed,
|
|
8
|
+
HISTORY_WINDOW,
|
|
9
|
+
HISTORY_WINDOW_DROP_CHUNK,
|
|
10
|
+
HISTORY_TOKEN_BUDGET,
|
|
11
|
+
HISTORY_TOKEN_DROP_CHUNK,
|
|
12
|
+
transcriptTokens,
|
|
13
|
+
} from './transcript.js';
|
|
14
|
+
import type { TranscriptMessage } from './transcript.js';
|
|
15
|
+
|
|
16
|
+
const msg = (role: TranscriptMessage['role'], content = 'x'): TranscriptMessage => ({ role, content });
|
|
17
|
+
|
|
18
|
+
/** user/assistant pairs, `n` messages long, each message distinct. */
|
|
19
|
+
function convoOf(n: number, content = (i: number) => `m${i}`): TranscriptMessage[] {
|
|
20
|
+
return Array.from({ length: n }, (_, i) => msg(i % 2 === 0 ? 'user' : 'assistant', content(i)));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('windowed — chunked message-count drops', () => {
|
|
24
|
+
it('keeps everything up to the window', () => {
|
|
25
|
+
const convo = convoOf(HISTORY_WINDOW);
|
|
26
|
+
expect(windowed(convo)).toEqual(convo);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('drops a whole chunk once over, then holds the front while messages arrive', () => {
|
|
30
|
+
const fronts: unknown[] = [];
|
|
31
|
+
for (let n = HISTORY_WINDOW + 1; n <= HISTORY_WINDOW + HISTORY_WINDOW_DROP_CHUNK; n++) {
|
|
32
|
+
const w = windowed(convoOf(n));
|
|
33
|
+
expect(w.length).toBeLessThanOrEqual(HISTORY_WINDOW);
|
|
34
|
+
fronts.push(w[0]!.content);
|
|
35
|
+
}
|
|
36
|
+
// One front for the whole chunk: the prefix did not move turn to turn.
|
|
37
|
+
expect(new Set(fronts).size).toBe(1);
|
|
38
|
+
expect(fronts[0]).toBe(`m${HISTORY_WINDOW_DROP_CHUNK}`);
|
|
39
|
+
// The first drop leaves room: ~75% of the window, not exactly the window.
|
|
40
|
+
expect(windowed(convoOf(HISTORY_WINDOW + 1)).length).toBe(HISTORY_WINDOW + 1 - HISTORY_WINDOW_DROP_CHUNK);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('moves the front by the next chunk only when the window fills again', () => {
|
|
44
|
+
const w = windowed(convoOf(HISTORY_WINDOW + HISTORY_WINDOW_DROP_CHUNK + 1));
|
|
45
|
+
expect(w[0]!.content).toBe(`m${2 * HISTORY_WINDOW_DROP_CHUNK}`);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('windowed — chunked token drops', () => {
|
|
50
|
+
// ~2k tokens per message (chars/4), so the budget holds ~32 of them.
|
|
51
|
+
const big = (i: number) => `${i}:${'y'.repeat(8_000)}`;
|
|
52
|
+
|
|
53
|
+
it('drops down to ~75% of the budget, not just under it', () => {
|
|
54
|
+
let n = 2;
|
|
55
|
+
while (transcriptTokens(convoOf(n, big)) <= HISTORY_TOKEN_BUDGET) n += 2;
|
|
56
|
+
const w = windowed(convoOf(n, big));
|
|
57
|
+
const kept = transcriptTokens(w);
|
|
58
|
+
expect(kept).toBeLessThanOrEqual(HISTORY_TOKEN_BUDGET);
|
|
59
|
+
expect(kept).toBeLessThanOrEqual(HISTORY_TOKEN_BUDGET - HISTORY_TOKEN_DROP_CHUNK + 4_100);
|
|
60
|
+
expect(w[0]!.role).toBe('user');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('holds the front across turns until a whole chunk of new work arrives', () => {
|
|
64
|
+
let n = 2;
|
|
65
|
+
while (transcriptTokens(convoOf(n, big)) <= HISTORY_TOKEN_BUDGET) n += 2;
|
|
66
|
+
const first = windowed(convoOf(n, big))[0];
|
|
67
|
+
// A few more turns (< one chunk of tokens) — same front.
|
|
68
|
+
for (let extra = 2; extra <= 4; extra += 2) {
|
|
69
|
+
expect(windowed(convoOf(n + extra, big))[0]!.content).toBe(first!.content);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('never drops the most recent user turn, however large', () => {
|
|
74
|
+
const convo = [msg('user', 'old'), msg('assistant', 'a'), msg('user', 'z'.repeat(HISTORY_TOKEN_BUDGET * 5))];
|
|
75
|
+
const w = windowed(convo);
|
|
76
|
+
expect(w[0]!.content).toBe(convo[2]!.content);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('still starts on a user turn after a drop (strict vendors)', () => {
|
|
80
|
+
const convo: TranscriptMessage[] = [];
|
|
81
|
+
for (let i = 0; i < 40; i++) convo.push(msg('user', `u${i}`), msg('assistant', big(i)), msg('tool', big(i)));
|
|
82
|
+
expect(windowed(convo)[0]!.role).toBe('user');
|
|
83
|
+
});
|
|
84
|
+
});
|
package/src/working/types.ts
CHANGED
|
@@ -29,4 +29,10 @@ export interface ConsolidateOptions {
|
|
|
29
29
|
at?: Date;
|
|
30
30
|
/** The workspace root — absolute paths under it are kept, made relative. */
|
|
31
31
|
root?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Distinct files (changed, deleted and read together) a run must touch for its map to
|
|
34
|
+
* be kept. Defaults to `MIN_CONSOLIDATED_FILES`; the same floor applies to a run that
|
|
35
|
+
* changed files as to one that only read.
|
|
36
|
+
*/
|
|
37
|
+
minFiles?: number;
|
|
32
38
|
}
|
|
@@ -83,7 +83,10 @@ describe('consolidate', () => {
|
|
|
83
83
|
|
|
84
84
|
it('skips a short lookup but keeps any change', () => {
|
|
85
85
|
expect(consolidate(snap({ read: ['a/b/c.ts', 'a/b/d.ts'] }))).toBeNull();
|
|
86
|
-
|
|
86
|
+
// 2026.10.27: the 3-file floor now holds for a run that changed files too (it used to
|
|
87
|
+
// keep a one-file fix as a map). The kept-change half moved to `minFiles` below.
|
|
88
|
+
expect(consolidate(snap({ edited: ['api/src/r/x.ts'] }))).toBeNull();
|
|
89
|
+
expect(consolidate(snap({ edited: ['api/src/r/x.ts'] }), { minFiles: 1 })?.key).toBe('codemap:api/src/r');
|
|
87
90
|
});
|
|
88
91
|
});
|
|
89
92
|
|
|
@@ -91,7 +94,7 @@ describe('mergeConsolidated', () => {
|
|
|
91
94
|
const prior = consolidate(snap({ read: ['w/s/a.ts', 'w/s/b.ts'], edited: ['w/s/c.ts'] }), { at: AT })!;
|
|
92
95
|
|
|
93
96
|
it('adds a small run to the area map, restating the earlier run\'s edits', () => {
|
|
94
|
-
const next = consolidate(snap({ task: 'fix d', edited: ['w/s/d.ts'] }), { at: AT })!;
|
|
97
|
+
const next = consolidate(snap({ task: 'fix d', edited: ['w/s/d.ts'] }), { at: AT, minFiles: 1 })!;
|
|
95
98
|
const merged = mergeConsolidated(next.key, prior.content, next.content);
|
|
96
99
|
expect(merged).toContain('asked: "fix d"');
|
|
97
100
|
const entries = merged.split('\n').filter((l) => l.startsWith('- '));
|
|
@@ -99,7 +102,7 @@ describe('mergeConsolidated', () => {
|
|
|
99
102
|
});
|
|
100
103
|
|
|
101
104
|
it('removes a file this run deleted and never stores the deletion line', () => {
|
|
102
|
-
const next = consolidate(snap({ deleted: ['w/s/a.ts'], edited: ['w/s/e.ts'] }), { at: AT })!;
|
|
105
|
+
const next = consolidate(snap({ deleted: ['w/s/a.ts'], edited: ['w/s/e.ts'] }), { at: AT, minFiles: 1 })!;
|
|
103
106
|
const merged = mergeConsolidated(next.key, prior.content, next.content);
|
|
104
107
|
expect(merged).not.toContain('w/s/a.ts');
|
|
105
108
|
expect(merged).not.toContain('(deleted)');
|
|
@@ -108,7 +111,7 @@ describe('mergeConsolidated', () => {
|
|
|
108
111
|
|
|
109
112
|
it('caps the map and says how many more', () => {
|
|
110
113
|
const big = consolidate(snap({ read: Array.from({ length: 12 }, (_, i) => `w/s/f${i}.ts`) }), { at: AT })!;
|
|
111
|
-
const next = consolidate(snap({ edited: ['w/s/new.ts'] }), { at: AT })!;
|
|
114
|
+
const next = consolidate(snap({ edited: ['w/s/new.ts'] }), { at: AT, minFiles: 1 })!;
|
|
112
115
|
const merged = mergeConsolidated(next.key, big.content, next.content);
|
|
113
116
|
expect(merged.split('\n').filter((l) => l.startsWith('- '))).toHaveLength(12);
|
|
114
117
|
expect(merged).toContain('(+1 more file(s) touched)');
|