@animalabs/context-manager 0.5.3 → 0.5.5
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/dist/src/adaptive/index.d.ts +6 -0
- package/dist/src/adaptive/index.d.ts.map +1 -1
- package/dist/src/adaptive/index.js +8 -0
- package/dist/src/adaptive/index.js.map +1 -1
- package/dist/src/adaptive/kv-cache-sim.d.ts +128 -0
- package/dist/src/adaptive/kv-cache-sim.d.ts.map +1 -0
- package/dist/src/adaptive/kv-cache-sim.js +0 -0
- package/dist/src/adaptive/kv-cache-sim.js.map +1 -0
- package/dist/src/adaptive/kv-control.d.ts +166 -0
- package/dist/src/adaptive/kv-control.d.ts.map +1 -0
- package/dist/src/adaptive/kv-control.js +434 -0
- package/dist/src/adaptive/kv-control.js.map +1 -0
- package/dist/src/adaptive/kv-replay.d.ts +76 -0
- package/dist/src/adaptive/kv-replay.d.ts.map +1 -0
- package/dist/src/adaptive/kv-replay.js +175 -0
- package/dist/src/adaptive/kv-replay.js.map +1 -0
- package/dist/src/adaptive/picker.d.ts +13 -0
- package/dist/src/adaptive/picker.d.ts.map +1 -1
- package/dist/src/adaptive/picker.js.map +1 -1
- package/dist/src/adaptive/render-offsets.d.ts +61 -0
- package/dist/src/adaptive/render-offsets.d.ts.map +1 -0
- package/dist/src/adaptive/render-offsets.js +88 -0
- package/dist/src/adaptive/render-offsets.js.map +1 -0
- package/dist/src/adaptive/strategies/kv-stable.d.ts +48 -0
- package/dist/src/adaptive/strategies/kv-stable.d.ts.map +1 -0
- package/dist/src/adaptive/strategies/kv-stable.js +153 -0
- package/dist/src/adaptive/strategies/kv-stable.js.map +1 -0
- package/dist/src/adaptive/summary-tree.d.ts +98 -0
- package/dist/src/adaptive/summary-tree.d.ts.map +1 -0
- package/dist/src/adaptive/summary-tree.js +214 -0
- package/dist/src/adaptive/summary-tree.js.map +1 -0
- package/dist/src/context-log.d.ts.map +1 -1
- package/dist/src/context-log.js +7 -2
- package/dist/src/context-log.js.map +1 -1
- package/dist/src/context-manager.d.ts +10 -5
- package/dist/src/context-manager.d.ts.map +1 -1
- package/dist/src/context-manager.js +9 -0
- package/dist/src/context-manager.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/message-store.js +1 -1
- package/dist/src/message-store.js.map +1 -1
- package/dist/src/phase-channel.d.ts +13 -0
- package/dist/src/phase-channel.d.ts.map +1 -0
- package/dist/src/phase-channel.js +55 -0
- package/dist/src/phase-channel.js.map +1 -0
- package/dist/src/strategies/autobiographical.d.ts +107 -44
- package/dist/src/strategies/autobiographical.d.ts.map +1 -1
- package/dist/src/strategies/autobiographical.js +437 -230
- package/dist/src/strategies/autobiographical.js.map +1 -1
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/types/index.d.ts.map +1 -1
- package/dist/src/types/index.js.map +1 -1
- package/dist/src/types/strategy.d.ts +70 -7
- package/dist/src/types/strategy.d.ts.map +1 -1
- package/dist/src/types/strategy.js +2 -0
- package/dist/src/types/strategy.js.map +1 -1
- package/dist/test/adaptive/kv-cache-sim.test.d.ts +16 -0
- package/dist/test/adaptive/kv-cache-sim.test.d.ts.map +1 -0
- package/dist/test/adaptive/kv-cache-sim.test.js +173 -0
- package/dist/test/adaptive/kv-cache-sim.test.js.map +1 -0
- package/dist/test/adaptive/kv-control.test.d.ts +14 -0
- package/dist/test/adaptive/kv-control.test.d.ts.map +1 -0
- package/dist/test/adaptive/kv-control.test.js +130 -0
- package/dist/test/adaptive/kv-control.test.js.map +1 -0
- package/dist/test/adaptive/kv-replay.test.d.ts +15 -0
- package/dist/test/adaptive/kv-replay.test.d.ts.map +1 -0
- package/dist/test/adaptive/kv-replay.test.js +112 -0
- package/dist/test/adaptive/kv-replay.test.js.map +1 -0
- package/dist/test/adaptive/kv-stable-strategy.test.d.ts +10 -0
- package/dist/test/adaptive/kv-stable-strategy.test.d.ts.map +1 -0
- package/dist/test/adaptive/kv-stable-strategy.test.js +89 -0
- package/dist/test/adaptive/kv-stable-strategy.test.js.map +1 -0
- package/dist/test/adaptive/pin-at-level.test.d.ts +16 -0
- package/dist/test/adaptive/pin-at-level.test.d.ts.map +1 -0
- package/dist/test/adaptive/pin-at-level.test.js +133 -0
- package/dist/test/adaptive/pin-at-level.test.js.map +1 -0
- package/dist/test/adaptive/render-offsets.test.d.ts +11 -0
- package/dist/test/adaptive/render-offsets.test.d.ts.map +1 -0
- package/dist/test/adaptive/render-offsets.test.js +85 -0
- package/dist/test/adaptive/render-offsets.test.js.map +1 -0
- package/dist/test/adaptive/summary-tree.test.d.ts +5 -0
- package/dist/test/adaptive/summary-tree.test.d.ts.map +1 -0
- package/dist/test/adaptive/summary-tree.test.js +72 -0
- package/dist/test/adaptive/summary-tree.test.js.map +1 -0
- package/dist/test/image-strip-render-stats.test.d.ts +2 -0
- package/dist/test/image-strip-render-stats.test.d.ts.map +1 -0
- package/dist/test/image-strip-render-stats.test.js +130 -0
- package/dist/test/image-strip-render-stats.test.js.map +1 -0
- package/dist/test/image-stripping.test.d.ts +13 -0
- package/dist/test/image-stripping.test.d.ts.map +1 -0
- package/dist/test/image-stripping.test.js +104 -0
- package/dist/test/image-stripping.test.js.map +1 -0
- package/dist/test/pins.test.js +40 -0
- package/dist/test/pins.test.js.map +1 -1
- package/dist/test/render-stats.test.d.ts +2 -0
- package/dist/test/render-stats.test.d.ts.map +1 -0
- package/dist/test/render-stats.test.js +91 -0
- package/dist/test/render-stats.test.js.map +1 -0
- package/dist/test/speculation-cap.test.d.ts +9 -3
- package/dist/test/speculation-cap.test.d.ts.map +1 -1
- package/dist/test/speculation-cap.test.js +175 -3
- package/dist/test/speculation-cap.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/adaptive/index.ts +48 -0
- package/src/adaptive/kv-cache-sim.ts +0 -0
- package/src/adaptive/kv-control.ts +574 -0
- package/src/adaptive/kv-replay.ts +238 -0
- package/src/adaptive/picker.ts +13 -0
- package/src/adaptive/render-offsets.ts +116 -0
- package/src/adaptive/strategies/kv-stable.ts +161 -0
- package/src/adaptive/summary-tree.ts +252 -0
- package/src/context-log.ts +7 -2
- package/src/context-manager.ts +13 -2
- package/src/index.ts +3 -0
- package/src/message-store.ts +1 -1
- package/src/phase-channel.ts +57 -0
- package/src/strategies/autobiographical.ts +453 -282
- package/src/types/index.ts +1 -0
- package/src/types/strategy.ts +71 -3
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SummaryTree — a typed, structural view over the adaptive summary forest.
|
|
3
|
+
*
|
|
4
|
+
* Built from PickerInputs (chunks + summaries + per-summary recall-pair token
|
|
5
|
+
* counts). Provides the navigation a frontier solver needs — ancestor chains,
|
|
6
|
+
* recall-pair token costs, leaf coverage, root/children traversal — with NO
|
|
7
|
+
* rendering or resolution state. Pure and deterministic.
|
|
8
|
+
*
|
|
9
|
+
* Structure mirrors the model the picker already uses:
|
|
10
|
+
* - `chunk.l1Id` + the `SummaryEntry.parentId` chain define the upward path.
|
|
11
|
+
* - `SummaryEntry.sourceLevel === 0` means `sourceIds` are leaf (message) ids;
|
|
12
|
+
* otherwise they are child summary ids.
|
|
13
|
+
* - A folded node's token cost is its recall-pair size
|
|
14
|
+
* (`recallPairTokens` ?? `SummaryEntry.tokens`) — matching
|
|
15
|
+
* `MutableFoldingState.computeTokens`.
|
|
16
|
+
*
|
|
17
|
+
* This is the substrate for the V2 best-fit tree-knapsack DP. See
|
|
18
|
+
* `docs/best-fit-frontier-resolution.md` §3, §6, §11.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { ChunkId, SummaryId } from './folding-strategy.js';
|
|
22
|
+
import type { PickerInputs } from './picker.js';
|
|
23
|
+
import type { SummaryEntry } from '../types/strategy.js';
|
|
24
|
+
import { getSummaryParentId } from '../types/strategy.js';
|
|
25
|
+
|
|
26
|
+
/** A raw chunk (resolution 0) — a leaf of the summary forest. */
|
|
27
|
+
export interface LeafNode {
|
|
28
|
+
kind: 'leaf';
|
|
29
|
+
chunkId: ChunkId;
|
|
30
|
+
/** Position in source order (lower = older). */
|
|
31
|
+
sequence: number;
|
|
32
|
+
rawTokens: number;
|
|
33
|
+
/** L1 summary covering this leaf, if any. */
|
|
34
|
+
l1Id?: SummaryId;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** An L_k summary node covering a contiguous range of leaves. */
|
|
38
|
+
export interface SummaryNode {
|
|
39
|
+
kind: 'summary';
|
|
40
|
+
id: SummaryId;
|
|
41
|
+
level: number;
|
|
42
|
+
/** Rendered tokens of this node's recall pair (the cost of folding to it). */
|
|
43
|
+
recallTokens: number;
|
|
44
|
+
/** Immediate children: leaf chunk ids (when childrenAreLeaves) or summary ids. */
|
|
45
|
+
childIds: string[];
|
|
46
|
+
/** True when childIds are leaf chunk ids (sourceLevel === 0). */
|
|
47
|
+
childrenAreLeaves: boolean;
|
|
48
|
+
/** All covered leaf chunk ids, recursively, in source order. */
|
|
49
|
+
leafChunkIds: ChunkId[];
|
|
50
|
+
/** Min/max source sequence of covered leaves (−1 if none resolve). */
|
|
51
|
+
firstSequence: number;
|
|
52
|
+
lastSequence: number;
|
|
53
|
+
sourceRange: { first: string; last: string };
|
|
54
|
+
/** Parent summary id, if a higher level has been produced. */
|
|
55
|
+
parentId?: SummaryId;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type TreeNode = LeafNode | SummaryNode;
|
|
59
|
+
|
|
60
|
+
/** Token cost of rendering a node collapsed: raw for a leaf, recall for a summary. */
|
|
61
|
+
export function nodeTokens(node: TreeNode): number {
|
|
62
|
+
return node.kind === 'leaf' ? node.rawTokens : node.recallTokens;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class SummaryTree {
|
|
66
|
+
private readonly leaves = new Map<ChunkId, LeafNode>();
|
|
67
|
+
private readonly nodes = new Map<SummaryId, SummaryNode>();
|
|
68
|
+
private readonly summaries: ReadonlyMap<SummaryId, SummaryEntry>;
|
|
69
|
+
private readonly recallPairTokens: ReadonlyMap<SummaryId, number>;
|
|
70
|
+
private readonly leafSeq = new Map<ChunkId, number>();
|
|
71
|
+
private rootCache: TreeNode[] | null = null;
|
|
72
|
+
|
|
73
|
+
constructor(inputs: PickerInputs) {
|
|
74
|
+
this.summaries = inputs.summaries;
|
|
75
|
+
this.recallPairTokens = inputs.recallPairTokens ?? new Map();
|
|
76
|
+
|
|
77
|
+
for (const c of inputs.chunks) {
|
|
78
|
+
this.leafSeq.set(c.id, c.sequence);
|
|
79
|
+
this.leaves.set(c.id, {
|
|
80
|
+
kind: 'leaf',
|
|
81
|
+
chunkId: c.id,
|
|
82
|
+
sequence: c.sequence,
|
|
83
|
+
rawTokens: c.rawTokens,
|
|
84
|
+
l1Id: c.l1Id,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
for (const [, s] of this.summaries) {
|
|
88
|
+
this.nodes.set(s.id, this.buildNode(s));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---- node access ----
|
|
93
|
+
|
|
94
|
+
/** All leaf nodes in source order (oldest first). */
|
|
95
|
+
orderedLeaves(): LeafNode[] {
|
|
96
|
+
return [...this.leaves.values()].sort((a, b) => a.sequence - b.sequence);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** All summary nodes (unordered). */
|
|
100
|
+
allSummaries(): SummaryNode[] {
|
|
101
|
+
return [...this.nodes.values()];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
leaf(chunkId: ChunkId): LeafNode | null {
|
|
105
|
+
return this.leaves.get(chunkId) ?? null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
summary(id: SummaryId): SummaryNode | null {
|
|
109
|
+
return this.nodes.get(id) ?? null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Recall-pair tokens for a summary, or null if unknown. */
|
|
113
|
+
recallTokens(id: SummaryId): number | null {
|
|
114
|
+
return this.nodes.get(id)?.recallTokens ?? null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Immediate children of a summary node (leaves or sub-summaries). */
|
|
118
|
+
children(node: SummaryNode): TreeNode[] {
|
|
119
|
+
const out: TreeNode[] = [];
|
|
120
|
+
if (node.childrenAreLeaves) {
|
|
121
|
+
for (const cid of node.childIds) {
|
|
122
|
+
const leaf = this.leaves.get(cid);
|
|
123
|
+
if (leaf) out.push(leaf);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
for (const sid of node.childIds) {
|
|
127
|
+
const sub = this.nodes.get(sid);
|
|
128
|
+
if (sub) out.push(sub);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** All leaf chunk ids under a summary, in source order. */
|
|
135
|
+
leavesUnder(id: SummaryId): ChunkId[] {
|
|
136
|
+
return this.nodes.get(id)?.leafChunkIds ?? [];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The L_level ancestor summary of a chunk, or null if not present. Matches
|
|
141
|
+
* `MutableFoldingState.ancestorAt` (walks the parentId chain from l1Id).
|
|
142
|
+
*/
|
|
143
|
+
ancestorAt(chunkId: ChunkId, level: number): SummaryNode | null {
|
|
144
|
+
if (level <= 0) return null;
|
|
145
|
+
const leaf = this.leaves.get(chunkId);
|
|
146
|
+
if (!leaf || !leaf.l1Id) return null;
|
|
147
|
+
let cur = this.nodes.get(leaf.l1Id);
|
|
148
|
+
while (cur && cur.level < level) {
|
|
149
|
+
if (!cur.parentId) return null;
|
|
150
|
+
cur = this.nodes.get(cur.parentId);
|
|
151
|
+
}
|
|
152
|
+
return cur && cur.level === level ? cur : null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Highest level k for which a summary covering this chunk exists (0 = none). */
|
|
156
|
+
maxLevel(chunkId: ChunkId): number {
|
|
157
|
+
const leaf = this.leaves.get(chunkId);
|
|
158
|
+
if (!leaf || !leaf.l1Id) return 0;
|
|
159
|
+
let cur = this.nodes.get(leaf.l1Id);
|
|
160
|
+
let max = 0;
|
|
161
|
+
while (cur) {
|
|
162
|
+
max = cur.level;
|
|
163
|
+
if (!cur.parentId) break;
|
|
164
|
+
cur = this.nodes.get(cur.parentId);
|
|
165
|
+
}
|
|
166
|
+
return max;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Forest roots over all chunks: top-level summaries (no parent present in the
|
|
171
|
+
* map) plus any leaf not covered by an L1. Each leaf belongs to exactly one
|
|
172
|
+
* root's subtree. Deterministically ordered by source sequence.
|
|
173
|
+
*/
|
|
174
|
+
roots(): TreeNode[] {
|
|
175
|
+
if (this.rootCache) return this.rootCache;
|
|
176
|
+
const out: TreeNode[] = [];
|
|
177
|
+
const covered = new Set<ChunkId>();
|
|
178
|
+
for (const node of this.nodes.values()) {
|
|
179
|
+
const parent = node.parentId ? this.nodes.get(node.parentId) : undefined;
|
|
180
|
+
if (parent) continue; // has a real parent → not a root
|
|
181
|
+
out.push(node);
|
|
182
|
+
for (const lid of node.leafChunkIds) covered.add(lid);
|
|
183
|
+
}
|
|
184
|
+
for (const leaf of this.leaves.values()) {
|
|
185
|
+
if (!covered.has(leaf.chunkId)) out.push(leaf);
|
|
186
|
+
}
|
|
187
|
+
out.sort((a, b) => sequenceOf(a) - sequenceOf(b));
|
|
188
|
+
this.rootCache = out;
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---- internals ----
|
|
193
|
+
|
|
194
|
+
private buildNode(s: SummaryEntry): SummaryNode {
|
|
195
|
+
const leafChunkIds = this.collectLeafIds(s);
|
|
196
|
+
let firstSequence = Infinity;
|
|
197
|
+
let lastSequence = -Infinity;
|
|
198
|
+
for (const id of leafChunkIds) {
|
|
199
|
+
const seq = this.leafSeq.get(id);
|
|
200
|
+
if (seq === undefined) continue;
|
|
201
|
+
if (seq < firstSequence) firstSequence = seq;
|
|
202
|
+
if (seq > lastSequence) lastSequence = seq;
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
kind: 'summary',
|
|
206
|
+
id: s.id,
|
|
207
|
+
level: s.level,
|
|
208
|
+
recallTokens: this.recallPairTokens.get(s.id) ?? s.tokens,
|
|
209
|
+
childIds: [...s.sourceIds],
|
|
210
|
+
childrenAreLeaves: s.sourceLevel === 0,
|
|
211
|
+
leafChunkIds,
|
|
212
|
+
firstSequence: firstSequence === Infinity ? -1 : firstSequence,
|
|
213
|
+
lastSequence: lastSequence === -Infinity ? -1 : lastSequence,
|
|
214
|
+
sourceRange: s.sourceRange,
|
|
215
|
+
parentId: getSummaryParentId(s),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Walk down to leaf chunk ids; sourceLevel 0 => sourceIds are leaves.
|
|
220
|
+
* Deduplicates leaves and guards against revisiting a summary, so a chunk
|
|
221
|
+
* reachable via multiple branches is counted once (real chronicles can have
|
|
222
|
+
* such overlap; double-counting breaks range/contiguity and value math). */
|
|
223
|
+
private collectLeafIds(summary: SummaryEntry): ChunkId[] {
|
|
224
|
+
const out: ChunkId[] = [];
|
|
225
|
+
const seenLeaves = new Set<ChunkId>();
|
|
226
|
+
const seenSummaries = new Set<SummaryId>();
|
|
227
|
+
const visit = (s: SummaryEntry): void => {
|
|
228
|
+
if (seenSummaries.has(s.id)) return;
|
|
229
|
+
seenSummaries.add(s.id);
|
|
230
|
+
if (s.sourceLevel === 0) {
|
|
231
|
+
for (const mid of s.sourceIds) {
|
|
232
|
+
if (!seenLeaves.has(mid)) {
|
|
233
|
+
seenLeaves.add(mid);
|
|
234
|
+
out.push(mid);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
for (const sid of s.sourceIds) {
|
|
239
|
+
const child = this.summaries.get(sid);
|
|
240
|
+
if (child) visit(child);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
visit(summary);
|
|
245
|
+
out.sort((a, b) => (this.leafSeq.get(a) ?? 0) - (this.leafSeq.get(b) ?? 0));
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function sequenceOf(n: TreeNode): number {
|
|
251
|
+
return n.kind === 'leaf' ? n.sequence : n.firstSequence;
|
|
252
|
+
}
|
package/src/context-log.ts
CHANGED
|
@@ -276,7 +276,12 @@ export class ContextLog {
|
|
|
276
276
|
case 'text':
|
|
277
277
|
return this.tokenEstimator(block.text);
|
|
278
278
|
case 'thinking':
|
|
279
|
-
|
|
279
|
+
// Signature-only blocks (display:'omitted') carry empty thinking text
|
|
280
|
+
return this.tokenEstimator(block.thinking ?? '');
|
|
281
|
+
case 'redacted_thinking':
|
|
282
|
+
// Encrypted reasoning payload, round-tripped verbatim — rough
|
|
283
|
+
// estimate from the data length so budgeting isn't blind to it.
|
|
284
|
+
return this.tokenEstimator((block as { data?: string }).data ?? '');
|
|
280
285
|
case 'tool_use':
|
|
281
286
|
return this.tokenEstimator(JSON.stringify(block.input)) + 20;
|
|
282
287
|
case 'tool_result':
|
|
@@ -286,7 +291,7 @@ export class ContextLog {
|
|
|
286
291
|
}
|
|
287
292
|
return block.content.reduce((sum, b) => sum + this.estimateBlockTokens(b), 0);
|
|
288
293
|
case 'image':
|
|
289
|
-
return block.tokenEstimate ??
|
|
294
|
+
return block.tokenEstimate ?? 1600; // ~1568px image ≈ 1600 tokens (Anthropic)
|
|
290
295
|
case 'document':
|
|
291
296
|
case 'audio':
|
|
292
297
|
case 'video':
|
package/src/context-manager.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
ContextInjection,
|
|
17
17
|
CompileResult,
|
|
18
18
|
ProtectedRange,
|
|
19
|
+
PinLevelOptions,
|
|
19
20
|
SearchQuery,
|
|
20
21
|
SearchResult,
|
|
21
22
|
SummaryEntry,
|
|
@@ -640,19 +641,29 @@ export class ContextManager {
|
|
|
640
641
|
*
|
|
641
642
|
* Throws if the active strategy doesn't support pins.
|
|
642
643
|
*/
|
|
643
|
-
pinRange(firstMessageId: MessageId, lastMessageId: MessageId, opts?:
|
|
644
|
+
pinRange(firstMessageId: MessageId, lastMessageId: MessageId, opts?: PinLevelOptions): string {
|
|
644
645
|
if (!isPinnableStrategy(this.strategy)) {
|
|
645
646
|
throw new Error('Active strategy does not support pins');
|
|
646
647
|
}
|
|
647
648
|
return this.strategy.pinRange(firstMessageId, lastMessageId, opts);
|
|
648
649
|
}
|
|
649
650
|
|
|
651
|
+
/**
|
|
652
|
+
* V2 dynamic pin-at-level-k: fix a range to render at EXACTLY fold level
|
|
653
|
+
* `level` (0 = raw) — the frontier cut passes through that L_k node. Honored
|
|
654
|
+
* only by `foldingStrategy: 'kv-stable'`; other strategies fall back to
|
|
655
|
+
* treating the range as raw. Returns the new pin id.
|
|
656
|
+
*/
|
|
657
|
+
pinAtLevel(firstMessageId: MessageId, lastMessageId: MessageId, level: number, opts?: { name?: string }): string {
|
|
658
|
+
return this.pinRange(firstMessageId, lastMessageId, { name: opts?.name, level });
|
|
659
|
+
}
|
|
660
|
+
|
|
650
661
|
/**
|
|
651
662
|
* Mark a single message as a "document" (semantically a body of
|
|
652
663
|
* information to retain in full). Same effect as a single-message pin
|
|
653
664
|
* with `kind: 'document'`. Returns the new pin id.
|
|
654
665
|
*/
|
|
655
|
-
markDocument(messageId: MessageId, opts?:
|
|
666
|
+
markDocument(messageId: MessageId, opts?: PinLevelOptions): string {
|
|
656
667
|
if (!isPinnableStrategy(this.strategy)) {
|
|
657
668
|
throw new Error('Active strategy does not support documents');
|
|
658
669
|
}
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
export { ContextManager } from './context-manager.js';
|
|
3
3
|
export type { ContextManagerConfig } from './context-manager.js';
|
|
4
4
|
|
|
5
|
+
// Phase channel (liveness-watchdog observability hook)
|
|
6
|
+
export { phaseChannel, enterPhase, withPhase, withPhaseAsync } from './phase-channel.js';
|
|
7
|
+
|
|
5
8
|
// Storage
|
|
6
9
|
export { MessageStore } from './message-store.js';
|
|
7
10
|
export type { MessageStoreEvent, MessageStoreListener } from './message-store.js';
|
package/src/message-store.ts
CHANGED
|
@@ -409,7 +409,7 @@ export class MessageStore {
|
|
|
409
409
|
}
|
|
410
410
|
return 0;
|
|
411
411
|
case 'image':
|
|
412
|
-
return block.tokenEstimate ??
|
|
412
|
+
return block.tokenEstimate ?? 1600; // ~1568px image ≈ 1600 tokens (Anthropic)
|
|
413
413
|
case 'document':
|
|
414
414
|
case 'audio':
|
|
415
415
|
case 'video':
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase channel — a process-wide observability hook for labelling the
|
|
3
|
+
* synchronous operation the main thread is currently running.
|
|
4
|
+
*
|
|
5
|
+
* Long synchronous work (context assembly, compression, merge-graph walks) can
|
|
6
|
+
* wedge the single-threaded event loop. An external liveness watchdog (in the
|
|
7
|
+
* host/agent-framework) can detect the wedge but not name what was running —
|
|
8
|
+
* unless the running code labels itself here. This module is a no-op sink by
|
|
9
|
+
* default; the watchdog installs `report` on start so labels reach its wedge
|
|
10
|
+
* report. context-manager owns the channel (no upward dependency); the watchdog
|
|
11
|
+
* (which depends on context-manager) wires it.
|
|
12
|
+
*/
|
|
13
|
+
const stack: string[] = [];
|
|
14
|
+
|
|
15
|
+
export const phaseChannel: {
|
|
16
|
+
/** Installed by the watchdog; receives the current (innermost) phase label. */
|
|
17
|
+
report: (label: string) => void;
|
|
18
|
+
} = {
|
|
19
|
+
report: () => {},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Enter a named phase. Returns a disposer that restores the previous phase. */
|
|
23
|
+
export function enterPhase(label: string): () => void {
|
|
24
|
+
stack.push(label);
|
|
25
|
+
phaseChannel.report(label);
|
|
26
|
+
let done = false;
|
|
27
|
+
return () => {
|
|
28
|
+
if (done) return;
|
|
29
|
+
done = true;
|
|
30
|
+
// Pop this label (tolerate out-of-order disposal defensively).
|
|
31
|
+
const idx = stack.lastIndexOf(label);
|
|
32
|
+
if (idx !== -1) stack.splice(idx, 1);
|
|
33
|
+
phaseChannel.report(stack[stack.length - 1] ?? 'idle');
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Run a synchronous section under a phase label (nesting-safe). */
|
|
38
|
+
export function withPhase<T>(label: string, fn: () => T): T {
|
|
39
|
+
const leave = enterPhase(label);
|
|
40
|
+
try {
|
|
41
|
+
return fn();
|
|
42
|
+
} finally {
|
|
43
|
+
leave();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Run an async section under a phase label. Note: this labels across awaits,
|
|
48
|
+
* which is what we want — the synchronous spans inside (where a wedge would
|
|
49
|
+
* occur) stay attributed to this phase. */
|
|
50
|
+
export async function withPhaseAsync<T>(label: string, fn: () => Promise<T>): Promise<T> {
|
|
51
|
+
const leave = enterPhase(label);
|
|
52
|
+
try {
|
|
53
|
+
return await fn();
|
|
54
|
+
} finally {
|
|
55
|
+
leave();
|
|
56
|
+
}
|
|
57
|
+
}
|