@seanhogg/builderforce-memory 2026.9.33 → 2026.10.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.
Files changed (116) hide show
  1. package/dist/errors/SSMError.d.ts +1 -1
  2. package/dist/errors/SSMError.d.ts.map +1 -1
  3. package/dist/errors/SSMError.js.map +1 -1
  4. package/dist/handoff/budget.d.ts +34 -0
  5. package/dist/handoff/budget.d.ts.map +1 -0
  6. package/dist/handoff/budget.js +77 -0
  7. package/dist/handoff/budget.js.map +1 -0
  8. package/dist/handoff/index.d.ts +18 -0
  9. package/dist/handoff/index.d.ts.map +1 -0
  10. package/dist/handoff/index.js +17 -0
  11. package/dist/handoff/index.js.map +1 -0
  12. package/dist/handoff/note.d.ts +35 -0
  13. package/dist/handoff/note.d.ts.map +1 -0
  14. package/dist/handoff/note.js +82 -0
  15. package/dist/handoff/note.js.map +1 -0
  16. package/dist/handoff/pack.d.ts +26 -0
  17. package/dist/handoff/pack.d.ts.map +1 -0
  18. package/dist/handoff/pack.js +131 -0
  19. package/dist/handoff/pack.js.map +1 -0
  20. package/dist/handoff/segments.d.ts +18 -0
  21. package/dist/handoff/segments.d.ts.map +1 -0
  22. package/dist/handoff/segments.js +36 -0
  23. package/dist/handoff/segments.js.map +1 -0
  24. package/dist/handoff/types.d.ts +59 -0
  25. package/dist/handoff/types.d.ts.map +1 -0
  26. package/dist/handoff/types.js +7 -0
  27. package/dist/handoff/types.js.map +1 -0
  28. package/dist/memory/MemoryStore.d.ts +120 -181
  29. package/dist/memory/MemoryStore.d.ts.map +1 -1
  30. package/dist/memory/MemoryStore.js +201 -389
  31. package/dist/memory/MemoryStore.js.map +1 -1
  32. package/dist/memory/SemanticRecall.d.ts +64 -0
  33. package/dist/memory/SemanticRecall.d.ts.map +1 -0
  34. package/dist/memory/SemanticRecall.js +188 -0
  35. package/dist/memory/SemanticRecall.js.map +1 -0
  36. package/dist/memory/records.d.ts +26 -0
  37. package/dist/memory/records.d.ts.map +1 -0
  38. package/dist/memory/records.js +71 -0
  39. package/dist/memory/records.js.map +1 -0
  40. package/dist/memory/ttlSweeper.d.ts +12 -0
  41. package/dist/memory/ttlSweeper.d.ts.map +1 -0
  42. package/dist/memory/ttlSweeper.js +28 -0
  43. package/dist/memory/ttlSweeper.js.map +1 -0
  44. package/dist/memory/types.d.ts +142 -0
  45. package/dist/memory/types.d.ts.map +1 -0
  46. package/dist/memory/types.js +6 -0
  47. package/dist/memory/types.js.map +1 -0
  48. package/dist/storage/ScopedFactRepository.d.ts +156 -0
  49. package/dist/storage/ScopedFactRepository.d.ts.map +1 -0
  50. package/dist/storage/ScopedFactRepository.js +278 -0
  51. package/dist/storage/ScopedFactRepository.js.map +1 -0
  52. package/dist/storage/factImport.d.ts +21 -0
  53. package/dist/storage/factImport.d.ts.map +1 -0
  54. package/dist/storage/factImport.js +61 -0
  55. package/dist/storage/factImport.js.map +1 -0
  56. package/dist/storage/factMutations.d.ts +46 -0
  57. package/dist/storage/factMutations.d.ts.map +1 -0
  58. package/dist/storage/factMutations.js +73 -0
  59. package/dist/storage/factMutations.js.map +1 -0
  60. package/dist/storage/factQueries.d.ts +16 -0
  61. package/dist/storage/factQueries.d.ts.map +1 -0
  62. package/dist/storage/factQueries.js +36 -0
  63. package/dist/storage/factQueries.js.map +1 -0
  64. package/dist/storage/factRetention.d.ts +30 -0
  65. package/dist/storage/factRetention.d.ts.map +1 -0
  66. package/dist/storage/factRetention.js +65 -0
  67. package/dist/storage/factRetention.js.map +1 -0
  68. package/dist/storage/fileLock.d.ts +56 -0
  69. package/dist/storage/fileLock.d.ts.map +1 -0
  70. package/dist/storage/fileLock.js +138 -0
  71. package/dist/storage/fileLock.js.map +1 -0
  72. package/dist/storage/fileTransaction.d.ts +33 -0
  73. package/dist/storage/fileTransaction.d.ts.map +1 -0
  74. package/dist/storage/fileTransaction.js +108 -0
  75. package/dist/storage/fileTransaction.js.map +1 -0
  76. package/dist/storage/idbAdapter.d.ts +95 -0
  77. package/dist/storage/idbAdapter.d.ts.map +1 -0
  78. package/dist/storage/idbAdapter.js +311 -0
  79. package/dist/storage/idbAdapter.js.map +1 -0
  80. package/dist/storage/nodeFileAdapter.d.ts +127 -0
  81. package/dist/storage/nodeFileAdapter.d.ts.map +1 -0
  82. package/dist/storage/nodeFileAdapter.js +275 -0
  83. package/dist/storage/nodeFileAdapter.js.map +1 -0
  84. package/dist/storage/syncJournal.d.ts +19 -0
  85. package/dist/storage/syncJournal.d.ts.map +1 -0
  86. package/dist/storage/syncJournal.js +46 -0
  87. package/dist/storage/syncJournal.js.map +1 -0
  88. package/dist/storage/types.d.ts +207 -0
  89. package/dist/storage/types.d.ts.map +1 -0
  90. package/dist/storage/types.js +90 -0
  91. package/dist/storage/types.js.map +1 -0
  92. package/package.json +8 -3
  93. package/src/errors/SSMError.ts +1 -0
  94. package/src/handoff/budget.ts +78 -0
  95. package/src/handoff/handoff.test.ts +247 -0
  96. package/src/handoff/index.ts +24 -0
  97. package/src/handoff/note.ts +102 -0
  98. package/src/handoff/pack.ts +156 -0
  99. package/src/handoff/segments.ts +42 -0
  100. package/src/handoff/types.ts +63 -0
  101. package/src/memory/MemoryStore.ts +245 -525
  102. package/src/memory/SemanticRecall.ts +204 -0
  103. package/src/memory/records.ts +79 -0
  104. package/src/memory/ttlSweeper.ts +29 -0
  105. package/src/memory/types.ts +149 -0
  106. package/src/storage/ScopedFactRepository.ts +373 -0
  107. package/src/storage/factImport.ts +73 -0
  108. package/src/storage/factMutations.ts +106 -0
  109. package/src/storage/factQueries.ts +41 -0
  110. package/src/storage/factRetention.ts +82 -0
  111. package/src/storage/fileLock.ts +162 -0
  112. package/src/storage/fileTransaction.ts +116 -0
  113. package/src/storage/idbAdapter.ts +373 -0
  114. package/src/storage/nodeFileAdapter.ts +331 -0
  115. package/src/storage/syncJournal.ts +47 -0
  116. package/src/storage/types.ts +262 -0
@@ -0,0 +1,156 @@
1
+ /**
2
+ * `packHandoff` — fit a transcript into the NEXT model's window (PRD 34 §3.3).
3
+ *
4
+ * Shape of the result, mirroring Builderforce's `assembleCompacted`
5
+ * (brain-embedded/src/workingTranscript.ts):
6
+ *
7
+ * leading system message(s) · ONE note for the dropped middle · the latest user
8
+ * directive re-injected verbatim (only when it fell in the middle) · the verbatim tail
9
+ *
10
+ * Only the LEADING run of system messages is the fixed prefix. A system message later in
11
+ * the conversation stays where it was: in the tail when the tail reaches it, else in the
12
+ * dropped middle — never hoisted to the front, where it would read as if it governed the
13
+ * whole conversation.
14
+ *
15
+ * The note's role keeps turns alternating: it is an `assistant` note when a user turn (the
16
+ * re-injected directive, or a tail that starts with one) follows it, and a `user` note
17
+ * when an assistant turn follows or nothing does — never two assistant turns in a row.
18
+ *
19
+ * The tail is the longest run of whole units (see `segments.ts`) from the end that fits
20
+ * what is left after the prefix, the directive and room for an elision marker. Whatever
21
+ * room the tail leaves is the note's: summary, else recall, else the marker. The result
22
+ * never exceeds the budget unless the prefix + directive alone do.
23
+ */
24
+
25
+ import { defaultEstimateTokens, handoffBudget, messageTokens, tokensOf } from './budget.js';
26
+ import { buildNote, composeNote, elisionMarker, handoffLine, noteTokens } from './note.js';
27
+ import { segmentTranscript, type Segment } from './segments.js';
28
+ import type { HandoffMessage, HandoffPack, HandoffPackInput, TokenEstimator } from './types.js';
29
+
30
+ interface Tail {
31
+ /** Index in the non-system transcript where the verbatim tail begins. */
32
+ start: number;
33
+ tokens: number;
34
+ }
35
+
36
+ /** Length of the leading run of `system` messages. */
37
+ function leadingSystemCount(all: readonly HandoffMessage[]): number {
38
+ let n = 0;
39
+ while (n < all.length && all[n]!.role === 'system') n += 1;
40
+ return n;
41
+ }
42
+
43
+ /** The note's role: `user` before an assistant turn (or at the end), else `assistant`. */
44
+ function noteRole(next: HandoffMessage | undefined): 'user' | 'assistant' {
45
+ return next === undefined || next.role === 'assistant' ? 'user' : 'assistant';
46
+ }
47
+
48
+ /** Index of the last `user` message, or -1. */
49
+ function latestDirectiveIndex(convo: readonly HandoffMessage[]): number {
50
+ for (let i = convo.length - 1; i >= 0; i--) if (convo[i]!.role === 'user') return i;
51
+ return -1;
52
+ }
53
+
54
+ /**
55
+ * Longest suffix of whole segments that fits `room`, counting the directive's cost
56
+ * separately while it still sits before the suffix. Never leads with a `tool` result.
57
+ */
58
+ function selectTail(
59
+ convo: readonly HandoffMessage[],
60
+ segs: readonly Segment[],
61
+ directiveIdx: number,
62
+ directiveTokens: number,
63
+ room: number,
64
+ ): Tail {
65
+ let start = convo.length;
66
+ let tokens = 0;
67
+ let first = segs.length;
68
+ for (let s = segs.length - 1; s >= 0; s--) {
69
+ const seg = segs[s]!;
70
+ const directiveOutside = directiveIdx >= 0 && directiveIdx < seg.start;
71
+ if (tokens + seg.tokens + (directiveOutside ? directiveTokens : 0) > room) break;
72
+ tokens += seg.tokens;
73
+ start = seg.start;
74
+ first = s;
75
+ }
76
+ // Only an orphan result at the very front can lead a unit with `tool`; drop it.
77
+ while (first < segs.length && convo[segs[first]!.start]!.role === 'tool') {
78
+ tokens -= segs[first]!.tokens;
79
+ first += 1;
80
+ start = first < segs.length ? segs[first]!.start : convo.length;
81
+ }
82
+ return { start, tokens };
83
+ }
84
+
85
+ function result(
86
+ messages: HandoffMessage[],
87
+ dropped: number,
88
+ estimate: TokenEstimator,
89
+ extra: Partial<Pick<HandoffPack, 'summarized' | 'recalled' | 'fits'>> = {},
90
+ ): HandoffPack {
91
+ return {
92
+ messages,
93
+ dropped,
94
+ summarized: extra.summarized ?? false,
95
+ recalled: extra.recalled ?? false,
96
+ estimatedTokens: tokensOf(messages, estimate),
97
+ fits: extra.fits ?? true,
98
+ };
99
+ }
100
+
101
+ export async function packHandoff(input: HandoffPackInput): Promise<HandoffPack> {
102
+ const estimate = input.estimateTokens ?? defaultEstimateTokens;
103
+ const budget = handoffBudget(input);
104
+ const all = input.messages;
105
+
106
+ // Under budget: hand the transcript over untouched.
107
+ if (tokensOf(all, estimate) <= budget) return result(all.slice(), 0, estimate);
108
+
109
+ const prefixLen = leadingSystemCount(all);
110
+ const system = all.slice(0, prefixLen);
111
+ const convo = all.slice(prefixLen);
112
+ const systemTokens = tokensOf(system, estimate);
113
+ const directiveIdx = latestDirectiveIndex(convo);
114
+ const directive = directiveIdx >= 0 ? convo[directiveIdx] : undefined;
115
+ const directiveTokens = directive ? messageTokens(directive, estimate) : 0;
116
+
117
+ // The irreducible core does not fit: send it anyway and say so.
118
+ if (systemTokens + directiveTokens > budget) {
119
+ const core = directive ? [...system, directive] : [...system];
120
+ return result(core, convo.length - (directive ? 1 : 0), estimate, { fits: false });
121
+ }
122
+
123
+ // Reserve room for the smallest note (the marker, at its widest count) when it fits.
124
+ const header = handoffLine(input.from, input.to);
125
+ const minNote = noteTokens(composeNote(header, elisionMarker(convo.length)), estimate);
126
+ const noteFits = systemTokens + directiveTokens + minNote <= budget;
127
+ const room = budget - systemTokens - (noteFits ? minNote : 0);
128
+
129
+ const tail = selectTail(convo, segmentTranscript(convo, estimate), directiveIdx, directiveTokens, room);
130
+ const reinject = directive !== undefined && directiveIdx < tail.start;
131
+ const droppedMsgs = convo.slice(0, tail.start).filter((_, i) => !(reinject && i === directiveIdx));
132
+
133
+ const used = systemTokens + tail.tokens + (reinject ? directiveTokens : 0);
134
+ const note = droppedMsgs.length > 0
135
+ ? await buildNote({
136
+ header,
137
+ dropped: droppedMsgs,
138
+ directive,
139
+ room: budget - used,
140
+ estimate,
141
+ summarize: input.summarize,
142
+ recall: input.recall,
143
+ timeoutMs: input.timeoutMs,
144
+ })
145
+ : null;
146
+
147
+ const after: HandoffMessage[] = reinject && directive ? [directive] : [];
148
+ after.push(...convo.slice(tail.start));
149
+ const messages: HandoffMessage[] = [...system];
150
+ if (note) messages.push({ role: noteRole(after[0]), content: note.text });
151
+ messages.push(...after);
152
+ return result(messages, droppedMsgs.length, estimate, {
153
+ summarized: note?.summarized ?? false,
154
+ recalled: note?.recalled ?? false,
155
+ });
156
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Atomic units of a transcript — the pieces the packer keeps or drops whole.
3
+ *
4
+ * An assistant message carrying `tool_calls` and the `tool` results that follow it form
5
+ * ONE unit: strict vendors reject a tool result whose call is missing, and a call whose
6
+ * results are missing. Every other message is a unit of its own. A stray `tool` result
7
+ * with no owning call joins the unit before it, so it can never lead a verbatim tail.
8
+ */
9
+
10
+ import { hasToolCalls, messageTokens } from './budget.js';
11
+ import type { HandoffMessage, TokenEstimator } from './types.js';
12
+
13
+ /** A contiguous, never-split run of messages: `messages = convo.slice(start, end)`. */
14
+ export interface Segment {
15
+ start: number;
16
+ end: number;
17
+ tokens: number;
18
+ }
19
+
20
+ /** Split `convo` into atomic units, in order. */
21
+ export function segmentTranscript(convo: readonly HandoffMessage[], estimate: TokenEstimator): Segment[] {
22
+ const out: Segment[] = [];
23
+ let i = 0;
24
+ while (i < convo.length) {
25
+ const m = convo[i]!;
26
+ let end = i + 1;
27
+ if (m.role === 'assistant' && hasToolCalls(m)) {
28
+ while (end < convo.length && convo[end]!.role === 'tool') end += 1;
29
+ }
30
+ let tokens = 0;
31
+ for (let k = i; k < end; k++) tokens += messageTokens(convo[k]!, estimate);
32
+ const prev = out[out.length - 1];
33
+ if (m.role === 'tool' && prev) {
34
+ prev.end = end;
35
+ prev.tokens += tokens;
36
+ } else {
37
+ out.push({ start: i, end, tokens });
38
+ }
39
+ i = end;
40
+ }
41
+ return out;
42
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The hand-off contract (PRD 34 §3.3) — the shapes a host passes in and gets back when
3
+ * a conversation moves from one model to another whose context window may be smaller.
4
+ * Pure data: no runtime imports, so the subpath stays Worker/browser/Node safe.
5
+ */
6
+
7
+ /** One chat message in the OpenAI-style shape every Builderforce host already speaks. */
8
+ export interface HandoffMessage {
9
+ role: 'system' | 'user' | 'assistant' | 'tool';
10
+ content: unknown;
11
+ tool_calls?: unknown;
12
+ tool_call_id?: string;
13
+ }
14
+
15
+ /** What {@link packHandoff} packs, and the budget it packs into. */
16
+ export interface HandoffPackInput {
17
+ messages: readonly HandoffMessage[];
18
+ /** The NEXT model's context window, in tokens. */
19
+ targetContextTokens: number;
20
+ /** Tokens held back for the next model's reply. */
21
+ reserveOutputTokens: number;
22
+ /** Tokens the advertised tool schemas cost. */
23
+ toolsTokens?: number;
24
+ /** Model refs, for the one-line hand-off note. */
25
+ from?: string;
26
+ to?: string;
27
+ /** Memory block for the latest directive, at most `budgetChars` long. */
28
+ recall?: (query: string, budgetChars: number) => Promise<string>;
29
+ /** Summary of the dropped middle. */
30
+ summarize?: (dropped: readonly HandoffMessage[]) => Promise<string>;
31
+ /** Token estimate for a text. Default: chars/4, rounded up. */
32
+ estimateTokens?: (text: string) => number;
33
+ /**
34
+ * Optional ceiling (ms) on each of `summarize` / `recall`. A call that outlives it is
35
+ * treated as a failure and packing falls through to the next note source. No ceiling
36
+ * when omitted.
37
+ */
38
+ timeoutMs?: number;
39
+ }
40
+
41
+ /** The packed transcript and how it was made. */
42
+ export interface HandoffPack {
43
+ messages: HandoffMessage[];
44
+ /** Messages from the input that are in neither the verbatim tail nor re-injected. */
45
+ dropped: number;
46
+ /** The note carries the `summarize` result. */
47
+ summarized: boolean;
48
+ /** The note carries the `recall` result. */
49
+ recalled: boolean;
50
+ estimatedTokens: number;
51
+ /** False only when the system message(s) + the latest directive alone exceed the budget. */
52
+ fits: boolean;
53
+ }
54
+
55
+ /** The three numbers the usable budget is computed from. */
56
+ export interface HandoffBudgetInput {
57
+ targetContextTokens: number;
58
+ reserveOutputTokens: number;
59
+ toolsTokens?: number;
60
+ }
61
+
62
+ /** A text → token-count estimator. */
63
+ export type TokenEstimator = (text: string) => number;