@omercnet/paseo-omp 0.4.0-next.129.1 → 0.4.0-next.133.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.
package/README.md CHANGED
@@ -20,7 +20,7 @@ before capture.
20
20
 
21
21
  ## Quick start
22
22
 
23
- Requirements: Paseo `>=0.9.0-beta.1 <0.10.0`, OMP `18.1.15` or newer, and OMP RPC protocol v2. Paseo 0.9 is required for the Help report's supported copy and external-link APIs, nested provider-subagent ancestry, spawning-tool links, npm-managed installation, and the complete client integration.
23
+ Requirements: Paseo `>=0.9.0 <0.10.0`, OMP `18.1.15` or newer, and OMP RPC protocol v2. Paseo 0.9 is required for the Help report's supported copy and external-link APIs, nested provider-subagent ancestry, spawning-tool links, npm-managed installation, and the complete client integration.
24
24
 
25
25
  ```bash
26
26
  paseo plugin install npm:@omercnet/paseo-omp@<version>
@@ -36,7 +36,7 @@ Open the **OMP** sidebar to review the provider-profile contract, choose which c
36
36
  - [Core-provider issue and parity audit](docs/core-provider-issue-audit.md)
37
37
  - [Alpha release checklist](docs/alpha-release-checklist.md)
38
38
 
39
- The plugin uses public Paseo 0.9 beta provider and client contracts and registers distinct `omp-plugin` and `omp-plugin-<profile>` identities; it does not modify the bundled `omp` provider.
39
+ The plugin uses public Paseo 0.9 provider and client contracts and registers distinct `omp-plugin` and `omp-plugin-<profile>` identities; it does not modify the bundled `omp` provider.
40
40
 
41
41
  ## Named OMP profiles
42
42
 
@@ -50,7 +50,7 @@ Profile selection is request-local: concurrent clients cannot change each other'
50
50
 
51
51
  ## Paseo provider SDK coverage
52
52
 
53
- This matrix inventories the complete capability set exported by the pinned `@getpaseo/plugin` 0.9.0-beta.1 provider SDK. It measures strict SDK surface coverage, not general product quality.
53
+ This matrix inventories the complete capability set exported by the pinned `@getpaseo/plugin` 0.9.0 provider SDK. It measures strict SDK surface coverage, not general product quality.
54
54
 
55
55
  **Current capability completeness: 61.8%.** The provider advertises 11 of 17 capabilities (64.7%); ten are complete and `session.configure` is partial.
56
56
 
@@ -113,7 +113,7 @@ Existing agents whose provider is `omp` remain owned by the bundled provider. Ne
113
113
 
114
114
  OMP `18.1.15` is the oldest version tested end to end. The direct provider's hard compatibility gate is `rpc-ui` protocol v2: metadata-free legacy ready frames and v1-only runtimes are rejected before a provider session opens because they cannot support the advertised persistence and conversation-rewind capabilities. Typed tool approvals remain capability-gated and fall back as described above. For terminal completion, a matching `agent_end.requestId` is authoritative and mismatches are ignored before state changes. Released OMP 18.2.x builds that omit that field use an ordered fallback requiring a fresh correlated native user entry followed by current-turn assistant activity, an idle non-compacting runtime, and no conflicting permission, tool, steer, or child-session work. Ambiguous events while OMP is active are ignored; unresolved ambiguity after confirmed idle fails only the Paseo turn. This trades a bounded residual same-agent stale-event risk for compatibility with released OMP instead of terminating and lazily restarting the runtime after every later prompt. Paseo clients, including mobile clients inside reconnect grace, do not own or terminate the daemon-managed OMP session.
115
115
 
116
- Paseo 0.9.0-beta.1 is the verified canary target and minimum supported release. It preserves nested provider-subagent ancestry and spawning-tool links, and provides npm-managed plugin sources, platform-owned external URL opening, and whole-item timeline transforms before Overview grouping. The required full Docker canary passes there with OMP 18.1.15 and 18.2.0.
116
+ Paseo 0.9.0 is the minimum supported release. The verified controlled canary evidence remains on the 0.9.0-beta.1 image, where the full Docker canary passed with OMP 18.1.15 and 18.2.0. That canary preserved nested provider-subagent ancestry and spawning-tool links, and validated npm-managed plugin sources, platform-owned external URL opening, and whole-item timeline transforms before Overview grouping.
117
117
 
118
118
  Install, update, disable, or remove `paseo-omp` independently of the bundled provider. Verify the provider snapshot contains `omp-plugin` after installation; a bundled `omp` entry may remain present and is not modified by this plugin.
119
119
 
@@ -99,7 +99,6 @@ function OrderedRoleEditor({
99
99
  return (
100
100
  <View style={styles.recordList}>
101
101
  {roles.map((role, index) => (
102
- // biome-ignore lint/suspicious/noArrayIndexKey: ordered rows are edited by position and must keep input identity while their value changes.
103
102
  <View key={index} style={styles.recordRow}>
104
103
  <Text style={styles.recordKey}>{index + 1}</Text>
105
104
  <TextInput
@@ -172,7 +171,6 @@ function RecordRoutingEditor({
172
171
  ? rawValue
173
172
  : "";
174
173
  return (
175
- // biome-ignore lint/suspicious/noArrayIndexKey: record keys are editable, so row position preserves input identity.
176
174
  <View key={index} style={styles.recordRow}>
177
175
  <TextInput
178
176
  accessibilityLabel={`${formatOmpSettingLabel(setting.path)} ${keyLabel} ${index + 1}`}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omercnet/paseo-omp",
3
- "version": "0.4.0-next.129.1",
3
+ "version": "0.4.0-next.133.1",
4
4
  "type": "module",
5
5
  "description": "Paseo integration for OMP, including its direct provider and workspace tooling.",
6
6
  "license": "MIT",
@@ -16,7 +16,7 @@
16
16
  "publishConfig": {
17
17
  "access": "public"
18
18
  },
19
- "packageManager": "npm@10.9.4",
19
+ "packageManager": "bun@1.4.2",
20
20
  "engines": {
21
21
  "node": ">=22.18.0"
22
22
  },
@@ -40,42 +40,41 @@
40
40
  "paseo-plugin.json"
41
41
  ],
42
42
  "scripts": {
43
- "check": "biome check --error-on-warnings .",
43
+ "check": "biome check .",
44
44
  "check:write": "biome check --write .",
45
- "test": "run-s test:ci:*",
45
+ "test": "vitest run",
46
46
  "test:coverage": "PASEO_OMP_COVERAGE=1 vitest run --coverage",
47
47
  "test:integration:docker": "node --import tsx scripts/test-docker-host-tools.ts",
48
48
  "test:integration:wsl": "node --import tsx scripts/test-wsl-host-tools.ts",
49
49
  "test:integration:canary": "node --import tsx scripts/test-canary.ts",
50
50
  "test:integration:install": "node --import tsx scripts/test-package-install.ts",
51
- "typecheck": "tsc --noEmit",
52
- "test:ci:unit": "vitest run",
53
- "build": "node ../.github/scripts/build-plugin.mjs"
51
+ "typecheck": "tsc --noEmit"
54
52
  },
55
53
  "dependencies": {
56
- "@getpaseo/protocol": "0.9.0-beta.1",
57
- "@modelcontextprotocol/sdk": "1.29.0",
54
+ "@getpaseo/protocol": "0.9.0",
55
+ "@modelcontextprotocol/sdk": "1.30.0",
58
56
  "yaml": "^2.9.0"
59
57
  },
60
58
  "devDependencies": {
61
- "@biomejs/biome": "^2.5.14",
62
- "@getpaseo/cli": "0.9.0-beta.1",
63
- "@getpaseo/client": "0.9.0-beta.1",
64
- "@getpaseo/plugin": "0.9.0-beta.1",
65
- "@getpaseo/server": "0.9.0-beta.1",
59
+ "@biomejs/biome": "^2.5.10",
60
+ "@getpaseo/cli": "0.9.0",
61
+ "@getpaseo/client": "0.9.0",
62
+ "@getpaseo/plugin": "0.9.0",
63
+ "@getpaseo/server": "0.9.0",
66
64
  "@oh-my-pi/pi-agent-core": "18.2.0",
67
65
  "@oh-my-pi/pi-coding-agent": "18.2.0",
68
66
  "@tanstack/react-query": "^5.102.3",
69
67
  "@types/node": "^24.10.1",
70
68
  "@types/react": "^19.2.18",
71
69
  "@vitest/coverage-v8": "^5.0.0",
72
- "npm-run-all2": "^9.0.3",
70
+ "esbuild": "0.28.2",
71
+ "pino": "10.3.1",
73
72
  "react": "19.1.0",
74
73
  "react-native": "0.81.5",
75
74
  "tsx": "^4.20.6",
76
75
  "typescript": "^7.0.2",
77
76
  "vitest": "^5.0.0",
78
- "zod": "^4.4.3"
77
+ "zod": "4.4.3"
79
78
  },
80
79
  "overrides": {
81
80
  "qs": "6.16.0"
package/paseo-plugin.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "id": "paseo-omp",
3
- "requirements": { "paseo": ">=0.9.0-beta.1 <0.10.0" },
3
+ "requirements": { "paseo": ">=0.9.0 <0.10.0" },
4
4
  "build": [["node", "scripts/prepare-dependencies.mjs"]]
5
5
  }
@@ -1,21 +1,54 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { existsSync } from "node:fs";
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
5
 
6
6
  const projectRoot = dirname(dirname(fileURLToPath(import.meta.url)));
7
7
 
8
- export function prepareDependencies(root = projectRoot, execute = execFileSync) {
9
- if (!existsSync(join(root, "package-lock.json"))) return false;
8
+ function loadRootCatalog(root) {
9
+ for (let directory = root; ; directory = dirname(directory)) {
10
+ const manifestPath = join(directory, "package.json");
11
+ if (existsSync(manifestPath)) {
12
+ const catalog = JSON.parse(readFileSync(manifestPath, "utf8")).catalog;
13
+ if (catalog) return catalog;
14
+ }
15
+ if (directory === dirname(directory)) return null;
16
+ }
17
+ }
18
+
19
+ function stageManifest(root, catalog, install) {
20
+ const manifestPath = join(root, "package.json");
21
+ const source = readFileSync(manifestPath, "utf8");
22
+ const manifest = JSON.parse(source);
23
+
24
+ for (const [name, spec] of Object.entries(manifest.dependencies ?? {})) {
25
+ if (spec !== "catalog:") continue;
26
+ if (typeof catalog[name] !== "string") throw new Error(`Missing catalog entry for ${name}`);
27
+ manifest.dependencies[name] = catalog[name];
28
+ }
29
+ delete manifest.devDependencies;
10
30
 
11
- execute(
12
- process.platform === "win32" ? "npm.cmd" : "npm",
13
- ["ci", "--omit=dev", "--ignore-scripts"],
14
- {
15
- cwd: root,
16
- stdio: "inherit",
17
- },
18
- );
31
+ writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
32
+ try {
33
+ install();
34
+ } finally {
35
+ writeFileSync(manifestPath, source);
36
+ }
37
+ }
38
+
39
+ export function prepareDependencies(root = projectRoot, execute = execFileSync) {
40
+ const catalog = loadRootCatalog(root);
41
+ if (!catalog) return false;
42
+ stageManifest(root, catalog, () => {
43
+ execute(
44
+ process.platform === "win32" ? "npm.cmd" : "npm",
45
+ ["install", "--omit=dev", "--ignore-scripts", "--no-package-lock", "--workspaces=false"],
46
+ {
47
+ cwd: root,
48
+ stdio: "inherit",
49
+ },
50
+ );
51
+ });
19
52
  return true;
20
53
  }
21
54
 
@@ -1,2 +1,2 @@
1
1
  /** Release Please keeps this bundle-safe constant aligned with package.json. */
2
- export const PASEO_OMP_PACKAGE_VERSION = "0.4.0-next.129.1"; // x-release-please-version
2
+ export const PASEO_OMP_PACKAGE_VERSION = "0.4.0-next.133.1"; // x-release-please-version
@@ -159,6 +159,7 @@ export class OmpRpcProcess {
159
159
  private readonly terminateProcessTree: (pid: number) => Promise<ProcessTreeCleanup>;
160
160
  private readonly streamedBlocks = new Map<number, string>();
161
161
  private readonly activeToolCallIds = new Set<string>();
162
+ private readonly announcedToolCallIds = new Set<string>();
162
163
  private pendingWriteBytes = 0;
163
164
  private commandTextLength = 0;
164
165
  private lineParts: Buffer[] = [];
@@ -1112,6 +1113,7 @@ export class OmpRpcProcess {
1112
1113
  if (event.data.type === "turn_end") {
1113
1114
  this.commandTextLength = 0;
1114
1115
  this.activeToolCallIds.clear();
1116
+ this.announcedToolCallIds.clear();
1115
1117
  }
1116
1118
  }
1117
1119
 
@@ -1133,6 +1135,7 @@ export class OmpRpcProcess {
1133
1135
  this.streamedBlocks.clear();
1134
1136
  this.commandTextLength = 0;
1135
1137
  this.activeToolCallIds.clear();
1138
+ this.announcedToolCallIds.clear();
1136
1139
  return true;
1137
1140
  }
1138
1141
  if (event.type === "command_output") {
@@ -1151,12 +1154,19 @@ export class OmpRpcProcess {
1151
1154
  this.activeToolCallIds.add(event.toolCallId);
1152
1155
  return true;
1153
1156
  }
1154
- if (event.type === "tool_execution_update" || event.type === "tool_stream_update") {
1157
+ if (event.type === "tool_stream_update") {
1158
+ return (
1159
+ this.activeToolCallIds.has(event.toolCallId) ||
1160
+ this.announcedToolCallIds.has(event.toolCallId)
1161
+ );
1162
+ }
1163
+ if (event.type === "tool_execution_update") {
1155
1164
  return this.activeToolCallIds.has(event.toolCallId);
1156
1165
  }
1157
1166
  if (event.type === "tool_execution_end") {
1158
1167
  if (!this.activeToolCallIds.has(event.toolCallId)) return false;
1159
1168
  this.activeToolCallIds.delete(event.toolCallId);
1169
+ this.announcedToolCallIds.delete(event.toolCallId);
1160
1170
  return true;
1161
1171
  }
1162
1172
  if (
@@ -1174,6 +1184,14 @@ export class OmpRpcProcess {
1174
1184
  nextBlocks.set(0, content);
1175
1185
  } else if (Array.isArray(content)) {
1176
1186
  for (const [index, part] of content.entries()) {
1187
+ if (
1188
+ part.type === "toolCall" &&
1189
+ typeof part.id === "string" &&
1190
+ (this.announcedToolCallIds.has(part.id) ||
1191
+ this.announcedToolCallIds.size < MAX_ACTIVE_TOOLS)
1192
+ ) {
1193
+ this.announcedToolCallIds.add(part.id);
1194
+ }
1177
1195
  const text =
1178
1196
  part.type === "text" ? part.text : part.type === "thinking" ? part.thinking : undefined;
1179
1197
  if (text !== undefined) nextBlocks.set(index, text);
@@ -63,6 +63,7 @@ import {
63
63
  OmpThinkingLevelSchema,
64
64
  validateBoundedText,
65
65
  } from "./omp-rpc-values";
66
+ import { buildOmpPromptRequest } from "./prompt-payload";
66
67
  import { OmpCleanupFailure, OmpPublicError } from "./security";
67
68
  import {
68
69
  listOmpSessionDescriptors,
@@ -497,7 +498,7 @@ class OmpRpcSession implements OmpRuntimeSession {
497
498
  const safeMessage = validateBoundedText(message, "prompt", MAX_TEXT_LENGTH);
498
499
  let acknowledgement: z.infer<typeof OmpPromptAckSchema> | undefined;
499
500
  const request = this.process.startRequest(
500
- { type: "prompt", message: safeMessage, ...(images.length > 0 ? { images } : {}) },
501
+ buildOmpPromptRequest(safeMessage, images),
501
502
  undefined,
502
503
  (value) => {
503
504
  acknowledgement = OmpPromptAckSchema.parse(value) ?? {};
@@ -511,7 +512,7 @@ class OmpRpcSession implements OmpRuntimeSession {
511
512
 
512
513
  async steer(message: string, images: readonly OmpImage[] = []): Promise<void> {
513
514
  const safeMessage = validateBoundedText(message, "steer", MAX_TEXT_LENGTH);
514
- await this.process.sendFrame({
515
+ await this.process.request({
515
516
  type: "steer",
516
517
  message: safeMessage,
517
518
  ...(images.length > 0 ? { images } : {}),
@@ -519,7 +520,7 @@ class OmpRpcSession implements OmpRuntimeSession {
519
520
  }
520
521
  async followUp(message: string, images: readonly OmpImage[] = []): Promise<void> {
521
522
  const safeMessage = validateBoundedText(message, "follow-up", MAX_TEXT_LENGTH);
522
- await this.process.sendFrame({
523
+ await this.process.request({
523
524
  type: "follow_up",
524
525
  message: safeMessage,
525
526
  ...(images.length > 0 ? { images } : {}),
@@ -14,6 +14,14 @@ export function isSafeCommandName(name: string): boolean {
14
14
  }
15
15
 
16
16
  export type OmpPromptPayload = { text: string; images: OmpImage[]; commandName?: string };
17
+ export function buildOmpPromptRequest(message: string, images: readonly OmpImage[] = []) {
18
+ return {
19
+ type: "prompt" as const,
20
+ message,
21
+ streamingBehavior: "followUp" as const,
22
+ ...(images.length > 0 ? { images } : {}),
23
+ };
24
+ }
17
25
 
18
26
  const REVIEW_LINE_MARKERS = { add: "+", remove: "-", context: " " } as const;
19
27
 
@@ -222,12 +230,10 @@ export function inlinePromptFrameBytes(
222
230
  delivery: "prompt" | "steer",
223
231
  ): number {
224
232
  const images = payload.images.map(({ type, mimeType }) => ({ type, data: "", mimeType }));
225
- const frame = {
226
- type: delivery,
227
- message: payload.text,
228
- ...(images.length > 0 ? { images } : {}),
229
- ...(delivery === "prompt" ? { id: "" } : {}),
230
- };
233
+ const frame =
234
+ delivery === "prompt"
235
+ ? { ...buildOmpPromptRequest(payload.text, images), id: "" }
236
+ : { type: "steer", message: payload.text, ...(images.length > 0 ? { images } : {}) };
231
237
  const requestIdBytes = delivery === "prompt" ? RPC_REQUEST_ID_BYTES : 0;
232
238
  return (
233
239
  utf8Bytes(JSON.stringify(frame)) +
@@ -80,6 +80,10 @@ type ToolSnapshot = {
80
80
  specializedRendered: boolean;
81
81
  silent: boolean;
82
82
  };
83
+ type PendingToolStream = {
84
+ output: JsonValue;
85
+ retainedBytes: number;
86
+ };
83
87
 
84
88
  export interface OmpTimelineScheduler {
85
89
  set(callback: () => void | Promise<void>, delayMs: number): unknown;
@@ -354,6 +358,8 @@ type CompactionSlot = {
354
358
 
355
359
  export class OmpTimelineProjector {
356
360
  private readonly tools = new Map<string, ToolSnapshot>();
361
+ private readonly pendingToolStreams = new Map<string, PendingToolStream>();
362
+ private pendingToolStreamBytes = 0;
357
363
  private stream: StreamSnapshot | null = null;
358
364
  private flushTimer: unknown;
359
365
  private currentTurnId: string | null = null;
@@ -493,6 +499,11 @@ export class OmpTimelineProjector {
493
499
  return;
494
500
  case "tool_execution_start": {
495
501
  this.flush(true);
502
+ const pendingStream = this.pendingToolStreams.get(event.toolCallId);
503
+ if (pendingStream) {
504
+ this.pendingToolStreams.delete(event.toolCallId);
505
+ this.pendingToolStreamBytes -= pendingStream.retainedBytes;
506
+ }
496
507
  if (this.toolIdentitySaturated || this.retiredToolCallIds.has(event.toolCallId)) return;
497
508
  const previous = this.tools.get(event.toolCallId);
498
509
  if (!previous && this.tools.size >= MAX_ACTIVE_TOOLS) return;
@@ -501,7 +512,17 @@ export class OmpTimelineProjector {
501
512
  MAX_PUBLIC_TOOL_PAYLOAD_BYTES,
502
513
  MAX_PUBLIC_TOOL_PAYLOAD_BYTES,
503
514
  );
504
- const retainedBytes = boundedJsonBytes(input, MAX_PUBLIC_TOOL_PAYLOAD_BYTES);
515
+ const inputBytes = boundedJsonBytes(input, MAX_PUBLIC_TOOL_PAYLOAD_BYTES);
516
+ let output = pendingStream?.output ?? null;
517
+ let retainedBytes = inputBytes + (pendingStream?.retainedBytes ?? 0);
518
+ if (
519
+ pendingStream &&
520
+ this.activeToolBytes - (previous?.retainedBytes ?? 0) + retainedBytes >
521
+ MAX_ACTIVE_TOOL_BYTES
522
+ ) {
523
+ output = null;
524
+ retainedBytes = inputBytes;
525
+ }
505
526
  if (
506
527
  this.activeToolBytes - (previous?.retainedBytes ?? 0) + retainedBytes >
507
528
  MAX_ACTIVE_TOOL_BYTES
@@ -520,7 +541,7 @@ export class OmpTimelineProjector {
520
541
  nativeName: event.toolName,
521
542
  name: this.dataFilter.text(displayName, 256),
522
543
  input,
523
- output: null,
544
+ output,
524
545
  retainedBytes,
525
546
  turnId,
526
547
  generation: this.runtimeGeneration,
@@ -534,15 +555,28 @@ export class OmpTimelineProjector {
534
555
  }
535
556
  case "tool_execution_update":
536
557
  case "tool_stream_update": {
537
- const previous = this.tools.get(event.toolCallId);
538
- if (!previous) return;
539
- if (previous.turnId !== turnId || previous.generation !== this.runtimeGeneration) return;
540
558
  const output = this.dataFilter.json(
541
559
  event.type === "tool_execution_update" ? event.partialResult : event.update,
542
560
  MAX_PUBLIC_TOOL_PAYLOAD_BYTES,
543
561
  MAX_PUBLIC_TOOL_PAYLOAD_BYTES,
544
562
  );
545
563
  const outputBytes = boundedJsonBytes(output, MAX_PUBLIC_TOOL_PAYLOAD_BYTES);
564
+ const previous = this.tools.get(event.toolCallId);
565
+ if (!previous) {
566
+ if (event.type !== "tool_stream_update") return;
567
+ const pending = this.pendingToolStreams.get(event.toolCallId);
568
+ if (!pending && this.pendingToolStreams.size >= MAX_ACTIVE_TOOLS) return;
569
+ if (
570
+ this.pendingToolStreamBytes - (pending?.retainedBytes ?? 0) + outputBytes >
571
+ MAX_ACTIVE_TOOL_BYTES
572
+ ) {
573
+ return;
574
+ }
575
+ this.pendingToolStreamBytes += outputBytes - (pending?.retainedBytes ?? 0);
576
+ this.pendingToolStreams.set(event.toolCallId, { output, retainedBytes: outputBytes });
577
+ return;
578
+ }
579
+ if (previous.turnId !== turnId || previous.generation !== this.runtimeGeneration) return;
546
580
  const inputBytes = boundedJsonBytes(previous.input, MAX_PUBLIC_TOOL_PAYLOAD_BYTES);
547
581
  const retainedBytes = inputBytes + outputBytes;
548
582
  if (this.activeToolBytes - previous.retainedBytes + retainedBytes > MAX_ACTIVE_TOOL_BYTES)
@@ -869,6 +903,7 @@ export class OmpTimelineProjector {
869
903
  this.activeToolBytes = 0;
870
904
  this.replayToolCalls.clear();
871
905
  this.tools.clear();
906
+ this.clearPendingToolStreams();
872
907
  this.commandText = "";
873
908
  this.commandPublishedText = "";
874
909
  this.revertEntryByToken.clear();
@@ -1136,6 +1171,7 @@ export class OmpTimelineProjector {
1136
1171
  this.clearFlushTimer();
1137
1172
  this.stream = null;
1138
1173
  this.tools.clear();
1174
+ this.clearPendingToolStreams();
1139
1175
  this.replayToolCalls.clear();
1140
1176
  this.activeToolBytes = 0;
1141
1177
  this.replayCandidates.clear();
@@ -1176,6 +1212,11 @@ export class OmpTimelineProjector {
1176
1212
  }
1177
1213
  this.activeToolBytes = 0;
1178
1214
  this.tools.clear();
1215
+ this.clearPendingToolStreams();
1216
+ }
1217
+ private clearPendingToolStreams(): void {
1218
+ this.pendingToolStreams.clear();
1219
+ this.pendingToolStreamBytes = 0;
1179
1220
  }
1180
1221
  private ensureTurn(turnId: string): void {
1181
1222
  if (this.currentTurnId === turnId) return;