pi-blackhole 0.4.2 → 0.4.3

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 (87) hide show
  1. package/README.md +15 -0
  2. package/dist/index.js +11660 -0
  3. package/dist/index.js.map +1 -0
  4. package/example-config.json +1 -1
  5. package/index.ts +37 -63
  6. package/package.json +21 -9
  7. package/src/commands/cleanup.ts +279 -240
  8. package/src/commands/memory.ts +236 -184
  9. package/src/commands/pi-vcc.ts +202 -152
  10. package/src/commands/vcc-recall.ts +126 -95
  11. package/src/core/brief.ts +167 -33
  12. package/src/core/build-sections.ts +8 -2
  13. package/src/core/config-env.ts +117 -0
  14. package/src/core/content.ts +31 -7
  15. package/src/core/drill-down.ts +41 -11
  16. package/src/core/filter-noise.ts +9 -3
  17. package/src/core/format-recall.ts +15 -6
  18. package/src/core/format.ts +14 -4
  19. package/src/core/lineage.ts +9 -3
  20. package/src/core/load-messages.ts +24 -5
  21. package/src/core/normalize.ts +38 -14
  22. package/src/core/recall-scope.ts +11 -3
  23. package/src/core/render-entries.ts +22 -6
  24. package/src/core/sanitize.ts +5 -1
  25. package/src/core/search-entries.ts +111 -19
  26. package/src/core/settings.ts +1 -3
  27. package/src/core/summarize.ts +42 -21
  28. package/src/core/unified-config.ts +549 -411
  29. package/src/extract/commits.ts +4 -2
  30. package/src/extract/files.ts +10 -5
  31. package/src/extract/goals.ts +7 -2
  32. package/src/hooks/before-compact.ts +210 -88
  33. package/src/om/agents/dropper/agent.ts +380 -265
  34. package/src/om/agents/dropper/coverage.ts +102 -82
  35. package/src/om/agents/observer/agent.ts +242 -206
  36. package/src/om/agents/reflector/agent.ts +212 -153
  37. package/src/om/cleanup.ts +239 -218
  38. package/src/om/clipboard.ts +59 -51
  39. package/src/om/compaction-trigger.ts +448 -333
  40. package/src/om/config.ts +13 -6
  41. package/src/om/configure-overlay.ts +518 -355
  42. package/src/om/consolidation.ts +1460 -953
  43. package/src/om/cooldown.ts +75 -65
  44. package/src/om/debug-log.ts +86 -68
  45. package/src/om/ids.ts +1 -1
  46. package/src/om/ledger/fold.ts +89 -78
  47. package/src/om/ledger/progress.ts +181 -153
  48. package/src/om/ledger/projection.ts +248 -185
  49. package/src/om/ledger/recall.ts +247 -196
  50. package/src/om/ledger/render-summary.ts +79 -50
  51. package/src/om/ledger/types.ts +146 -117
  52. package/src/om/model-budget.ts +23 -13
  53. package/src/om/pending.ts +243 -179
  54. package/src/om/provider-stream.ts +52 -7
  55. package/src/om/retryable-error.ts +12 -16
  56. package/src/om/reverse-recall.ts +97 -91
  57. package/src/om/runtime.ts +474 -375
  58. package/src/om/serialize.ts +190 -166
  59. package/src/om/status-overlay.ts +246 -195
  60. package/src/om/tokens.ts +28 -21
  61. package/src/pi-base/blackhole-settings.ts +437 -0
  62. package/src/pi-base/config-manager.ts +440 -0
  63. package/src/pi-base/config.ts +469 -0
  64. package/src/pi-base/env.ts +43 -0
  65. package/src/pi-base/paths.ts +47 -0
  66. package/src/pi-base/settings/body.ts +1648 -0
  67. package/src/pi-base/settings/fields/action.ts +43 -0
  68. package/src/pi-base/settings/fields/boolean.ts +47 -0
  69. package/src/pi-base/settings/fields/custom.ts +72 -0
  70. package/src/pi-base/settings/fields/enum.ts +310 -0
  71. package/src/pi-base/settings/fields/index.ts +46 -0
  72. package/src/pi-base/settings/fields/model.ts +452 -0
  73. package/src/pi-base/settings/fields/string.ts +527 -0
  74. package/src/pi-base/settings/fields/text.ts +115 -0
  75. package/src/pi-base/settings/frame.ts +197 -0
  76. package/src/pi-base/settings/index.ts +77 -0
  77. package/src/pi-base/settings/inline-edit.ts +313 -0
  78. package/src/pi-base/settings/modal.ts +152 -0
  79. package/src/pi-base/settings/types.ts +500 -0
  80. package/src/pi-base/settings/validate-field.ts +113 -0
  81. package/src/pi-base/shell.ts +117 -0
  82. package/src/pi-base/types.ts +6 -0
  83. package/src/pi-base/ui.ts +32 -0
  84. package/src/tools/recall.ts +347 -225
  85. package/src/types.ts +20 -3
  86. package/tsup.config.ts +23 -0
  87. package/vitest.config.ts +15 -15
@@ -15,11 +15,11 @@ import { matchesKey } from "@earendil-works/pi-tui";
15
15
  // ---------------------------------------------------------------------------
16
16
 
17
17
  type ThemeShim = {
18
- fg: (style: string, text: string) => string;
18
+ fg: (style: string, text: string) => string;
19
19
  };
20
20
 
21
21
  const EMPTY_THEME: ThemeShim = {
22
- fg: (_s: string, t: string) => t,
22
+ fg: (_s: string, t: string) => t,
23
23
  };
24
24
 
25
25
  // ---------------------------------------------------------------------------
@@ -27,20 +27,20 @@ const EMPTY_THEME: ThemeShim = {
27
27
  // ---------------------------------------------------------------------------
28
28
 
29
29
  export interface StatusInfo {
30
- compaction: "auto" | "manual" | "off";
31
- compactionEngine: "blackhole" | "pi-default";
32
- tailBehavior: "pi-default" | "minimal";
33
- memory: boolean;
34
- compactAfterTokens: number;
35
- consolidationInFlight: boolean;
36
- compactInFlight: boolean;
37
- lastObserverError?: string;
38
- lastReflectorError?: string;
39
- lastDropperError?: string;
30
+ compaction: "auto" | "manual" | "off";
31
+ compactionEngine: "blackhole" | "pi-default";
32
+ tailBehavior: "pi-default" | "minimal";
33
+ memory: boolean;
34
+ compactAfterTokens: number;
35
+ consolidationInFlight: boolean;
36
+ compactInFlight: boolean;
37
+ lastObserverError?: string;
38
+ lastReflectorError?: string;
39
+ lastDropperError?: string;
40
40
  }
41
41
 
42
42
  export interface StatusResult {
43
- action: "configure" | "close";
43
+ action: "configure" | "close";
44
44
  }
45
45
 
46
46
  /**
@@ -58,186 +58,237 @@ export interface StatusResult {
58
58
  * ```
59
59
  */
60
60
  export function createStatusOverlay(
61
- info: StatusInfo,
62
- theme: unknown,
63
- tui: { requestRender: () => void },
64
- done: (result: StatusResult | undefined) => void,
65
- ): { render(width: number): string[]; handleInput(data: string): void; invalidate(): void; dispose(): void } {
66
- const th: ThemeShim = (theme && typeof (theme as Record<string, unknown>).fg === "function")
67
- ? theme as ThemeShim
68
- : EMPTY_THEME;
69
-
70
- const fg = (style: string, text: string) => th.fg(style, text);
71
- const dim = (s: string) => fg("dim", s);
72
- const accent = (s: string) => fg("accent", s);
73
- const success = (s: string) => fg("success", s);
74
- const warning = (s: string) => fg("warning", s);
75
- const error = (s: string) => fg("error", s);
76
- const muted = (s: string) => fg("muted", s);
77
-
78
- const configItems: { label: string; value: string }[] = [
79
- { label: "Compaction", value: info.compaction },
80
- { label: "Engine", value: info.compactionEngine },
81
- { label: "Tail behavior", value: info.tailBehavior },
82
- { label: "Memory", value: info.memory ? "Enabled" : "Disabled" },
83
- { label: "Threshold", value: `${info.compactAfterTokens.toLocaleString()} tok` },
84
- ];
85
-
86
- const pipelineItems: { label: string; value: string }[] = [
87
- { label: "Compaction in flight", value: info.compactInFlight ? "yes" : "no" },
88
- { label: "Consolidation in flight", value: info.consolidationInFlight ? "yes" : "no" },
89
- ];
90
-
91
- const errorItems: { label: string; value: string }[] = [];
92
- if (info.lastObserverError) errorItems.push({ label: "Observer", value: info.lastObserverError });
93
- if (info.lastReflectorError) errorItems.push({ label: "Reflector", value: info.lastReflectorError });
94
- if (info.lastDropperError) errorItems.push({ label: "Dropper", value: info.lastDropperError });
95
-
96
- const actionItems = [
97
- { label: "Open configure overlay", value: "configure" as const },
98
- { label: "Close", value: "close" as const },
99
- ];
100
-
101
- // Contiguous selectable item model: no section headers in nav indices
102
- const selectableConfigCount = configItems.length;
103
- const selectablePipelineCount = pipelineItems.length;
104
- const selectableErrorCount = errorItems.length;
105
- const selectableActionCount = actionItems.length;
106
- const totalSelectable = selectableConfigCount + selectablePipelineCount + selectableErrorCount + selectableActionCount;
107
-
108
- // Render helpers: map a contiguous nav index to the display positions
109
- // (section headers are inserted at render time, not in the nav model)
110
- const navIsAction = (idx: number) => idx >= selectableConfigCount + selectablePipelineCount + selectableErrorCount;
111
-
112
- let selectedIndex = 0;
113
- let cachedLines: string[] | undefined;
114
-
115
- function invalidate(): void {
116
- cachedLines = undefined;
117
- try { tui.requestRender(); } catch { /* no-op */ }
118
- }
119
-
120
- function handleInput(data: string): void {
121
- if (matchesKey(data, "escape")) {
122
- done({ action: "close" });
123
- return;
124
- }
125
-
126
- if (matchesKey(data, "enter") || matchesKey(data, "space")) {
127
- if (navIsAction(selectedIndex)) {
128
- const actionIdx = selectedIndex - selectableConfigCount - selectablePipelineCount - selectableErrorCount;
129
- const action = actionItems[actionIdx]!;
130
- done({ action: action.value });
131
- return;
132
- }
133
- return;
134
- }
135
-
136
- if (matchesKey(data, "up")) {
137
- selectedIndex = Math.max(0, selectedIndex - 1);
138
- invalidate();
139
- return;
140
- }
141
- if (matchesKey(data, "down")) {
142
- selectedIndex = Math.min(totalSelectable - 1, selectedIndex + 1);
143
- invalidate();
144
- return;
145
- }
146
- }
147
-
148
- function render(width: number): string[] {
149
- if (cachedLines) return cachedLines;
150
-
151
- const w = Math.max(2, Math.min(width - 2, 74));
152
- const innerW = w - 4;
153
-
154
- const pad = (s: string, len: number) => {
155
- const vis = visibleWidth(s);
156
- return s + " ".repeat(Math.max(0, len - vis));
157
- };
158
-
159
- const line = (content: string) => `│ ${pad(content, innerW)} │`;
160
- const border = (s: string) => fg("border", s);
161
-
162
- const lines: string[] = [];
163
-
164
- // Top border + header
165
- lines.push(fg("border", `╭${"─".repeat(w - 2)}╮`));
166
- lines.push(fg("border", `│ ${accent("Blackhole Status")}${" ".repeat(Math.max(0, innerW + 1 - 16))}│`));
167
- lines.push(fg("border", `├${"─".repeat(w - 2)}┤`));
168
-
169
- // Config section
170
- lines.push(border(line(` ${dim("─── Compaction Config ───")}`)));
171
- for (let i = 0; i < configItems.length; i++) {
172
- const item = configItems[i]!;
173
- const isSelected = selectedIndex === i;
174
- const prefix = isSelected ? accent(" ›") : " ";
175
- const label = isSelected ? accent(`${item.label}:`) : `${item.label}:`;
176
- let value: string;
177
- switch (item.label) {
178
- case "Compaction":
179
- value = item.value === "auto" ? accent(item.value) : item.value === "off" ? muted(item.value) : dim(item.value);
180
- break;
181
- case "Engine":
182
- value = item.value === "blackhole" ? success(item.value) : dim(item.value);
183
- break;
184
- case "Memory":
185
- value = item.value === "Enabled" ? success("Enabled") : muted("Disabled");
186
- break;
187
- default:
188
- value = dim(item.value);
189
- }
190
- lines.push(border(line(`${prefix} ${pad(label, 16)} ${value}`)));
191
- }
192
-
193
- // Pipeline section
194
- if (pipelineItems.length > 0) {
195
- lines.push(border(line(` ${dim("─── Pipeline ───")}`)));
196
- for (let i = 0; i < pipelineItems.length; i++) {
197
- const item = pipelineItems[i]!;
198
- const isSelected = selectedIndex === selectableConfigCount + i;
199
- const prefix = isSelected ? accent(" ›") : " ";
200
- const label = isSelected ? accent(`${item.label}:`) : `${item.label}:`;
201
- const value = item.value === "yes" ? warning("yes") : dim("no");
202
- lines.push(border(line(`${prefix} ${pad(label, 16)} ${value}`)));
203
- }
204
- }
205
-
206
- // Errors section
207
- if (errorItems.length > 0) {
208
- lines.push(border(line(` ${dim("─── Last Errors ───")}`)));
209
- for (let i = 0; i < errorItems.length; i++) {
210
- const item = errorItems[i]!;
211
- const isSelected = selectedIndex === selectableConfigCount + selectablePipelineCount + i;
212
- const prefix = isSelected ? accent(" ›") : " ";
213
- const label = isSelected ? accent(`${item.label}:`) : `${item.label}:`;
214
- const truncated = item.value.length > 40
215
- ? item.value.slice(0, 37) + "..."
216
- : item.value;
217
- lines.push(border(line(`${prefix} ${pad(label, 16)} ${error(truncated)}`)));
218
- }
219
- }
220
-
221
- // Separator
222
- lines.push(border(line(` ${dim("─── Actions ───")}`)));
223
-
224
- // Action items
225
- for (let i = 0; i < actionItems.length; i++) {
226
- const item = actionItems[i]!;
227
- const isSelected = selectedIndex === selectableConfigCount + selectablePipelineCount + selectableErrorCount + i;
228
- const prefix = isSelected ? accent(" ▶") : " ";
229
- const text = isSelected ? accent(item.label) : dim(item.label);
230
- lines.push(border(line(`${prefix} ${text}`)));
231
- }
232
-
233
- // Bottom hints
234
- lines.push(border(line("")));
235
- lines.push(border(dim(" ↑↓ navigate Enter/Space select Esc close ")));
236
- lines.push(fg("border", `╰${"─".repeat(w - 2)}╯`));
237
-
238
- cachedLines = lines;
239
- return lines;
240
- }
241
-
242
- return { render, handleInput, invalidate, dispose: () => {} };
61
+ info: StatusInfo,
62
+ theme: unknown,
63
+ tui: { requestRender: () => void },
64
+ done: (result: StatusResult | undefined) => void,
65
+ ): {
66
+ render(width: number): string[];
67
+ handleInput(data: string): void;
68
+ invalidate(): void;
69
+ dispose(): void;
70
+ } {
71
+ const th: ThemeShim =
72
+ theme && typeof (theme as Record<string, unknown>).fg === "function"
73
+ ? (theme as ThemeShim)
74
+ : EMPTY_THEME;
75
+
76
+ const fg = (style: string, text: string) => th.fg(style, text);
77
+ const dim = (s: string) => fg("dim", s);
78
+ const accent = (s: string) => fg("accent", s);
79
+ const success = (s: string) => fg("success", s);
80
+ const warning = (s: string) => fg("warning", s);
81
+ const error = (s: string) => fg("error", s);
82
+ const muted = (s: string) => fg("muted", s);
83
+
84
+ const configItems: { label: string; value: string }[] = [
85
+ { label: "Compaction", value: info.compaction },
86
+ { label: "Engine", value: info.compactionEngine },
87
+ { label: "Tail behavior", value: info.tailBehavior },
88
+ { label: "Memory", value: info.memory ? "Enabled" : "Disabled" },
89
+ {
90
+ label: "Threshold",
91
+ value: `${info.compactAfterTokens.toLocaleString()} tok`,
92
+ },
93
+ ];
94
+
95
+ const pipelineItems: { label: string; value: string }[] = [
96
+ {
97
+ label: "Compaction in flight",
98
+ value: info.compactInFlight ? "yes" : "no",
99
+ },
100
+ {
101
+ label: "Consolidation in flight",
102
+ value: info.consolidationInFlight ? "yes" : "no",
103
+ },
104
+ ];
105
+
106
+ const errorItems: { label: string; value: string }[] = [];
107
+ if (info.lastObserverError)
108
+ errorItems.push({ label: "Observer", value: info.lastObserverError });
109
+ if (info.lastReflectorError)
110
+ errorItems.push({ label: "Reflector", value: info.lastReflectorError });
111
+ if (info.lastDropperError)
112
+ errorItems.push({ label: "Dropper", value: info.lastDropperError });
113
+
114
+ const actionItems = [
115
+ { label: "Open configure overlay", value: "configure" as const },
116
+ { label: "Close", value: "close" as const },
117
+ ];
118
+
119
+ // Contiguous selectable item model: no section headers in nav indices
120
+ const selectableConfigCount = configItems.length;
121
+ const selectablePipelineCount = pipelineItems.length;
122
+ const selectableErrorCount = errorItems.length;
123
+ const selectableActionCount = actionItems.length;
124
+ const totalSelectable =
125
+ selectableConfigCount +
126
+ selectablePipelineCount +
127
+ selectableErrorCount +
128
+ selectableActionCount;
129
+
130
+ // Render helpers: map a contiguous nav index to the display positions
131
+ // (section headers are inserted at render time, not in the nav model)
132
+ const navIsAction = (idx: number) =>
133
+ idx >=
134
+ selectableConfigCount + selectablePipelineCount + selectableErrorCount;
135
+
136
+ let selectedIndex = 0;
137
+ let cachedLines: string[] | undefined;
138
+
139
+ function invalidate(): void {
140
+ cachedLines = undefined;
141
+ try {
142
+ tui.requestRender();
143
+ } catch {
144
+ /* no-op */
145
+ }
146
+ }
147
+
148
+ function handleInput(data: string): void {
149
+ if (matchesKey(data, "escape")) {
150
+ done({ action: "close" });
151
+ return;
152
+ }
153
+
154
+ if (matchesKey(data, "enter") || matchesKey(data, "space")) {
155
+ if (navIsAction(selectedIndex)) {
156
+ const actionIdx =
157
+ selectedIndex -
158
+ selectableConfigCount -
159
+ selectablePipelineCount -
160
+ selectableErrorCount;
161
+ const action = actionItems[actionIdx]!;
162
+ done({ action: action.value });
163
+ return;
164
+ }
165
+ return;
166
+ }
167
+
168
+ if (matchesKey(data, "up")) {
169
+ selectedIndex = Math.max(0, selectedIndex - 1);
170
+ invalidate();
171
+ return;
172
+ }
173
+ if (matchesKey(data, "down")) {
174
+ selectedIndex = Math.min(totalSelectable - 1, selectedIndex + 1);
175
+ invalidate();
176
+ return;
177
+ }
178
+ }
179
+
180
+ function render(width: number): string[] {
181
+ if (cachedLines) return cachedLines;
182
+
183
+ const w = Math.max(2, Math.min(width - 2, 74));
184
+ const innerW = w - 4;
185
+
186
+ const pad = (s: string, len: number) => {
187
+ const vis = visibleWidth(s);
188
+ return s + " ".repeat(Math.max(0, len - vis));
189
+ };
190
+
191
+ const line = (content: string) => `│ ${pad(content, innerW)} │`;
192
+ const border = (s: string) => fg("border", s);
193
+
194
+ const lines: string[] = [];
195
+
196
+ // Top border + header
197
+ lines.push(fg("border", `╭${"─".repeat(w - 2)}╮`));
198
+ lines.push(
199
+ fg(
200
+ "border",
201
+ `│ ${accent("Blackhole Status")}${" ".repeat(Math.max(0, innerW + 1 - 16))}│`,
202
+ ),
203
+ );
204
+ lines.push(fg("border", `├${"─".repeat(w - 2)}┤`));
205
+
206
+ // Config section
207
+ lines.push(border(line(` ${dim("─── Compaction Config ───")}`)));
208
+ for (let i = 0; i < configItems.length; i++) {
209
+ const item = configItems[i]!;
210
+ const isSelected = selectedIndex === i;
211
+ const prefix = isSelected ? accent(" ›") : " ";
212
+ const label = isSelected ? accent(`${item.label}:`) : `${item.label}:`;
213
+ let value: string;
214
+ switch (item.label) {
215
+ case "Compaction":
216
+ value =
217
+ item.value === "auto"
218
+ ? accent(item.value)
219
+ : item.value === "off"
220
+ ? muted(item.value)
221
+ : dim(item.value);
222
+ break;
223
+ case "Engine":
224
+ value =
225
+ item.value === "blackhole" ? success(item.value) : dim(item.value);
226
+ break;
227
+ case "Memory":
228
+ value =
229
+ item.value === "Enabled" ? success("Enabled") : muted("Disabled");
230
+ break;
231
+ default:
232
+ value = dim(item.value);
233
+ }
234
+ lines.push(border(line(`${prefix} ${pad(label, 16)} ${value}`)));
235
+ }
236
+
237
+ // Pipeline section
238
+ if (pipelineItems.length > 0) {
239
+ lines.push(border(line(` ${dim("─── Pipeline ───")}`)));
240
+ for (let i = 0; i < pipelineItems.length; i++) {
241
+ const item = pipelineItems[i]!;
242
+ const isSelected = selectedIndex === selectableConfigCount + i;
243
+ const prefix = isSelected ? accent(" ›") : " ";
244
+ const label = isSelected ? accent(`${item.label}:`) : `${item.label}:`;
245
+ const value = item.value === "yes" ? warning("yes") : dim("no");
246
+ lines.push(border(line(`${prefix} ${pad(label, 16)} ${value}`)));
247
+ }
248
+ }
249
+
250
+ // Errors section
251
+ if (errorItems.length > 0) {
252
+ lines.push(border(line(` ${dim("─── Last Errors ───")}`)));
253
+ for (let i = 0; i < errorItems.length; i++) {
254
+ const item = errorItems[i]!;
255
+ const isSelected =
256
+ selectedIndex === selectableConfigCount + selectablePipelineCount + i;
257
+ const prefix = isSelected ? accent(" ›") : " ";
258
+ const label = isSelected ? accent(`${item.label}:`) : `${item.label}:`;
259
+ const truncated =
260
+ item.value.length > 40 ? item.value.slice(0, 37) + "..." : item.value;
261
+ lines.push(
262
+ border(line(`${prefix} ${pad(label, 16)} ${error(truncated)}`)),
263
+ );
264
+ }
265
+ }
266
+
267
+ // Separator
268
+ lines.push(border(line(` ${dim("─── Actions ───")}`)));
269
+
270
+ // Action items
271
+ for (let i = 0; i < actionItems.length; i++) {
272
+ const item = actionItems[i]!;
273
+ const isSelected =
274
+ selectedIndex ===
275
+ selectableConfigCount +
276
+ selectablePipelineCount +
277
+ selectableErrorCount +
278
+ i;
279
+ const prefix = isSelected ? accent(" ▶") : " ";
280
+ const text = isSelected ? accent(item.label) : dim(item.label);
281
+ lines.push(border(line(`${prefix} ${text}`)));
282
+ }
283
+
284
+ // Bottom hints
285
+ lines.push(border(line("")));
286
+ lines.push(border(dim(" ↑↓ navigate Enter/Space select Esc close ")));
287
+ lines.push(fg("border", `╰${"─".repeat(w - 2)}╯`));
288
+
289
+ cachedLines = lines;
290
+ return lines;
291
+ }
292
+
293
+ return { render, handleInput, invalidate, dispose: () => {} };
243
294
  }
package/src/om/tokens.ts CHANGED
@@ -7,27 +7,34 @@
7
7
  import { estimateTokens as estimateMessageTokens } from "@earendil-works/pi-coding-agent";
8
8
 
9
9
  export function estimateStringTokens(text: string): number {
10
- return Math.ceil(text.length / 4);
10
+ return Math.ceil(text.length / 4);
11
11
  }
12
12
 
13
- export function estimateEntryTokens(entry: { type: string; message?: unknown; content?: unknown; summary?: unknown }): number {
14
- if (entry.type === "message" && entry.message) {
15
- return estimateMessageTokens(entry.message as Parameters<typeof estimateMessageTokens>[0]);
16
- }
17
- if (entry.type === "custom_message" && entry.content) {
18
- const content = entry.content;
19
- if (typeof content === "string") return estimateStringTokens(content);
20
- if (Array.isArray(content)) {
21
- let total = 0;
22
- for (const block of content) {
23
- if (block.type === "text" && block.text) total += estimateStringTokens(block.text);
24
- }
25
- return total;
26
- }
27
- }
28
- if (entry.type === "branch_summary" && typeof entry.summary === "string") {
29
- return estimateStringTokens(entry.summary);
30
- }
31
- return 0;
13
+ export function estimateEntryTokens(entry: {
14
+ type: string;
15
+ message?: unknown;
16
+ content?: unknown;
17
+ summary?: unknown;
18
+ }): number {
19
+ if (entry.type === "message" && entry.message) {
20
+ return estimateMessageTokens(
21
+ entry.message as Parameters<typeof estimateMessageTokens>[0],
22
+ );
23
+ }
24
+ if (entry.type === "custom_message" && entry.content) {
25
+ const content = entry.content;
26
+ if (typeof content === "string") return estimateStringTokens(content);
27
+ if (Array.isArray(content)) {
28
+ let total = 0;
29
+ for (const block of content) {
30
+ if (block.type === "text" && block.text)
31
+ total += estimateStringTokens(block.text);
32
+ }
33
+ return total;
34
+ }
35
+ }
36
+ if (entry.type === "branch_summary" && typeof entry.summary === "string") {
37
+ return estimateStringTokens(entry.summary);
38
+ }
39
+ return 0;
32
40
  }
33
-