pi-maestro-teammate 2.8.1 → 2.9.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-maestro-teammate",
3
- "version": "2.8.1",
3
+ "version": "2.9.0",
4
4
  "description": "Pi extension — teammate agent dispatch with DAG task graphs, RPC messaging, and compact TUI",
5
5
  "type": "module",
6
6
  "engines": {
@@ -177,7 +177,7 @@
177
177
  },
178
178
  "files": [
179
179
  "bin/*.mjs",
180
- "src/**/*.{ts,json}",
180
+ "src/**/*.{ts,mjs,json}",
181
181
  "types/**/*.d.ts",
182
182
  "agents/",
183
183
  "README.md"
@@ -2148,7 +2148,8 @@ export async function handleChildInteractionRequest(
2148
2148
  const details = isRecord(response.details) ? response.details : {};
2149
2149
  result = response.isError || details.cancelled || !Array.isArray(details.answers)
2150
2150
  ? { action: "cancel", error: response.isError ? "Root question broker failed" : undefined }
2151
- : { action: "answer", answers: details.answers, ...(Array.isArray(details.decisions) ? { decisions: details.decisions } : {}) };
2151
+ : { action: "answer", answers: details.answers, ...(Array.isArray(details.decisions) ? { decisions: details.decisions } : {}),
2152
+ ...(Array.isArray(details.recommendations) ? { recommendations: details.recommendations } : {}) };
2152
2153
  } else if (!ctx?.hasUI) {
2153
2154
  decisionSource = "headless";
2154
2155
  result = interaction === "permission" ? { action: "deny" } : { action: "cancel" };
@@ -868,27 +868,50 @@ export function appendBoundedTranscriptMessage(
868
868
 
869
869
  export function createUtf8LineDecoder(
870
870
  maxBufferedBytes = EXECUTION_BUFFER_LIMITS.lineBytes,
871
+ onOverflow?: (info: { receivedBytes: number; maxBytes: number; prefix: string }) => void,
872
+ onLine?: (line: string) => void,
871
873
  ): Utf8LineDecoder {
872
874
  const decoder = new StringDecoder("utf8");
873
875
  let buffered = "";
876
+ let receivedBytes = 0;
877
+ let prefix = "";
878
+ let overflowReported = false;
879
+ const append = (text: string): void => {
880
+ receivedBytes += Buffer.byteLength(text);
881
+ if (Buffer.byteLength(prefix) < 256) prefix = truncateUtf8Head(prefix + text.slice(0, 256), 256);
882
+ buffered = appendUtf8Tail(buffered, text, maxBufferedBytes);
883
+ if (!overflowReported && receivedBytes > maxBufferedBytes) {
884
+ overflowReported = true;
885
+ onOverflow?.({ receivedBytes, maxBytes: maxBufferedBytes, prefix });
886
+ }
887
+ };
888
+ const emit = (lines: string[]): void => {
889
+ const line = buffered.endsWith("\r") ? buffered.slice(0, -1) : buffered;
890
+ if (onLine) onLine(line);
891
+ else lines.push(line);
892
+ buffered = "";
893
+ receivedBytes = 0;
894
+ prefix = "";
895
+ overflowReported = false;
896
+ };
874
897
  return {
875
898
  write(chunk: Buffer): string[] {
876
899
  const text = decoder.write(chunk);
877
900
  const lines: string[] = [];
878
901
  let start = 0;
879
902
  for (let newline = text.indexOf("\n"); newline !== -1; newline = text.indexOf("\n", start)) {
880
- const line = appendUtf8Tail(buffered, text.slice(start, newline), maxBufferedBytes);
881
- lines.push(line.endsWith("\r") ? line.slice(0, -1) : line);
882
- buffered = "";
903
+ append(text.slice(start, newline));
904
+ emit(lines);
883
905
  start = newline + 1;
884
906
  }
885
- buffered = appendUtf8Tail(buffered, text.slice(start), maxBufferedBytes);
907
+ append(text.slice(start));
886
908
  return lines;
887
909
  },
888
910
  end(): string[] {
889
- const tail = appendUtf8Tail(buffered, decoder.end(), maxBufferedBytes);
890
- buffered = "";
891
- return tail ? [tail.endsWith("\r") ? tail.slice(0, -1) : tail] : [];
911
+ append(decoder.end());
912
+ const lines: string[] = [];
913
+ if (buffered) emit(lines);
914
+ return lines;
892
915
  },
893
916
  };
894
917
  }
@@ -106,6 +106,7 @@ import {
106
106
  releasePublishedTurnHistory,
107
107
  resetUsage,
108
108
  setUsageSnapshot,
109
+ truncateUtf8Head,
109
110
  truncateUtf8Tail,
110
111
  writeSchemaFile,
111
112
  writeSystemPromptFile,
@@ -875,6 +876,10 @@ export async function runSingleAttempt(
875
876
  forkSessionFile,
876
877
  params.fast ?? agentConfig.fast,
877
878
  );
879
+ const projectionImport = `--import=${new URL("./rpc-stdout-projection.mjs", import.meta.url).href}`;
880
+ if (!spawnEnv.NODE_OPTIONS?.split(/\s+/).includes(projectionImport)) {
881
+ spawnEnv.NODE_OPTIONS = [spawnEnv.NODE_OPTIONS, projectionImport].filter(Boolean).join(" ");
882
+ }
878
883
  const spawnSpec = getPiSpawnCommand(piArgs);
879
884
  const probedChildVersion = options.spawnChildProcess
880
885
  ? options.childPiVersion
@@ -1141,8 +1146,17 @@ export async function runSingleAttempt(
1141
1146
  // Parse JSON lines from stdout. Pi's agent_end event can repeat the full
1142
1147
  // message history even without structured output, so framing must allow the
1143
1148
  // bounded transcript plus JSON escaping and event metadata in every lane.
1144
- const stdoutLines = createUtf8LineDecoder(EXECUTION_BUFFER_LIMITS.transcriptBytes * 4);
1149
+ const failStdoutProtocol = (reason: string): void => {
1150
+ if (state.terminal) return;
1151
+ state.receivedFirstActivity = true;
1152
+ state.stdoutProtocolViolation = true;
1153
+ addReplayEvidence("stdout-protocol", reason);
1154
+ state.runtimeFailure = `Teammate stdout JSONL protocol violation (agent=${params.agent}, correlationId=${correlationId}): ${reason}`;
1155
+ appendBoundedTranscriptMessage(messages, { role: "system", content: state.runtimeFailure });
1156
+ completeTurn(undefined, true, 1, "failed");
1157
+ };
1145
1158
  const processStdoutLine = (line: string) => {
1159
+ if (state.terminal) return;
1146
1160
  const trimmed = line.trim();
1147
1161
  if (!trimmed) {
1148
1162
  state.receivedFirstActivity = true;
@@ -1150,24 +1164,34 @@ export async function runSingleAttempt(
1150
1164
  if (timers.firstActivity) clearTimeout(timers.firstActivity);
1151
1165
  return;
1152
1166
  }
1167
+ let event: JsonLineEvent;
1168
+ try {
1169
+ event = JSON.parse(trimmed) as JsonLineEvent;
1170
+ } catch (error) {
1171
+ failStdoutProtocol(`malformed JSON, receivedBytes=${Buffer.byteLength(line)}: ${error instanceof Error ? truncateUtf8Head(error.message, 256) : String(error)}`);
1172
+ return;
1173
+ }
1174
+ if (!event || typeof event !== "object" || typeof event.type !== "string") {
1175
+ failStdoutProtocol(`invalid event envelope, receivedBytes=${Buffer.byteLength(line)}`);
1176
+ return;
1177
+ }
1153
1178
  try {
1154
- const event = JSON.parse(trimmed) as JsonLineEvent;
1155
1179
  processEvent(event);
1156
- } catch {
1157
- state.receivedFirstActivity = true;
1158
- addReplayEvidence("stdout-protocol", "malformed-json-line");
1159
- state.stdoutProtocolViolation = true;
1160
- if (timers.firstActivity) clearTimeout(timers.firstActivity);
1161
- state.lastContent = appendUtf8Tail(
1162
- state.lastContent,
1163
- trimmed + "\n",
1164
- EXECUTION_BUFFER_LIMITS.streamBytes,
1165
- );
1180
+ } catch (error) {
1181
+ failStdoutProtocol(`event processing failed, event=${truncateUtf8Head(event.type, 64)}: ${error instanceof Error ? truncateUtf8Head(error.message, 256) : String(error)}`);
1166
1182
  }
1167
1183
  };
1184
+ const stdoutLines = createUtf8LineDecoder(
1185
+ EXECUTION_BUFFER_LIMITS.transcriptBytes * 4,
1186
+ ({ receivedBytes, maxBytes, prefix }) => {
1187
+ const eventType = /"type"\s*:\s*"([\w-]{1,64})"/.exec(prefix)?.[1] ?? "unknown";
1188
+ failStdoutProtocol(`oversized event=${eventType}, receivedBytes=${receivedBytes}, limitBytes=${maxBytes}`);
1189
+ },
1190
+ processStdoutLine,
1191
+ );
1168
1192
  child.stdout?.on("data", (chunk: Buffer) => {
1169
1193
  pokeLifecycleDeadline();
1170
- for (const line of stdoutLines.write(chunk)) processStdoutLine(line);
1194
+ stdoutLines.write(chunk);
1171
1195
  });
1172
1196
 
1173
1197
  const clearInterrupt = (): PendingInterrupt | undefined => {
@@ -1358,7 +1382,10 @@ export async function runSingleAttempt(
1358
1382
  exitCode = 0,
1359
1383
  terminalStatus: AgentTerminalStatus = exitCode === 0 ? "completed" : "failed",
1360
1384
  ): void {
1361
- if (state.terminal || state.turnLifecycleSettled) return;
1385
+ if (state.terminal) return;
1386
+ // A settled result stays published, but a later corrupt RPC line must
1387
+ // retire the still-live runtime and report a failed lifecycle.
1388
+ if (state.turnLifecycleSettled && !(state.stdoutProtocolViolation && terminalStatus === "failed")) return;
1362
1389
  if (state.stdoutProtocolViolation && exitCode === 0) {
1363
1390
  appendBoundedTranscriptMessage(messages, {
1364
1391
  role: "system",
@@ -0,0 +1,51 @@
1
+ // The SDK serializes whole RPC events, including repeated tool-result images.
2
+ // Project only telemetry; the model and persisted session retain the originals.
3
+ function projectContent(value) {
4
+ if (!value || !Array.isArray(value.content)) return value;
5
+ let changed = false;
6
+ const content = value.content.map((part) => {
7
+ if (part?.type !== "image" || typeof part.data !== "string") return part;
8
+ changed = true;
9
+ return { type: "text", text: `[image omitted from teammate telemetry: ${Buffer.byteLength(part.data)} encoded bytes]` };
10
+ });
11
+ return changed ? { ...value, content } : value;
12
+ }
13
+
14
+ export function projectRpcEvent(event) {
15
+ const projected = { ...event };
16
+ for (const key of ["message", "result", "partialResult"]) {
17
+ if (event[key]) projected[key] = projectContent(event[key]);
18
+ }
19
+ for (const key of ["messages", "toolResults"]) {
20
+ if (Array.isArray(event[key])) projected[key] = event[key].map(projectContent);
21
+ }
22
+ return projected;
23
+ }
24
+
25
+ export function projectRpcWrite(chunk) {
26
+ if (typeof chunk !== "string" || !chunk.endsWith("\n") || !chunk.includes('"image"')) return chunk;
27
+ // RPC writes one complete JSONL event per write. Preserve malformed writes
28
+ // verbatim so the parent remains the protocol-failure authority.
29
+ let event;
30
+ try {
31
+ event = JSON.parse(chunk);
32
+ } catch (error) {
33
+ if (error instanceof SyntaxError) return chunk;
34
+ throw error;
35
+ }
36
+ if (!event || typeof event !== "object" || typeof event.type !== "string") return chunk;
37
+ return `${JSON.stringify(projectRpcEvent(event))}\n`;
38
+ }
39
+
40
+ export function installRpcStdoutProjection(stream) {
41
+ const originalWrite = stream.write;
42
+ stream.write = function (chunk, encodingOrCallback, callback) {
43
+ return originalWrite.call(this, projectRpcWrite(chunk), encodingOrCallback, callback);
44
+ };
45
+ }
46
+
47
+ // A child inherits NODE_OPTIONS into tools too; never intercept their stdout.
48
+ if (process.env.PI_TEAMMATE_CHILD === "1"
49
+ && process.argv.some((arg, index, args) => arg === "--mode" && args[index + 1] === "rpc")) {
50
+ installRpcStdoutProjection(process.stdout);
51
+ }
@@ -98,7 +98,14 @@ export interface SessionHistoryOptions {
98
98
  signal?: AbortSignal;
99
99
  }
100
100
  export type SessionHistoryListOptions = SessionHistoryOptions;
101
- export type SessionHistorySearchOptions = SessionHistoryOptions;
101
+ export interface SessionHistorySearchOptions extends SessionHistoryOptions {
102
+ /** Literal matching is the compatibility default; terms require every unique normalized term in one projected entry. */
103
+ mode?: "literal" | "terms";
104
+ /** Relevance ranks exact text, continuous phrase, then the tightest term span; ties retain source order. */
105
+ sort?: "source" | "relevance";
106
+ /** Zero-based match offset within this fresh scan (not a snapshot cursor). */
107
+ offset?: number;
108
+ }
102
109
  export type SessionHistoryReadOptions = SessionHistoryOptions;
103
110
  export type SessionHistoryCompactionOptions = Omit<SessionHistoryOptions, "include">;
104
111
 
@@ -161,7 +168,10 @@ export interface SessionHistorySearchResult extends ScanMetrics {
161
168
  generation?: number;
162
169
  query: string;
163
170
  matches: readonly SessionHistorySearchMatch[];
171
+ /** Total matches in this scan, independent of pagination. */
164
172
  matchCount: number;
173
+ hasMore: boolean;
174
+ nextOffset?: number;
165
175
  }
166
176
  export interface SessionHistoryCompactionResult extends ScanMetrics {
167
177
  version: typeof SESSION_HISTORY_VERSION;
@@ -251,6 +261,7 @@ export class SessionHistoryService {
251
261
  }
252
262
 
253
263
  async list(options: SessionHistoryListOptions = {}): Promise<SessionHistoryListResult> {
264
+ rejectSearchOptions(options);
254
265
  const include = normalizedInclude(options.include);
255
266
  const limit = boundedLimit(options.limit);
256
267
  const scan = await this.#scan(include, options.signal);
@@ -268,36 +279,57 @@ export class SessionHistoryService {
268
279
 
269
280
  async search(query: string, options: SessionHistorySearchOptions = {}): Promise<SessionHistorySearchResult> {
270
281
  validateQuery(query);
282
+ const mode = options.mode ?? "literal";
283
+ const sort = options.sort ?? "source";
284
+ if (mode !== "literal" && mode !== "terms") throw new TypeError("mode must be literal or terms.");
285
+ if (sort !== "source" && sort !== "relevance") throw new TypeError("sort must be source or relevance.");
286
+ const offset = options.offset ?? 0;
287
+ if (!Number.isSafeInteger(offset) || offset < 0) throw new TypeError("offset must be a non-negative safe integer.");
288
+ const folded = mode === "terms" ? normalizedSearchText(query) : query.toLocaleLowerCase("en-US");
289
+ if (mode === "terms" && folded.length === 0) throw new TypeError("terms query must contain non-whitespace text.");
290
+ const terms = mode === "terms" ? [...new Set(folded.split(" "))] : [folded];
271
291
  const include = normalizedInclude(options.include);
272
292
  const limit = boundedLimit(options.limit);
273
293
  const scan = await this.#scan(include, options.signal);
274
- const folded = query.toLocaleLowerCase("en-US");
275
- const matches: SessionHistorySearchMatch[] = [];
276
- let matchCount = 0;
294
+ const ranked: Array<{ entry: SessionHistoryEntry; index: number; rank: number; span: number; sourceOrder: number }> = [];
277
295
  for (const session of scan.sessions) {
278
296
  for (const entry of session.entries) {
279
- const index = entry.text.toLocaleLowerCase("en-US").indexOf(folded);
280
- if (index < 0) continue;
281
- matchCount += 1;
282
- if (matches.length >= limit) continue;
283
- matches.push({
284
- sessionId: entry.sessionId,
285
- entryId: entry.entryId,
286
- turn: entry.turn,
287
- resourceUri: entry.resourceUri,
288
- kind: entry.kind,
289
- timestamp: entry.timestamp,
290
- snippet: boundedSnippet(entry.text, index),
297
+ const text = mode === "terms" ? normalizedSearchText(entry.text) : entry.text.toLocaleLowerCase("en-US");
298
+ const indices = terms.map((term) => text.indexOf(term));
299
+ if (indices.some((index) => index < 0)) continue;
300
+ const phraseIndex = text.indexOf(folded);
301
+ ranked.push({
302
+ entry,
303
+ index: phraseIndex >= 0 ? phraseIndex : Math.min(...indices),
304
+ rank: text === folded ? 0 : phraseIndex >= 0 ? 1 : 2,
305
+ span: sort === "relevance" ? tightestTermSpan(text, terms, indices) : 0,
306
+ sourceOrder: ranked.length,
291
307
  });
292
308
  }
293
309
  }
310
+ if (sort === "relevance") ranked.sort((left, right) =>
311
+ left.rank - right.rank || left.span - right.span || left.sourceOrder - right.sourceOrder);
312
+ const page = ranked.slice(offset, offset + limit);
313
+ const matches: SessionHistorySearchMatch[] = page.map(({ entry, index }) => ({
314
+ sessionId: entry.sessionId,
315
+ entryId: entry.entryId,
316
+ turn: entry.turn,
317
+ resourceUri: entry.resourceUri,
318
+ kind: entry.kind,
319
+ timestamp: entry.timestamp,
320
+ snippet: boundedSnippet(entry.text, mode === "terms" && entry.text.length > MAX_SESSION_HISTORY_SNIPPET_CHARS
321
+ ? originalSearchIndex(entry.text, index) : index),
322
+ }));
323
+ const remaining = Math.max(0, ranked.length - offset - matches.length);
294
324
  return resultWithMetrics(scan, {
295
325
  version: SESSION_HISTORY_VERSION,
296
326
  ...(scan.generation === undefined ? {} : { generation: scan.generation }),
297
327
  query,
298
328
  matches: Object.freeze(matches),
299
- matchCount,
300
- }, matchCount - matches.length);
329
+ matchCount: ranked.length,
330
+ hasMore: remaining > 0,
331
+ ...(remaining > 0 ? { nextOffset: offset + matches.length } : {}),
332
+ }, remaining);
301
333
  }
302
334
 
303
335
  async searchSessions(query: string, options: SessionHistorySearchOptions = {}): Promise<SessionHistorySearchResult> {
@@ -305,6 +337,7 @@ export class SessionHistoryService {
305
337
  }
306
338
 
307
339
  async compactions(options: SessionHistoryCompactionOptions = {}): Promise<SessionHistoryCompactionResult> {
340
+ rejectSearchOptions(options);
308
341
  const limit = boundedLimit(options.limit);
309
342
  const scan = await this.#scan(new Set<SessionHistoryInclude>(["compaction"]), options.signal);
310
343
  const all = scan.sessions
@@ -331,6 +364,7 @@ export class SessionHistoryService {
331
364
  options: SessionHistoryReadOptions = {},
332
365
  ): Promise<SessionHistoryReadResult> {
333
366
  const request = typeof sessionOrRequest === "string" ? { sessionId: sessionOrRequest, ...options } : sessionOrRequest;
367
+ rejectSearchOptions(request);
334
368
  validateIdentifierInput(request.sessionId, "sessionId");
335
369
  if (request.entryId !== undefined) validateIdentifierInput(request.entryId, "entryId");
336
370
  if (request.turn !== undefined && (!Number.isSafeInteger(request.turn) || request.turn < 0)) {
@@ -775,6 +809,50 @@ function boundedLimit(value: number | undefined): number {
775
809
  }
776
810
  return value;
777
811
  }
812
+ function rejectSearchOptions(options: SessionHistoryOptions): void {
813
+ for (const key of ["mode", "sort", "offset"] as const) {
814
+ if (key in options) throw new TypeError(`${key} is available only for search.`);
815
+ }
816
+ }
817
+ function normalizedSearchText(text: string): string {
818
+ return text.normalize("NFKC").toLocaleLowerCase("en-US").replace(/\s+/gu, " ").trim();
819
+ }
820
+
821
+ /** Map a non-whitespace match back via whole-prefix normalization, only for
822
+ * returned long snippets. Per-character/grapheme normalization is insufficient:
823
+ * compatibility Hangul jamo can compose across original grapheme boundaries.
824
+ * Prefix lengths account for composition, expansion and collapsed whitespace. */
825
+ function originalSearchIndex(text: string, target: number): number {
826
+ let low = 0;
827
+ let high = text.length;
828
+ while (low < high) {
829
+ const middle = Math.floor((low + high) / 2);
830
+ if (normalizedSearchText(text.slice(0, middle)).length > target) high = middle;
831
+ else low = middle + 1;
832
+ }
833
+ let index = Math.max(0, low - 1);
834
+ // A normalized character may expand to several UTF-16 units; anchor an
835
+ // astral source character at its high surrogate, not in its middle.
836
+ if (index > 0 && /[\uDC00-\uDFFF]/.test(text[index]!)) index -= 1;
837
+ return index;
838
+ }
839
+
840
+ /** Smallest covering window over all unique substring terms; no tokenization
841
+ * beyond query whitespace and no matching across projected entries. */
842
+ function tightestTermSpan(text: string, terms: readonly string[], initial: readonly number[]): number {
843
+ if (terms.length === 1) return terms[0]!.length;
844
+ const indices = [...initial];
845
+ let best = Infinity;
846
+ while (true) {
847
+ const start = Math.min(...indices);
848
+ const end = Math.max(...indices.map((index, term) => index + terms[term]!.length));
849
+ best = Math.min(best, end - start);
850
+ const earliest = indices.indexOf(start);
851
+ const next = text.indexOf(terms[earliest]!, start + 1);
852
+ if (next < 0) return best;
853
+ indices[earliest] = next;
854
+ }
855
+ }
778
856
  function validateQuery(query: string): void {
779
857
  if (typeof query !== "string" || query.length === 0 || query.length > MAX_SESSION_HISTORY_QUERY_CHARS || query.includes("\0")) {
780
858
  throw new TypeError(`query must be a non-empty string of at most ${MAX_SESSION_HISTORY_QUERY_CHARS} characters.`);
@@ -522,7 +522,11 @@ export declare function appendBoundedTranscriptMessage(messages: Array<{
522
522
  role: string;
523
523
  content: string;
524
524
  }): void;
525
- export declare function createUtf8LineDecoder(maxBufferedBytes?: number): Utf8LineDecoder;
525
+ export declare function createUtf8LineDecoder(maxBufferedBytes?: number, onOverflow?: (info: {
526
+ receivedBytes: number;
527
+ maxBytes: number;
528
+ prefix: string;
529
+ }) => void, onLine?: (line: string) => void): Utf8LineDecoder;
526
530
  export declare function appendDistinctAssistantMessage(messages: Array<{
527
531
  role: string;
528
532
  content: string;
@@ -59,7 +59,14 @@ export interface SessionHistoryOptions {
59
59
  signal?: AbortSignal;
60
60
  }
61
61
  export type SessionHistoryListOptions = SessionHistoryOptions;
62
- export type SessionHistorySearchOptions = SessionHistoryOptions;
62
+ export interface SessionHistorySearchOptions extends SessionHistoryOptions {
63
+ /** Literal matching is the compatibility default; terms require every unique normalized term in one projected entry. */
64
+ mode?: "literal" | "terms";
65
+ /** Relevance ranks exact text, continuous phrase, then the tightest term span; ties retain source order. */
66
+ sort?: "source" | "relevance";
67
+ /** Zero-based match offset within this fresh scan (not a snapshot cursor). */
68
+ offset?: number;
69
+ }
63
70
  export type SessionHistoryReadOptions = SessionHistoryOptions;
64
71
  export type SessionHistoryCompactionOptions = Omit<SessionHistoryOptions, "include">;
65
72
  export interface SessionHistorySession {
@@ -117,7 +124,10 @@ export interface SessionHistorySearchResult extends ScanMetrics {
117
124
  generation?: number;
118
125
  query: string;
119
126
  matches: readonly SessionHistorySearchMatch[];
127
+ /** Total matches in this scan, independent of pagination. */
120
128
  matchCount: number;
129
+ hasMore: boolean;
130
+ nextOffset?: number;
121
131
  }
122
132
  export interface SessionHistoryCompactionResult extends ScanMetrics {
123
133
  version: typeof SESSION_HISTORY_VERSION;