taskplane 0.28.3 → 0.28.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.
Files changed (71) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +215 -215
  3. package/bin/gitignore-patterns.mjs +79 -79
  4. package/bin/rpc-wrapper.mjs +1086 -1086
  5. package/bin/taskplane.mjs +3254 -3254
  6. package/dashboard/public/app.js +2573 -2573
  7. package/dashboard/public/index.html +139 -139
  8. package/dashboard/public/style.css +1882 -1882
  9. package/dashboard/public/taskplane-word-color.svg +18 -18
  10. package/dashboard/public/taskplane-word-white.svg +18 -18
  11. package/dashboard/server.cjs +1666 -1666
  12. package/extensions/reviewer-extension.ts +119 -119
  13. package/extensions/task-orchestrator.ts +28 -28
  14. package/extensions/taskplane/abort.ts +502 -502
  15. package/extensions/taskplane/agent-bridge-extension.ts +765 -765
  16. package/extensions/taskplane/agent-host.ts +833 -745
  17. package/extensions/taskplane/cleanup.ts +747 -747
  18. package/extensions/taskplane/config-loader.ts +1328 -1322
  19. package/extensions/taskplane/config-schema.ts +692 -682
  20. package/extensions/taskplane/config.ts +73 -73
  21. package/extensions/taskplane/context-window.ts +66 -66
  22. package/extensions/taskplane/diagnostic-reports.ts +463 -463
  23. package/extensions/taskplane/diagnostics.ts +385 -385
  24. package/extensions/taskplane/engine-worker-entry.mjs +34 -34
  25. package/extensions/taskplane/engine-worker.ts +381 -381
  26. package/extensions/taskplane/engine.ts +4539 -4527
  27. package/extensions/taskplane/execution.ts +2733 -2708
  28. package/extensions/taskplane/extension.ts +30 -9
  29. package/extensions/taskplane/formatting.ts +773 -773
  30. package/extensions/taskplane/git.ts +90 -90
  31. package/extensions/taskplane/index.ts +28 -28
  32. package/extensions/taskplane/lane-runner.ts +1383 -1360
  33. package/extensions/taskplane/mailbox.ts +689 -689
  34. package/extensions/taskplane/merge.ts +3135 -3135
  35. package/extensions/taskplane/messages.ts +985 -985
  36. package/extensions/taskplane/migrations.ts +278 -278
  37. package/extensions/taskplane/naming.ts +117 -117
  38. package/extensions/taskplane/path-resolver.ts +237 -237
  39. package/extensions/taskplane/persistence.ts +2087 -2087
  40. package/extensions/taskplane/process-registry.ts +416 -416
  41. package/extensions/taskplane/quality-gate.ts +1033 -1033
  42. package/extensions/taskplane/resume.ts +2879 -2878
  43. package/extensions/taskplane/sessions.ts +57 -57
  44. package/extensions/taskplane/settings-loader.ts +136 -136
  45. package/extensions/taskplane/settings-tui.ts +1867 -1865
  46. package/extensions/taskplane/sidecar-telemetry.ts +252 -252
  47. package/extensions/taskplane/supervisor-primer.md +1694 -1694
  48. package/extensions/taskplane/supervisor.ts +4341 -4341
  49. package/extensions/taskplane/task-executor-core.ts +550 -550
  50. package/extensions/taskplane/tmux-compat.ts +37 -37
  51. package/extensions/taskplane/types.ts +4297 -4278
  52. package/extensions/taskplane/verification.ts +542 -542
  53. package/extensions/taskplane/waves.ts +1548 -1548
  54. package/extensions/taskplane/workspace.ts +705 -705
  55. package/extensions/taskplane/worktree.ts +2604 -2505
  56. package/package.json +57 -57
  57. package/skills/create-taskplane-task/SKILL.md +465 -465
  58. package/skills/create-taskplane-task/references/prompt-template.md +285 -285
  59. package/templates/agents/local/supervisor.md +33 -33
  60. package/templates/agents/local/task-merger.md +27 -27
  61. package/templates/agents/local/task-reviewer.md +30 -30
  62. package/templates/agents/local/task-worker.md +34 -34
  63. package/templates/agents/supervisor-routing.md +92 -92
  64. package/templates/agents/supervisor.md +168 -168
  65. package/templates/agents/task-merger.md +214 -214
  66. package/templates/agents/task-reviewer.md +192 -192
  67. package/templates/agents/task-worker.md +429 -429
  68. package/templates/tasks/EXAMPLE-001-hello-world/PROMPT.md +98 -98
  69. package/templates/tasks/EXAMPLE-001-hello-world/STATUS.md +73 -73
  70. package/templates/tasks/EXAMPLE-002-parallel-smoke/PROMPT.md +97 -97
  71. package/templates/tasks/EXAMPLE-002-parallel-smoke/STATUS.md +73 -73
@@ -1,252 +1,252 @@
1
- /**
2
- * Sidecar Telemetry Utilities
3
- *
4
- * Canonical home for sidecar JSONL tailing and telemetry delta parsing.
5
- *
6
- * These utilities are used by the orchestrator to poll live worker telemetry
7
- * (token usage, tool calls, retry state) from sidecar JSONL files written by
8
- * the pi coding agent during task execution.
9
- *
10
- * @since TP-161
11
- */
12
-
13
- import { existsSync, mkdirSync, statSync, openSync, readSync, closeSync } from "fs";
14
- import { join, dirname } from "path";
15
-
16
- // ── Sidecar Directory Resolution ──────────────────────────────────────
17
-
18
- /**
19
- * Returns the .pi directory path for sidecar files (lane state, conversation logs).
20
- * In orchestrated mode, the orchestrator passes ORCH_SIDECAR_DIR pointing to the
21
- * MAIN repo's .pi/ directory (not the worktree's).
22
- */
23
- export function getSidecarDir(): string {
24
- // Orchestrator provides the main repo .pi path
25
- const orchDir = process.env.ORCH_SIDECAR_DIR;
26
- if (orchDir) {
27
- if (!existsSync(orchDir)) mkdirSync(orchDir, { recursive: true });
28
- return orchDir;
29
- }
30
- // Fallback: walk up from cwd
31
- let dir = process.cwd();
32
- for (let i = 0; i < 10; i++) {
33
- const piDir = join(dir, ".pi");
34
- if (existsSync(piDir)) return piDir;
35
- const parent = dirname(dir);
36
- if (parent === dir) break;
37
- dir = parent;
38
- }
39
- const piDir = join(process.cwd(), ".pi");
40
- if (!existsSync(piDir)) mkdirSync(piDir, { recursive: true });
41
- return piDir;
42
- }
43
-
44
- // ── Sidecar Tail State ────────────────────────────────────────────────
45
-
46
- /**
47
- * Mutable state for incremental byte-offset sidecar JSONL reading.
48
- * One instance per sidecar file, persists across poll ticks within a session.
49
- */
50
- export interface SidecarTailState {
51
- /** Byte offset of the next unread position in the sidecar file */
52
- offset: number;
53
- /** Partial trailing line from the last read (incomplete JSONL line) */
54
- partial: string;
55
- /** Whether a retry is currently active (persisted across ticks) */
56
- retryActive: boolean;
57
- }
58
-
59
- export function createSidecarTailState(): SidecarTailState {
60
- return { offset: 0, partial: "", retryActive: false };
61
- }
62
-
63
- // ── Sidecar Telemetry Delta ───────────────────────────────────────────
64
-
65
- /**
66
- * Parsed telemetry accumulated from sidecar JSONL events.
67
- * Returned by tailSidecarJsonl() on each tick.
68
- */
69
- export interface SidecarTelemetryDelta {
70
- /** Per-turn input tokens (sum of new message_end events in this tick) */
71
- inputTokens: number;
72
- outputTokens: number;
73
- cacheReadTokens: number;
74
- cacheWriteTokens: number;
75
- /** Incremental cost from new message_end events */
76
- cost: number;
77
- /** Most recent totalTokens from message_end usage (cumulative, for context %) */
78
- latestTotalTokens: number;
79
- /** Tool calls observed in this tick */
80
- toolCalls: number;
81
- /** Last tool description from tool_execution_start */
82
- lastTool: string;
83
- /** Whether a retry is currently active (persisted across ticks via SidecarTailState) */
84
- retryActive: boolean;
85
- /** Total retries started in this tick */
86
- retriesStarted: number;
87
- /** Error message from the most recent auto_retry_start */
88
- lastRetryError: string;
89
- /** Whether any sidecar events were parsed in this tick (used for callback gating) */
90
- hadEvents: boolean;
91
- /** Authoritative context usage from pi get_session_stats (pi ≥ 0.63.0, null if unavailable) */
92
- contextUsage: { percent: number; totalTokens: number; maxTokens: number } | null;
93
- /** True when a get_session_stats response was seen but lacked contextUsage (older pi) */
94
- sawStatsResponseWithoutContextUsage: boolean;
95
- }
96
-
97
- // ── Incremental JSONL Tailing ─────────────────────────────────────────
98
-
99
- /**
100
- * Incrementally read new lines from a sidecar JSONL file and parse telemetry events.
101
- *
102
- * O(new) per call — only reads bytes after the previous offset. Handles:
103
- * - File not yet created (returns zero delta)
104
- * - Empty reads (no new data since last tick)
105
- * - Partial trailing lines (buffered for next call)
106
- * - Malformed JSON lines (skipped with stderr warning, does not break iteration)
107
- *
108
- * The caller (poll loop) accumulates the returned deltas into TaskState.
109
- */
110
- export function tailSidecarJsonl(filePath: string, tailState: SidecarTailState): SidecarTelemetryDelta {
111
- const delta: SidecarTelemetryDelta = {
112
- inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
113
- cost: 0, latestTotalTokens: 0, toolCalls: 0, lastTool: "",
114
- retryActive: tailState.retryActive, retriesStarted: 0, lastRetryError: "",
115
- hadEvents: false, contextUsage: null, sawStatsResponseWithoutContextUsage: false,
116
- };
117
-
118
- // Gracefully handle missing file (wrapper hasn't written yet)
119
- let fileSize: number;
120
- try {
121
- fileSize = statSync(filePath).size;
122
- } catch {
123
- return delta; // File doesn't exist yet — no-op
124
- }
125
-
126
- if (fileSize <= tailState.offset) {
127
- return delta; // No new data
128
- }
129
-
130
- // Read new bytes from offset to end of file
131
- const bytesToRead = fileSize - tailState.offset;
132
- const buf = Buffer.alloc(bytesToRead);
133
- let fd: number;
134
- try {
135
- fd = openSync(filePath, "r");
136
- } catch {
137
- return delta; // File became inaccessible between stat and open
138
- }
139
- try {
140
- readSync(fd, buf, 0, bytesToRead, tailState.offset);
141
- } catch {
142
- closeSync(fd);
143
- return delta; // Read error — try again next tick
144
- }
145
- closeSync(fd);
146
- tailState.offset = fileSize;
147
-
148
- // Split into lines, preserving any partial trailing line
149
- const chunk = tailState.partial + buf.toString("utf-8");
150
- const lines = chunk.split("\n");
151
- // Last element is either "" (if chunk ended with \n) or a partial line
152
- tailState.partial = lines.pop() || "";
153
-
154
- for (const line of lines) {
155
- const trimmed = line.trim();
156
- if (!trimmed) continue;
157
-
158
- let event: any;
159
- try {
160
- event = JSON.parse(trimmed);
161
- } catch {
162
- // Malformed JSON — skip silently (concurrent write race, truncated line)
163
- continue;
164
- }
165
-
166
- if (!event || !event.type) continue;
167
-
168
- delta.hadEvents = true;
169
-
170
- switch (event.type) {
171
- case "message_end": {
172
- const usage = event.message?.usage;
173
- if (usage) {
174
- delta.inputTokens += usage.input || 0;
175
- delta.outputTokens += usage.output || 0;
176
- delta.cacheReadTokens += usage.cacheRead || 0;
177
- delta.cacheWriteTokens += usage.cacheWrite || 0;
178
- if (usage.cost) {
179
- delta.cost += typeof usage.cost === "object"
180
- ? (usage.cost.total || 0)
181
- : (typeof usage.cost === "number" ? usage.cost : 0);
182
- }
183
- // totalTokens is cumulative (grows each turn) — use latest value.
184
- // Include cacheRead tokens: pi's totalTokens and the
185
- // input+output fallback both exclude cache reads, but cached
186
- // tokens still consume context window capacity.
187
- const rawTotal = usage.totalTokens
188
- || ((usage.input || 0) + (usage.output || 0));
189
- const totalTokens = rawTotal + (usage.cacheRead || 0);
190
- if (totalTokens > delta.latestTotalTokens) {
191
- delta.latestTotalTokens = totalTokens;
192
- }
193
- }
194
- break;
195
- }
196
-
197
- case "tool_execution_start": {
198
- delta.toolCalls++;
199
- const toolDesc = event.toolName || "unknown";
200
- let argPreview = "";
201
- if (event.args) {
202
- if (typeof event.args === "string") {
203
- argPreview = event.args.slice(0, 80);
204
- } else if (typeof event.args === "object") {
205
- const firstVal = Object.values(event.args)[0];
206
- if (typeof firstVal === "string") {
207
- argPreview = (firstVal as string).slice(0, 80);
208
- }
209
- }
210
- }
211
- delta.lastTool = argPreview ? `${toolDesc} ${argPreview}` : toolDesc;
212
- break;
213
- }
214
-
215
- case "auto_retry_start": {
216
- delta.retriesStarted++;
217
- delta.lastRetryError = event.errorMessage || event.error || "unknown";
218
- tailState.retryActive = true;
219
- break;
220
- }
221
-
222
- case "auto_retry_end": {
223
- tailState.retryActive = false;
224
- break;
225
- }
226
-
227
- case "response": {
228
- // get_session_stats response from pi ≥ 0.63.0 — authoritative context usage
229
- if (event.success === true && event.data?.contextUsage) {
230
- const cu = event.data.contextUsage;
231
- // pi sends `percent` (pi ≥ 0.63.0); accept `percentUsed` as legacy fallback
232
- const pctValue = cu.percent ?? cu.percentUsed;
233
- if (typeof pctValue === "number") {
234
- delta.contextUsage = {
235
- percent: pctValue,
236
- totalTokens: cu.totalTokens || 0,
237
- maxTokens: cu.maxTokens || 0,
238
- };
239
- }
240
- } else if (event.success === true && event.data && !event.data.contextUsage) {
241
- // Successful get_session_stats response but no contextUsage — older pi
242
- delta.sawStatsResponseWithoutContextUsage = true;
243
- }
244
- break;
245
- }
246
- }
247
- }
248
-
249
- // Reflect persisted retry state into the delta for the caller
250
- delta.retryActive = tailState.retryActive;
251
- return delta;
252
- }
1
+ /**
2
+ * Sidecar Telemetry Utilities
3
+ *
4
+ * Canonical home for sidecar JSONL tailing and telemetry delta parsing.
5
+ *
6
+ * These utilities are used by the orchestrator to poll live worker telemetry
7
+ * (token usage, tool calls, retry state) from sidecar JSONL files written by
8
+ * the pi coding agent during task execution.
9
+ *
10
+ * @since TP-161
11
+ */
12
+
13
+ import { existsSync, mkdirSync, statSync, openSync, readSync, closeSync } from "fs";
14
+ import { join, dirname } from "path";
15
+
16
+ // ── Sidecar Directory Resolution ──────────────────────────────────────
17
+
18
+ /**
19
+ * Returns the .pi directory path for sidecar files (lane state, conversation logs).
20
+ * In orchestrated mode, the orchestrator passes ORCH_SIDECAR_DIR pointing to the
21
+ * MAIN repo's .pi/ directory (not the worktree's).
22
+ */
23
+ export function getSidecarDir(): string {
24
+ // Orchestrator provides the main repo .pi path
25
+ const orchDir = process.env.ORCH_SIDECAR_DIR;
26
+ if (orchDir) {
27
+ if (!existsSync(orchDir)) mkdirSync(orchDir, { recursive: true });
28
+ return orchDir;
29
+ }
30
+ // Fallback: walk up from cwd
31
+ let dir = process.cwd();
32
+ for (let i = 0; i < 10; i++) {
33
+ const piDir = join(dir, ".pi");
34
+ if (existsSync(piDir)) return piDir;
35
+ const parent = dirname(dir);
36
+ if (parent === dir) break;
37
+ dir = parent;
38
+ }
39
+ const piDir = join(process.cwd(), ".pi");
40
+ if (!existsSync(piDir)) mkdirSync(piDir, { recursive: true });
41
+ return piDir;
42
+ }
43
+
44
+ // ── Sidecar Tail State ────────────────────────────────────────────────
45
+
46
+ /**
47
+ * Mutable state for incremental byte-offset sidecar JSONL reading.
48
+ * One instance per sidecar file, persists across poll ticks within a session.
49
+ */
50
+ export interface SidecarTailState {
51
+ /** Byte offset of the next unread position in the sidecar file */
52
+ offset: number;
53
+ /** Partial trailing line from the last read (incomplete JSONL line) */
54
+ partial: string;
55
+ /** Whether a retry is currently active (persisted across ticks) */
56
+ retryActive: boolean;
57
+ }
58
+
59
+ export function createSidecarTailState(): SidecarTailState {
60
+ return { offset: 0, partial: "", retryActive: false };
61
+ }
62
+
63
+ // ── Sidecar Telemetry Delta ───────────────────────────────────────────
64
+
65
+ /**
66
+ * Parsed telemetry accumulated from sidecar JSONL events.
67
+ * Returned by tailSidecarJsonl() on each tick.
68
+ */
69
+ export interface SidecarTelemetryDelta {
70
+ /** Per-turn input tokens (sum of new message_end events in this tick) */
71
+ inputTokens: number;
72
+ outputTokens: number;
73
+ cacheReadTokens: number;
74
+ cacheWriteTokens: number;
75
+ /** Incremental cost from new message_end events */
76
+ cost: number;
77
+ /** Most recent totalTokens from message_end usage (cumulative, for context %) */
78
+ latestTotalTokens: number;
79
+ /** Tool calls observed in this tick */
80
+ toolCalls: number;
81
+ /** Last tool description from tool_execution_start */
82
+ lastTool: string;
83
+ /** Whether a retry is currently active (persisted across ticks via SidecarTailState) */
84
+ retryActive: boolean;
85
+ /** Total retries started in this tick */
86
+ retriesStarted: number;
87
+ /** Error message from the most recent auto_retry_start */
88
+ lastRetryError: string;
89
+ /** Whether any sidecar events were parsed in this tick (used for callback gating) */
90
+ hadEvents: boolean;
91
+ /** Authoritative context usage from pi get_session_stats (pi ≥ 0.63.0, null if unavailable) */
92
+ contextUsage: { percent: number; totalTokens: number; maxTokens: number } | null;
93
+ /** True when a get_session_stats response was seen but lacked contextUsage (older pi) */
94
+ sawStatsResponseWithoutContextUsage: boolean;
95
+ }
96
+
97
+ // ── Incremental JSONL Tailing ─────────────────────────────────────────
98
+
99
+ /**
100
+ * Incrementally read new lines from a sidecar JSONL file and parse telemetry events.
101
+ *
102
+ * O(new) per call — only reads bytes after the previous offset. Handles:
103
+ * - File not yet created (returns zero delta)
104
+ * - Empty reads (no new data since last tick)
105
+ * - Partial trailing lines (buffered for next call)
106
+ * - Malformed JSON lines (skipped with stderr warning, does not break iteration)
107
+ *
108
+ * The caller (poll loop) accumulates the returned deltas into TaskState.
109
+ */
110
+ export function tailSidecarJsonl(filePath: string, tailState: SidecarTailState): SidecarTelemetryDelta {
111
+ const delta: SidecarTelemetryDelta = {
112
+ inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
113
+ cost: 0, latestTotalTokens: 0, toolCalls: 0, lastTool: "",
114
+ retryActive: tailState.retryActive, retriesStarted: 0, lastRetryError: "",
115
+ hadEvents: false, contextUsage: null, sawStatsResponseWithoutContextUsage: false,
116
+ };
117
+
118
+ // Gracefully handle missing file (wrapper hasn't written yet)
119
+ let fileSize: number;
120
+ try {
121
+ fileSize = statSync(filePath).size;
122
+ } catch {
123
+ return delta; // File doesn't exist yet — no-op
124
+ }
125
+
126
+ if (fileSize <= tailState.offset) {
127
+ return delta; // No new data
128
+ }
129
+
130
+ // Read new bytes from offset to end of file
131
+ const bytesToRead = fileSize - tailState.offset;
132
+ const buf = Buffer.alloc(bytesToRead);
133
+ let fd: number;
134
+ try {
135
+ fd = openSync(filePath, "r");
136
+ } catch {
137
+ return delta; // File became inaccessible between stat and open
138
+ }
139
+ try {
140
+ readSync(fd, buf, 0, bytesToRead, tailState.offset);
141
+ } catch {
142
+ closeSync(fd);
143
+ return delta; // Read error — try again next tick
144
+ }
145
+ closeSync(fd);
146
+ tailState.offset = fileSize;
147
+
148
+ // Split into lines, preserving any partial trailing line
149
+ const chunk = tailState.partial + buf.toString("utf-8");
150
+ const lines = chunk.split("\n");
151
+ // Last element is either "" (if chunk ended with \n) or a partial line
152
+ tailState.partial = lines.pop() || "";
153
+
154
+ for (const line of lines) {
155
+ const trimmed = line.trim();
156
+ if (!trimmed) continue;
157
+
158
+ let event: any;
159
+ try {
160
+ event = JSON.parse(trimmed);
161
+ } catch {
162
+ // Malformed JSON — skip silently (concurrent write race, truncated line)
163
+ continue;
164
+ }
165
+
166
+ if (!event || !event.type) continue;
167
+
168
+ delta.hadEvents = true;
169
+
170
+ switch (event.type) {
171
+ case "message_end": {
172
+ const usage = event.message?.usage;
173
+ if (usage) {
174
+ delta.inputTokens += usage.input || 0;
175
+ delta.outputTokens += usage.output || 0;
176
+ delta.cacheReadTokens += usage.cacheRead || 0;
177
+ delta.cacheWriteTokens += usage.cacheWrite || 0;
178
+ if (usage.cost) {
179
+ delta.cost += typeof usage.cost === "object"
180
+ ? (usage.cost.total || 0)
181
+ : (typeof usage.cost === "number" ? usage.cost : 0);
182
+ }
183
+ // totalTokens is cumulative (grows each turn) — use latest value.
184
+ // Include cacheRead tokens: pi's totalTokens and the
185
+ // input+output fallback both exclude cache reads, but cached
186
+ // tokens still consume context window capacity.
187
+ const rawTotal = usage.totalTokens
188
+ || ((usage.input || 0) + (usage.output || 0));
189
+ const totalTokens = rawTotal + (usage.cacheRead || 0);
190
+ if (totalTokens > delta.latestTotalTokens) {
191
+ delta.latestTotalTokens = totalTokens;
192
+ }
193
+ }
194
+ break;
195
+ }
196
+
197
+ case "tool_execution_start": {
198
+ delta.toolCalls++;
199
+ const toolDesc = event.toolName || "unknown";
200
+ let argPreview = "";
201
+ if (event.args) {
202
+ if (typeof event.args === "string") {
203
+ argPreview = event.args.slice(0, 80);
204
+ } else if (typeof event.args === "object") {
205
+ const firstVal = Object.values(event.args)[0];
206
+ if (typeof firstVal === "string") {
207
+ argPreview = (firstVal as string).slice(0, 80);
208
+ }
209
+ }
210
+ }
211
+ delta.lastTool = argPreview ? `${toolDesc} ${argPreview}` : toolDesc;
212
+ break;
213
+ }
214
+
215
+ case "auto_retry_start": {
216
+ delta.retriesStarted++;
217
+ delta.lastRetryError = event.errorMessage || event.error || "unknown";
218
+ tailState.retryActive = true;
219
+ break;
220
+ }
221
+
222
+ case "auto_retry_end": {
223
+ tailState.retryActive = false;
224
+ break;
225
+ }
226
+
227
+ case "response": {
228
+ // get_session_stats response from pi ≥ 0.63.0 — authoritative context usage
229
+ if (event.success === true && event.data?.contextUsage) {
230
+ const cu = event.data.contextUsage;
231
+ // pi sends `percent` (pi ≥ 0.63.0); accept `percentUsed` as legacy fallback
232
+ const pctValue = cu.percent ?? cu.percentUsed;
233
+ if (typeof pctValue === "number") {
234
+ delta.contextUsage = {
235
+ percent: pctValue,
236
+ totalTokens: cu.totalTokens || 0,
237
+ maxTokens: cu.maxTokens || 0,
238
+ };
239
+ }
240
+ } else if (event.success === true && event.data && !event.data.contextUsage) {
241
+ // Successful get_session_stats response but no contextUsage — older pi
242
+ delta.sawStatsResponseWithoutContextUsage = true;
243
+ }
244
+ break;
245
+ }
246
+ }
247
+ }
248
+
249
+ // Reflect persisted retry state into the delta for the caller
250
+ delta.retryActive = tailState.retryActive;
251
+ return delta;
252
+ }