pikiloom 0.4.26 → 0.4.27

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 (155) hide show
  1. package/dashboard/dist/assets/{AgentTab-BNJ73P0m.js → AgentTab-DIP-y4dl.js} +1 -1
  2. package/dashboard/dist/assets/ConnectionModal-BCfvhkiI.js +1 -0
  3. package/dashboard/dist/assets/{DirBrowser-D-GT-G-w.js → DirBrowser-Dot7ua9-.js} +1 -1
  4. package/dashboard/dist/assets/{ExtensionsTab-BrJMulJR.js → ExtensionsTab-GioMx4S6.js} +1 -1
  5. package/dashboard/dist/assets/{IMAccessTab-auPnx468.js → IMAccessTab-C9_r8jJP.js} +1 -1
  6. package/dashboard/dist/assets/{Modal-lYfO9m-I.js → Modal-M1AP1sgR.js} +1 -1
  7. package/dashboard/dist/assets/{Modals-PRdWOIY9.js → Modals-CCm3UUfJ.js} +1 -1
  8. package/dashboard/dist/assets/{Select-BhbU-rll.js → Select-YubDM16P.js} +1 -1
  9. package/dashboard/dist/assets/{SessionPanel-_0dLWvEb.js → SessionPanel-Cnv9o96n.js} +1 -1
  10. package/dashboard/dist/assets/SystemTab-VgNivIe3.js +1 -0
  11. package/dashboard/dist/assets/index-BY7l20bi.js +23 -0
  12. package/dashboard/dist/assets/index-BlqvzDUL.js +3 -0
  13. package/dashboard/dist/assets/index-CXIN3nTr.css +1 -0
  14. package/dashboard/dist/assets/{shared-C5uXvhZJ.js → shared-C0SAInnT.js} +1 -1
  15. package/dashboard/dist/index.html +2 -2
  16. package/dist/agent/acp-client.js +0 -59
  17. package/dist/agent/artifacts.js +1 -38
  18. package/dist/agent/auto-update.js +2 -78
  19. package/dist/agent/await-resume.js +0 -20
  20. package/dist/agent/cli/auth.js +3 -51
  21. package/dist/agent/cli/catalog.js +0 -3
  22. package/dist/agent/cli/detector.js +1 -19
  23. package/dist/agent/cli/index.js +0 -3
  24. package/dist/agent/cli/registry.js +0 -25
  25. package/dist/agent/driver.js +0 -14
  26. package/dist/agent/drivers/claude-tui.js +2 -769
  27. package/dist/agent/drivers/claude.js +12 -631
  28. package/dist/agent/drivers/codex.js +11 -162
  29. package/dist/agent/drivers/gemini.js +6 -114
  30. package/dist/agent/drivers/hermes.js +0 -128
  31. package/dist/agent/goal.js +0 -45
  32. package/dist/agent/handover.js +1 -46
  33. package/dist/agent/images.js +7 -133
  34. package/dist/agent/index.js +1 -29
  35. package/dist/agent/mcp/bridge.js +1 -197
  36. package/dist/agent/mcp/extensions.js +5 -84
  37. package/dist/agent/mcp/oauth.js +2 -69
  38. package/dist/agent/mcp/registry.js +2 -33
  39. package/dist/agent/mcp/session-server.js +2 -45
  40. package/dist/agent/mcp/tools/ask-user.js +0 -6
  41. package/dist/agent/mcp/tools/await-resume.js +0 -15
  42. package/dist/agent/mcp/tools/goal.js +0 -25
  43. package/dist/agent/mcp/tools/types.js +0 -5
  44. package/dist/agent/mcp/tools/workspace.js +0 -23
  45. package/dist/agent/npm.js +0 -14
  46. package/dist/agent/session.js +86 -196
  47. package/dist/agent/skill-installer.js +3 -70
  48. package/dist/agent/skills.js +0 -43
  49. package/dist/agent/stream.js +5 -148
  50. package/dist/agent/types.js +0 -12
  51. package/dist/agent/utils.js +2 -120
  52. package/dist/bot/bot.js +3 -468
  53. package/dist/bot/command-ui.js +0 -45
  54. package/dist/bot/commands.js +0 -51
  55. package/dist/bot/headless-bot.js +0 -21
  56. package/dist/bot/host.js +1 -5
  57. package/dist/bot/human-loop.js +0 -8
  58. package/dist/bot/menu.js +0 -3
  59. package/dist/bot/orchestration.js +0 -3
  60. package/dist/bot/render-shared.js +0 -63
  61. package/dist/bot/session-hub.js +14 -92
  62. package/dist/bot/session-status.js +1 -5
  63. package/dist/bot/streaming.js +0 -53
  64. package/dist/browser-profile.js +0 -51
  65. package/dist/browser-supervisor.js +0 -75
  66. package/dist/catalog/cli-tools.js +0 -53
  67. package/dist/catalog/index.js +0 -18
  68. package/dist/catalog/local-models.js +0 -18
  69. package/dist/catalog/mcp-servers.js +0 -35
  70. package/dist/catalog/skill-repos.js +0 -32
  71. package/dist/channels/base.js +0 -19
  72. package/dist/channels/dingtalk/bot.js +0 -3
  73. package/dist/channels/dingtalk/channel.js +0 -33
  74. package/dist/channels/discord/bot.js +0 -7
  75. package/dist/channels/discord/channel.js +0 -22
  76. package/dist/channels/feishu/bot.js +0 -71
  77. package/dist/channels/feishu/channel.js +0 -78
  78. package/dist/channels/feishu/markdown.js +0 -5
  79. package/dist/channels/feishu/render.js +0 -45
  80. package/dist/channels/health.js +0 -46
  81. package/dist/channels/slack/bot.js +0 -7
  82. package/dist/channels/slack/channel.js +1 -27
  83. package/dist/channels/states.js +0 -3
  84. package/dist/channels/telegram/bot.js +0 -60
  85. package/dist/channels/telegram/channel.js +2 -99
  86. package/dist/channels/telegram/directory.js +0 -3
  87. package/dist/channels/telegram/live-preview.js +0 -8
  88. package/dist/channels/telegram/render.js +0 -30
  89. package/dist/channels/wecom/bot.js +0 -3
  90. package/dist/channels/wecom/channel.js +0 -58
  91. package/dist/channels/weixin/api.js +0 -9
  92. package/dist/channels/weixin/bot.js +0 -88
  93. package/dist/channels/weixin/channel.js +0 -6
  94. package/dist/cli/autostart.js +1 -44
  95. package/dist/cli/channel-supervisor.js +1 -59
  96. package/dist/cli/channels.js +0 -6
  97. package/dist/cli/main.js +1 -145
  98. package/dist/cli/onboarding.js +0 -6
  99. package/dist/cli/run.js +0 -11
  100. package/dist/cli/setup-wizard.js +0 -3
  101. package/dist/core/config/runtime-config.js +0 -38
  102. package/dist/core/config/user-config.js +0 -63
  103. package/dist/core/config/validation.js +0 -9
  104. package/dist/core/constants.js +0 -268
  105. package/dist/core/git.js +0 -31
  106. package/dist/core/legacy-compat.js +1 -28
  107. package/dist/core/logging.js +0 -3
  108. package/dist/core/platform.js +0 -28
  109. package/dist/core/process-control.js +0 -8
  110. package/dist/core/secrets/index.js +0 -17
  111. package/dist/core/secrets/inline-seal.js +0 -7
  112. package/dist/core/secrets/ref.js +0 -19
  113. package/dist/core/secrets/resolver.js +0 -8
  114. package/dist/core/secrets/store.js +1 -12
  115. package/dist/core/utils.js +1 -37
  116. package/dist/core/version.js +0 -3
  117. package/dist/dashboard/platform.js +1 -35
  118. package/dist/dashboard/routes/agents.js +2 -71
  119. package/dist/dashboard/routes/cli.js +0 -27
  120. package/dist/dashboard/routes/config.js +0 -54
  121. package/dist/dashboard/routes/extensions.js +0 -132
  122. package/dist/dashboard/routes/local-models.js +0 -68
  123. package/dist/dashboard/routes/models.js +0 -46
  124. package/dist/dashboard/routes/sessions.js +0 -100
  125. package/dist/dashboard/runtime.js +1 -58
  126. package/dist/dashboard/server.js +0 -49
  127. package/dist/dashboard/session-control.js +2 -69
  128. package/dist/model/catalog.js +1 -17
  129. package/dist/model/index.js +0 -15
  130. package/dist/model/injector.js +1 -181
  131. package/dist/model/provider-models.js +1 -48
  132. package/dist/model/responses-bridge.js +3 -45
  133. package/dist/model/store.js +0 -17
  134. package/dist/model/types.js +0 -12
  135. package/dist/model/validation.js +1 -18
  136. package/dist/pikichannel/adapter-pikiloom.js +0 -31
  137. package/dist/pikichannel/code.js +0 -12
  138. package/dist/pikichannel/codec.js +0 -14
  139. package/dist/pikichannel/host.js +4 -33
  140. package/dist/pikichannel/protocol.js +0 -52
  141. package/dist/pikichannel/rendezvous-broker.js +5 -26
  142. package/dist/pikichannel/rendezvous-host.js +3 -16
  143. package/dist/pikichannel/server.js +6 -55
  144. package/dist/pikichannel/transport.js +0 -25
  145. package/dist/pikichannel/transports/webrtc-host.js +0 -18
  146. package/dist/pikichannel/transports/webrtc-shared.js +4 -34
  147. package/dist/pikichannel/transports/websocket-host.js +1 -14
  148. package/dist/pikichannel/turn.js +2 -69
  149. package/dist/pikichannel/web/sdk.js +14 -71
  150. package/package.json +1 -1
  151. package/dashboard/dist/assets/ConnectionModal-Dt05H2f9.js +0 -1
  152. package/dashboard/dist/assets/SystemTab-CN_QrRKt.js +0 -1
  153. package/dashboard/dist/assets/index-CP3LYJR2.js +0 -23
  154. package/dashboard/dist/assets/index-C_neLnaO.js +0 -3
  155. package/dashboard/dist/assets/index-CyCspgtc.css +0 -1
@@ -1,20 +1,12 @@
1
- /**
2
- * Claude Code CLI driver: stream parsing, session reads, model listing, usage.
3
- */
4
1
  import fs from 'node:fs';
5
2
  import path from 'node:path';
6
3
  import { execSync, spawn } from 'node:child_process';
7
4
  import { createInterface } from 'node:readline';
8
5
  import { registerDriver } from '../driver.js';
9
- import {
10
- // shared helpers
11
- Q, run, agentError, agentLog, agentWarn, buildStreamPreviewMeta, computeContext, pushRecentActivity, summarizeClaudeToolUse, summarizeClaudeToolResult, joinErrorMessages, parseTodoWriteAsPlan, previewToolCallInput, previewToolCallResult, detectClaudeApiError, isRetryableClaudeApiError, detectClaudeModelError, claudeModelErrorMessage, emitSessionIdUpdate, IMAGE_EXTS, mimeForExt, listPikiloomSessions, mergeManagedAndNativeSessions, managedRecordToSessionInfo, readTailLines, stripInjectedPrompts, sanitizeSessionUserPreviewText, SESSION_PREVIEW_IMAGE_PLACEHOLDER_RE, CLAUDE_AT_MENTION_IMAGE_RE, extractClaudeAtMentionImagePaths, attachAgentImage, applyTurnWindow, shortValue, roundPercent, modelFamily, normalizeClaudeModelId, emptyUsage, normalizeUsageStatus, collapseSkillPrompt, } from '../index.js';
6
+ import { Q, run, agentError, agentLog, agentWarn, buildStreamPreviewMeta, computeContext, pushRecentActivity, summarizeClaudeToolUse, summarizeClaudeToolResult, joinErrorMessages, parseTodoWriteAsPlan, previewToolCallInput, previewToolCallResult, detectClaudeApiError, isRetryableClaudeApiError, detectClaudeModelError, claudeModelErrorMessage, emitSessionIdUpdate, IMAGE_EXTS, mimeForExt, listPikiloomSessions, mergeManagedAndNativeSessions, managedRecordToSessionInfo, readTailLines, stripInjectedPrompts, sanitizeSessionUserPreviewText, SESSION_PREVIEW_IMAGE_PLACEHOLDER_RE, CLAUDE_AT_MENTION_IMAGE_RE, extractClaudeAtMentionImagePaths, attachAgentImage, applyTurnWindow, shortValue, roundPercent, modelFamily, normalizeClaudeModelId, emptyUsage, normalizeUsageStatus, collapseSkillPrompt, } from '../index.js';
12
7
  import { AGENT_STREAM_HARD_KILL_GRACE_MS, AGENT_GRACEFUL_ABORT_GRACE_MS, SESSION_RUNNING_THRESHOLD_MS } from '../../core/constants.js';
13
8
  import { terminateProcessTree } from '../../core/process-control.js';
14
9
  import { getHome, IS_MAC, encodePathAsDirName } from '../../core/platform.js';
15
- // ---------------------------------------------------------------------------
16
- // Multimodal stdin
17
- // ---------------------------------------------------------------------------
18
10
  function buildClaudeUserMessage(prompt, attachments) {
19
11
  const content = [];
20
12
  for (const filePath of attachments) {
@@ -42,20 +34,6 @@ function claudeUsesStreamJsonInput(o) {
42
34
  return !!o.attachments?.length || !!o.onSteerReady;
43
35
  }
44
36
  const CLAUDE_STEER_IDLE_CLOSE_MS = 1200;
45
- /**
46
- * Effort + multi-agent-Workflow gate args, shared by BOTH Claude spawn paths
47
- * (`claude -p` in claudeCmd below and the PTY/TUI driver in claude-tui.ts).
48
- * Kept in one place so the gate can never drift between them — the omission
49
- * that once left the Workflow tool always-on under the TUI driver.
50
- *
51
- * "ultra" is a synthetic picker rung (max depth + Workflow orchestration), never
52
- * a real --effort value — translate it to `max` so a stray "ultra" can't reach
53
- * and break the CLI, and treat it as an implicit workflow opt-in. The Workflow
54
- * tool ships in the default toolset and triggers on a bare "workflow" keyword;
55
- * under the bypassPermissions mode pikiloom runs by default that could auto-spawn
56
- * a fleet of sub-agents, so drop it entirely unless orchestration was explicitly
57
- * enabled (the workflow flag or the "ultra" rung).
58
- */
59
37
  export function claudeEffortAndWorkflowArgs(o) {
60
38
  const args = [];
61
39
  const ultraEffort = o.thinkingEffort === 'ultra';
@@ -65,25 +43,6 @@ export function claudeEffortAndWorkflowArgs(o) {
65
43
  args.push('--disallowed-tools', 'Workflow');
66
44
  return args;
67
45
  }
68
- /**
69
- * Env keys the claude CLI exports to its own subprocesses (Bash tool, hooks)
70
- * to mark them as children of a running session. If pikiloom itself was
71
- * launched from inside a Claude Code session — agent-driven `npm run dev`
72
- * restarts, `! npx pikiloom` typed into the TUI, the self-bootstrap path —
73
- * these leak into the daemon's environment and every claude it spawns
74
- * inherits them. A claude started with `CLAUDE_CODE_CHILD_SESSION` set runs
75
- * in child-session mode: it mirrors transcript persistence to its (absent)
76
- * SDK parent instead of writing `~/.claude/projects/<dir>/<id>.jsonl`.
77
- * The TUI driver tails that JSONL as its only text source, so a contaminated
78
- * spawn streams nothing, returns "(no textual response)", and loses the whole
79
- * turn on SIGTERM. Verified on 2.1.173: with these vars set the transcript
80
- * never grows past the ai-title line; with them scrubbed every event lands
81
- * 0.2–1.2s after it happens.
82
- *
83
- * Deliberately a closed list: config-style vars users set on purpose
84
- * (CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_MAX_OUTPUT_TOKENS, …) must survive.
85
- * Shared by both spawn paths (`claude -p` here and the PTY/TUI driver).
86
- */
87
46
  const CLAUDE_SESSION_CONTEXT_ENV_KEYS = [
88
47
  'CLAUDECODE',
89
48
  'CLAUDE_CODE_CHILD_SESSION',
@@ -98,9 +57,6 @@ export function scrubClaudeSessionContextEnv(env) {
98
57
  for (const key of CLAUDE_SESSION_CONTEXT_ENV_KEYS)
99
58
  delete env[key];
100
59
  }
101
- // ---------------------------------------------------------------------------
102
- // Command & parser
103
- // ---------------------------------------------------------------------------
104
60
  function claudeCmd(o) {
105
61
  const args = ['claude', '-p', '--verbose', '--output-format', 'stream-json', '--include-partial-messages'];
106
62
  const model = normalizeClaudeModelId(o.claudeModel);
@@ -108,11 +64,6 @@ function claudeCmd(o) {
108
64
  args.push('--model', model);
109
65
  if (o.claudePermissionMode)
110
66
  args.push('--permission-mode', o.claudePermissionMode);
111
- // Fork: branch off the parent's full history into a fresh sessionId. The
112
- // claude CLI exposes this via `--resume <parent> --fork-session`; the new
113
- // session inherits the parent's transcript and gets its own JSONL file.
114
- // We record `forkedAtTurn` as lineage metadata only — the agent's actual
115
- // context is the full parent history.
116
67
  if (o.forkOf) {
117
68
  args.push('--resume', o.forkOf.parentSessionId, '--fork-session');
118
69
  }
@@ -126,8 +77,6 @@ function claudeCmd(o) {
126
77
  if (o.attachments?.length)
127
78
  o._stdinOverride = buildClaudeUserMessage(o.prompt, o.attachments);
128
79
  }
129
- // Effort + Workflow gate — shared with the TUI driver (claude-tui.ts) so the
130
- // two spawn paths can never drift. See claudeEffortAndWorkflowArgs.
131
80
  args.push(...claudeEffortAndWorkflowArgs(o));
132
81
  if (o.claudeAppendSystemPrompt)
133
82
  args.push('--append-system-prompt', o.claudeAppendSystemPrompt);
@@ -137,16 +86,10 @@ function claudeCmd(o) {
137
86
  args.push(...o.claudeExtraArgs);
138
87
  return args;
139
88
  }
140
- /**
141
- * Route a JSONL event that belongs to a sub-agent (parent_tool_use_id is set).
142
- * The event is owned by the Task tool_use whose id matches `parentToolUseId`;
143
- * we accumulate the sub-agent's model and tool calls without ever touching
144
- * `s.recentActivity` or `s.text` (those stay scoped to the parent agent).
145
- */
146
89
  function routeClaudeSubAgentEvent(ev, t, parentToolUseId, s) {
147
90
  const sub = s.subAgents.get(parentToolUseId);
148
91
  if (!sub)
149
- return; // Task tool_use should always precede sub-agent events; ignore stragglers.
92
+ return;
150
93
  if (t === 'system' || t === 'assistant') {
151
94
  const model = ev.model ?? ev.message?.model;
152
95
  if (typeof model === 'string' && model.trim())
@@ -189,16 +132,8 @@ function buildClaudeTurnUsage(u, turnOutput) {
189
132
  meta.turnOutputTokens = turnOutput;
190
133
  return meta;
191
134
  }
192
- /** Hard cap beyond which a native Claude session is treated as idle regardless
193
- * of the last JSONL event — guards against sessions abandoned mid-turn (Ctrl-C
194
- * during a tool call, terminal crash) so they don't stick on "running". */
195
135
  const CLAUDE_NATIVE_RUNNING_HARD_CAP_MS = 5 * 60 * 1000;
196
136
  const CLAUDE_TURN_TERMINAL_STOP_REASONS = new Set(['end_turn', 'stop_sequence', 'max_tokens', 'refusal']);
197
- /** Inspect a native Claude JSONL to decide whether a turn is currently in
198
- * progress. Pure-mtime detection misses cases where Claude is mid-tool-use and
199
- * hasn't appended for >10s; this checks the trailing event for a non-terminal
200
- * state (user message awaiting reply, or assistant message with a non-end stop
201
- * reason like `tool_use`). */
202
137
  function isClaudeNativeSessionRunning(filePath, mtimeMs) {
203
138
  const age = Date.now() - mtimeMs;
204
139
  if (age < SESSION_RUNNING_THRESHOLD_MS)
@@ -218,7 +153,6 @@ function isClaudeNativeSessionRunning(filePath, mtimeMs) {
218
153
  continue;
219
154
  }
220
155
  const t = ev?.type;
221
- // Skip auto-title events — they fire after a turn completes and are not a liveness signal.
222
156
  if (t === 'ai-title' || t === 'system')
223
157
  continue;
224
158
  if (t === 'user')
@@ -227,7 +161,6 @@ function isClaudeNativeSessionRunning(filePath, mtimeMs) {
227
161
  const stop = ev?.message?.stop_reason;
228
162
  return stop != null ? !CLAUDE_TURN_TERMINAL_STOP_REASONS.has(stop) : true;
229
163
  }
230
- // Unknown event type — be conservative.
231
164
  return false;
232
165
  }
233
166
  return false;
@@ -244,15 +177,6 @@ export function claudeContextWindowFromModel(model) {
244
177
  return 1_000_000;
245
178
  return null;
246
179
  }
247
- // Mirrors Claude Code 2.1.112's `Yn()` + `v38()` denominator (and `pj()` display
248
- // formula) — verified by extracting cli.js from the last JS-source release.
249
- // uDY = 20_000 // max output reservation cap
250
- // t_7 = 13_000 // auto-compact buffer
251
- // Effective denominator with autoCompact enabled (cc's default) is
252
- // `window - 20K - 13K`. Without these subtractions the percent we display
253
- // drifts from the number cc itself reports (e.g. Opus 1M shows the user
254
- // `Context left until auto-compact: X%` against a 967_000 denominator, not
255
- // against a flat 1_000_000).
256
180
  const CLAUDE_MAX_OUTPUT_RESERVE = 20_000;
257
181
  const CLAUDE_AUTOCOMPACT_BUFFER = 13_000;
258
182
  const CLAUDE_USABLE_WINDOW_RESERVE = CLAUDE_MAX_OUTPUT_RESERVE + CLAUDE_AUTOCOMPACT_BUFFER;
@@ -263,11 +187,6 @@ export function claudeEffectiveContextWindow(advertised) {
263
187
  return advertised;
264
188
  return advertised - CLAUDE_USABLE_WINDOW_RESERVE;
265
189
  }
266
- // cc's `ey6` (was `hYB` pre-2.1.112) — context size of one assistant call. The
267
- // `output_tokens` slice matters: cc walks back to the latest assistant message
268
- // and adds its output to the input/cached/creation counters because that
269
- // generated content already exists in conversation history and would be
270
- // re-fed to the next call.
271
190
  function claudeContextUsedFromUsage(u) {
272
191
  return (u.input ?? 0) + (u.cached ?? 0) + (u.cacheCreation ?? 0) + (u.output ?? 0);
273
192
  }
@@ -280,28 +199,10 @@ function recomputeClaudeContextUsed(s) {
280
199
  });
281
200
  s.contextUsedTokens = total > 0 ? total : null;
282
201
  }
283
- /**
284
- * Tool names whose `tool_result` image content does NOT count as
285
- * assistant-generated output and must NOT be re-rendered in the assistant
286
- * card. These tools merely read existing files (user attachments, project
287
- * assets) — the bytes already lived somewhere the user can see them, so
288
- * surfacing them again in the assistant block creates a duplicate of the
289
- * user's own upload below Claude's text reply.
290
- *
291
- * Genuine image producers (MCP image-gen tools, mermaid-mcp, chart, dalle-mcp,
292
- * Codex built-in image_gen, …) are NOT in this set and continue to render
293
- * normally.
294
- */
295
202
  const CLAUDE_FILE_READING_TOOLS = new Set(['Read']);
296
203
  function isClaudeFileReadingTool(toolName) {
297
204
  return !!toolName && CLAUDE_FILE_READING_TOOLS.has(toolName);
298
205
  }
299
- /**
300
- * Walk a content array (assistant message body OR a tool_result.content array)
301
- * and push any image entries into the stream state's `imageBlocks`, deduped by
302
- * the first 64 chars of base64 data. Used during live parsing so the final
303
- * StreamResult carries every image the turn produced.
304
- */
305
206
  function accumulateClaudeImagesFromContent(content, s) {
306
207
  if (!Array.isArray(content))
307
208
  return;
@@ -320,9 +221,6 @@ function accumulateClaudeImagesFromContent(content, s) {
320
221
  s.imageBlocks?.push(block);
321
222
  }
322
223
  else if (entry.type === 'tool_result' && Array.isArray(entry.content)) {
323
- // MCP / Skill tool_result with multimodal content — recurse for images,
324
- // but skip tools that just read existing files (the bytes are already
325
- // visible in the user's own upload bubble).
326
224
  const toolName = entry.tool_use_id ? s.claudeToolsById?.get(entry.tool_use_id)?.name : null;
327
225
  if (isClaudeFileReadingTool(toolName))
328
226
  continue;
@@ -330,12 +228,6 @@ function accumulateClaudeImagesFromContent(content, s) {
330
228
  }
331
229
  }
332
230
  }
333
- /**
334
- * Read the server-assigned task id from a TaskCreate tool_result. Claude
335
- * surfaces it via the structured `ev.toolUseResult.task.id` companion field,
336
- * with a textual fallback ("Task #N created successfully: …") that we parse
337
- * if the structured form is missing.
338
- */
339
231
  function readClaudeTaskCreateId(ev, block) {
340
232
  const structured = ev?.toolUseResult?.task?.id;
341
233
  if (structured != null && String(structured).trim())
@@ -348,15 +240,8 @@ function readClaudeTaskCreateId(ev, block) {
348
240
  }
349
241
  return null;
350
242
  }
351
- /**
352
- * Rebuild s.plan from the accumulated TaskCreate / TaskUpdate state so the
353
- * dashboard + IM plan card show the canonical Claude Code 2.x task progress.
354
- * Order follows insertion order (matches the on-screen Claude task list).
355
- */
356
243
  function rebuildClaudePlanFromTasks(s) {
357
244
  if (!s.claudeTaskOrder?.length) {
358
- // Nothing to render — leave s.plan alone so TodoWrite-era data (if any)
359
- // doesn't get clobbered by an empty rebuild.
360
245
  return;
361
246
  }
362
247
  const steps = [];
@@ -384,7 +269,6 @@ function ensureClaudeBgAgentState(s) {
384
269
  if (typeof s.lastTaskNotificationAt !== 'number')
385
270
  s.lastTaskNotificationAt = 0;
386
271
  }
387
- /** Record a Task/Agent tool_use launched with `run_in_background: true`. */
388
272
  export function registerClaudeBackgroundAgentLaunch(s, toolUseId) {
389
273
  const id = String(toolUseId || '').trim();
390
274
  if (!id)
@@ -392,18 +276,6 @@ export function registerClaudeBackgroundAgentLaunch(s, toolUseId) {
392
276
  ensureClaudeBgAgentState(s);
393
277
  s.bgAgentLaunchedToolUseIds.add(id);
394
278
  }
395
- /**
396
- * Record a `Bash` tool_use launched with `run_in_background: true`.
397
- *
398
- * Background Bash lives *inside the claude process* exactly like a
399
- * backgrounded sub-agent: its tool_result is a launch ack, the real
400
- * completion arrives later as a `<task-notification>` which re-invokes the
401
- * model in the same process. Before this registration existed only Task/Agent
402
- * launches counted as "pending background work" — a turn that backgrounded a
403
- * Bash command would hit Stop, decideClaudeTuiStop saw pending=0 and
404
- * terminated the PTY, killing the command and its future report-back turn
405
- * (the「claude 后台任务一停止就被掐死」failure).
406
- */
407
279
  export function registerClaudeBackgroundBashLaunch(s, toolUseId) {
408
280
  const id = String(toolUseId || '').trim();
409
281
  if (!id)
@@ -412,7 +284,6 @@ export function registerClaudeBackgroundBashLaunch(s, toolUseId) {
412
284
  s.bgAgentLaunchedToolUseIds.add(id);
413
285
  s.bgBashToolUseIds.add(id);
414
286
  }
415
- /** Launched background tasks (agents + bash) whose <task-notification> hasn't arrived yet. */
416
287
  export function pendingClaudeBackgroundAgentCount(s) {
417
288
  const launched = s?.bgAgentLaunchedToolUseIds;
418
289
  if (!launched?.size)
@@ -425,9 +296,6 @@ export function pendingClaudeBackgroundAgentCount(s) {
425
296
  }
426
297
  return pending;
427
298
  }
428
- /** Pending background *Bash* tasks specifically. Unlike agents (whose sidecar
429
- * JSONL keeps emitting events while alive), a background command is silent by
430
- * nature — callers use this to pick a longer hold/stall budget. */
431
299
  export function pendingClaudeBackgroundBashCount(s) {
432
300
  const bash = s?.bgBashToolUseIds;
433
301
  if (!bash?.size)
@@ -440,13 +308,6 @@ export function pendingClaudeBackgroundBashCount(s) {
440
308
  }
441
309
  return pending;
442
310
  }
443
- /**
444
- * Pull the background task id out of a launch ack. Claude Code's backgrounded
445
- * Bash tool_result reads like "Command running in background with ID: bash_3
446
- * (output: …)" — the id is what the later <task-notification> carries (its
447
- * <tool-use-id> is often omitted for bash), so mapping id → tool_use here is
448
- * what lets applyClaudeTaskNotification resolve the completion.
449
- */
450
311
  export function extractClaudeBackgroundTaskId(content) {
451
312
  let text = '';
452
313
  if (typeof content === 'string')
@@ -470,13 +331,6 @@ export function extractClaudeBackgroundTaskId(content) {
470
331
  const m = text.match(/\b(?:ID|id)\s*[::]?\s*[`"']?([A-Za-z0-9][A-Za-z0-9_-]{1,63})/);
471
332
  return m ? m[1] : null;
472
333
  }
473
- /**
474
- * Pull the workflow runId (`wf_…`) out of a Workflow launch ack. The Workflow
475
- * tool returns immediately with `{ runId }` and the orchestration runs in the
476
- * background; its later `<task-notification>` may carry only that id (no
477
- * `<tool-use-id>`), so mapping runId → tool_use here is what lets
478
- * applyClaudeTaskNotification resolve the completion and release the PTY hold.
479
- */
480
334
  export function extractClaudeWorkflowRunId(content) {
481
335
  let text = '';
482
336
  if (typeof content === 'string')
@@ -500,17 +354,6 @@ export function extractClaudeWorkflowRunId(content) {
500
354
  const m = text.match(/\bwf_[a-z0-9][a-z0-9-]{4,}\b/i);
501
355
  return m ? m[0] : null;
502
356
  }
503
- /**
504
- * Parse a `<task-notification>` wrapper out of a user event's content.
505
- * Shape (observed, Claude Code 2.x):
506
- * <task-notification>
507
- * <task-id>a7a61bd5e0e76e0f3</task-id>
508
- * <tool-use-id>toolu_01MsPk…</tool-use-id> ← omitted for orphaned tasks
509
- * <output-file>…</output-file>
510
- * <status>completed | failed | killed</status>
511
- * <summary>…</summary>
512
- * </task-notification>
513
- */
514
357
  export function extractClaudeTaskNotification(content) {
515
358
  let text = '';
516
359
  if (typeof content === 'string')
@@ -529,18 +372,8 @@ export function extractClaudeTaskNotification(content) {
529
372
  };
530
373
  return { taskId: tag('task-id'), toolUseId: tag('tool-use-id'), status: tag('status') };
531
374
  }
532
- /**
533
- * Fold a task-notification into the stream state: mark the matching background
534
- * agent completed and flip its preview card status. Notifications for unknown
535
- * tasks (orphans from a previous process, background Bash, …) still bump
536
- * `lastTaskNotificationAt` — the TUI driver uses that timestamp to tell a
537
- * pre-notification Stop hook apart from the model's post-resume Stop.
538
- */
539
375
  export function applyClaudeTaskNotification(s, notification, eventAtMs) {
540
376
  ensureClaudeBgAgentState(s);
541
- // Prefer the event's own timestamp: when a JSONL flush delivers the
542
- // notification and the wrap-up segment's Stop in one burst, parse-time would
543
- // postdate the Stop and make a genuinely-fresh Stop look stale.
544
377
  s.lastTaskNotificationAt = eventAtMs && Number.isFinite(eventAtMs) ? eventAtMs : Date.now();
545
378
  const toolUseId = notification.toolUseId
546
379
  || (notification.taskId ? s.bgTaskIdToToolUse.get(notification.taskId) : undefined)
@@ -563,11 +396,6 @@ export function applyClaudeTaskNotification(s, notification, eventAtMs) {
563
396
  }
564
397
  export function claudeParse(ev, s) {
565
398
  const t = ev.type || '';
566
- // Sub-agent events (Task tool spawns a child agent) carry parent_tool_use_id
567
- // pointing back to the parent's Task tool_use_id. They share the JSONL stream
568
- // with parent events but must be isolated so their tool calls don't pollute
569
- // the parent's activity list and their model/effort don't override the
570
- // parent's runtime context.
571
399
  const parentToolUseId = (typeof ev.parent_tool_use_id === 'string' && ev.parent_tool_use_id)
572
400
  ? ev.parent_tool_use_id : null;
573
401
  if (parentToolUseId) {
@@ -587,12 +415,6 @@ export function claudeParse(ev, s) {
587
415
  const inner = ev.event || {};
588
416
  if (inner.type === 'message_start') {
589
417
  const u = inner.message?.usage;
590
- // Per-call semantics: each LLM call inside a turn resets the counters,
591
- // so the displayed In/Cached/Out and contextPercent describe the same
592
- // call (matches cc's `LX(messages)` which returns the latest assistant
593
- // usage, not a cumulative across calls).
594
- // The finished call's output is folded into the turn-cumulative base
595
- // first so `turnOutputTokens` keeps climbing across tool roundtrips.
596
418
  s.turnOutputTokensBase = (s.turnOutputTokensBase ?? 0) + (s.outputTokens ?? 0);
597
419
  s.inputTokens = u?.input_tokens ?? 0;
598
420
  s.cachedInputTokens = u?.cache_read_input_tokens ?? 0;
@@ -600,10 +422,6 @@ export function claudeParse(ev, s) {
600
422
  s.outputTokens = 0;
601
423
  recomputeClaudeContextUsed(s);
602
424
  }
603
- // When a new text/thinking block starts after an earlier one (e.g. between
604
- // a text block and a tool_use and back to text), insert a paragraph break
605
- // so deltas from distinct blocks don't collapse into a single markdown
606
- // paragraph.
607
425
  if (inner.type === 'content_block_start') {
608
426
  const blockType = inner.content_block?.type;
609
427
  if (blockType === 'text' && s.text && !s.text.endsWith('\n\n')) {
@@ -625,8 +443,6 @@ export function claudeParse(ev, s) {
625
443
  s.stopReason = d.stop_reason ?? s.stopReason;
626
444
  const u = inner.usage;
627
445
  if (u) {
628
- // message_delta reports running totals for the active call. Per-call
629
- // semantics: just overwrite — last value wins.
630
446
  if (u.input_tokens != null)
631
447
  s.inputTokens = u.input_tokens;
632
448
  if (u.cache_read_input_tokens != null)
@@ -647,17 +463,7 @@ export function claudeParse(ev, s) {
647
463
  }
648
464
  if (t === 'assistant') {
649
465
  const msg = ev.message || {};
650
- // Skip Claude CLI's synthetic feedback events on the live channel — they
651
- // arrive as `assistant` events but represent runtime notices (no response,
652
- // model error, …), not real Claude output. The historical jsonl reader
653
- // converts them into `system_notice` blocks; on the live stream we just
654
- // drop them so they don't pollute s.text / s.thinking.
655
466
  if (msg.model === '<synthetic>') {
656
- // …except the "selected model is unavailable" notice (404 model_not_found):
657
- // a hard, non-retryable failure. The result event's text fallback below
658
- // also catches it, but recording s.errors here upgrades the turn from a
659
- // bare "(no textual response)" reply to a clear error + non-retryable
660
- // stopReason (so doClaudeWithRetry won't loop on the same dead model).
661
467
  if (!s.errors) {
662
468
  const synthText = (msg.content || [])
663
469
  .filter((b) => b?.type === 'text').map((b) => b.text || '').join(' ');
@@ -682,7 +488,6 @@ export function claudeParse(ev, s) {
682
488
  if (!toolId || s.seenClaudeToolIds.has(toolId))
683
489
  continue;
684
490
  const toolName = String(block?.name || 'Tool').trim() || 'Tool';
685
- // TodoWrite → update plan instead of adding activity noise (Claude Code 1.x)
686
491
  if (toolName === 'TodoWrite') {
687
492
  const plan = parseTodoWriteAsPlan(block?.input);
688
493
  if (plan)
@@ -691,11 +496,6 @@ export function claudeParse(ev, s) {
691
496
  s.claudeToolsById.set(toolId, { name: toolName, summary: 'Update plan' });
692
497
  continue;
693
498
  }
694
- // TaskCreate / TaskUpdate → 2.x plan tools. Same intent as TodoWrite, but
695
- // emitted one task at a time. Buffer TaskCreate inputs until the matching
696
- // tool_result arrives with the server-assigned id; apply TaskUpdate status
697
- // changes against the running map. Both rebuild s.plan so the dashboard /
698
- // IM plan card keeps surfacing total + current progress.
699
499
  if (toolName === 'TaskCreate') {
700
500
  const subject = typeof block?.input?.subject === 'string' ? block.input.subject.trim() : '';
701
501
  if (subject)
@@ -723,9 +523,6 @@ export function claudeParse(ev, s) {
723
523
  s.claudeToolsById.set(toolId, { name: toolName, summary: `Update task ${taskId || '?'} → ${rawStatus || 'unknown'}` });
724
524
  continue;
725
525
  }
726
- // Task → represents a sub-agent invocation. Carve it out as its own
727
- // streamed unit so the child's tool stream and model don't bleed into
728
- // the parent's activity card.
729
526
  if (toolName === 'Task' || toolName === 'Agent') {
730
527
  const input = block?.input || {};
731
528
  const subAgent = {
@@ -743,20 +540,9 @@ export function claudeParse(ev, s) {
743
540
  s.claudeToolsById.set(toolId, { name: toolName, summary: subAgent.description || 'Run task' });
744
541
  continue;
745
542
  }
746
- // Background Bash — same in-process lifecycle as a backgrounded agent:
747
- // launch ack now, <task-notification> later. Register so the TUI driver
748
- // holds the PTY open instead of SIGTERMing the command mid-flight.
749
543
  if (toolName === 'Bash' && block?.input?.run_in_background === true) {
750
544
  registerClaudeBackgroundBashLaunch(s, toolId);
751
545
  }
752
- // Workflow → multi-agent orchestration. ALWAYS backgrounded: the tool
753
- // returns immediately with a runId and the orchestration keeps running
754
- // *inside the claude process*, reporting completion via a later
755
- // `<task-notification>` — the same in-process lifecycle as a
756
- // run_in_background Task. Register it so decideClaudeTuiStop holds the PTY
757
- // open instead of SIGTERMing the in-flight workflow when the launch
758
- // segment's Stop fires (the「ultra 下 workflow 离线跑、TUI 误判结束退出把
759
- // workflow 打断」failure — the workflow analogue of the bg-Bash fix above).
760
546
  if (toolName === 'Workflow') {
761
547
  registerClaudeBackgroundAgentLaunch(s, toolId);
762
548
  }
@@ -779,10 +565,6 @@ export function claudeParse(ev, s) {
779
565
  if (t === 'user') {
780
566
  const msg = ev.message || {};
781
567
  const contents = Array.isArray(msg.content) ? msg.content : [];
782
- // Background-task completion notice. Claude Code injects these as user
783
- // events when a `run_in_background` task finishes (or dies); they are the
784
- // only completion signal backgrounded agents ever get — the Task tool's
785
- // own tool_result fired back at launch time as an ack.
786
568
  const notification = extractClaudeTaskNotification(msg.content);
787
569
  if (notification) {
788
570
  const eventAtMs = typeof ev.timestamp === 'string' ? Date.parse(ev.timestamp) : NaN;
@@ -791,10 +573,6 @@ export function claudeParse(ev, s) {
791
573
  const toolResults = contents.filter((b) => b?.type === 'tool_result');
792
574
  for (const block of toolResults) {
793
575
  const toolId = String(block?.tool_use_id || '').trim();
794
- // Dedup against tool_results already pushed by the TUI hook stream —
795
- // PreToolUse / PostToolUse arrive in real time, JSONL eventually
796
- // delivers the same events at end-of-turn and would otherwise re-push
797
- // each summary into activity / re-process TaskCreate's plan entry.
798
576
  if (toolId && s.seenClaudeToolResultIds?.has(toolId))
799
577
  continue;
800
578
  if (toolId) {
@@ -803,9 +581,6 @@ export function claudeParse(ev, s) {
803
581
  s.seenClaudeToolResultIds.add(toolId);
804
582
  }
805
583
  const tool = toolId ? s.claudeToolsById.get(toolId) : undefined;
806
- // Skip TodoWrite / TaskCreate / TaskUpdate results from activity — plan
807
- // card handles them. TaskCreate's tool_result carries the assigned task
808
- // id, which we splice into the running task list before skipping.
809
584
  if (tool?.name === 'TodoWrite')
810
585
  continue;
811
586
  if (tool?.name === 'TaskCreate') {
@@ -822,13 +597,6 @@ export function claudeParse(ev, s) {
822
597
  }
823
598
  if (tool?.name === 'TaskUpdate')
824
599
  continue;
825
- // Sub-agent tool_result closes out the sub-agent's lifecycle — flip its
826
- // status and skip the regular activity append (the sub-agent card carries
827
- // it). The result content text is the sub-agent's full response which
828
- // would otherwise leak into the parent activity feed.
829
- // Exception: a `run_in_background` launch returns its tool_result
830
- // immediately as a mere ack — the agent is still running. Its real
831
- // completion is the later <task-notification> (see the user branch).
832
600
  if (tool?.name === 'Task' || tool?.name === 'Agent') {
833
601
  const sub = s.subAgents.get(toolId);
834
602
  if (sub) {
@@ -844,18 +612,12 @@ export function claudeParse(ev, s) {
844
612
  tool.result = previewToolCallResult(block?.content);
845
613
  tool.status = block?.is_error ? 'failed' : 'done';
846
614
  }
847
- // Background Bash launch ack → map its task id to the tool_use so the
848
- // later <task-notification> (which usually omits <tool-use-id> for bash)
849
- // can resolve and decrement the pending count.
850
615
  if (tool?.name === 'Bash' && s.bgBashToolUseIds?.has(toolId)
851
616
  && !s.bgAgentCompletedToolUseIds?.has(toolId)) {
852
617
  const taskId = extractClaudeBackgroundTaskId(block?.content);
853
618
  if (taskId && !s.bgTaskIdToToolUse.has(taskId))
854
619
  s.bgTaskIdToToolUse.set(taskId, toolId);
855
620
  }
856
- // Workflow launch ack carries the runId (wf_…). Map it → tool_use so a
857
- // later <task-notification> that identifies the workflow only by task id
858
- // (no <tool-use-id>) still resolves and decrements the pending count.
859
621
  if (tool?.name === 'Workflow' && s.bgAgentLaunchedToolUseIds?.has(toolId)
860
622
  && !s.bgAgentCompletedToolUseIds?.has(toolId)) {
861
623
  const runId = extractClaudeWorkflowRunId(block?.content);
@@ -863,11 +625,6 @@ export function claudeParse(ev, s) {
863
625
  s.bgTaskIdToToolUse.set(runId, toolId);
864
626
  }
865
627
  pushRecentActivity(s.recentActivity, summarizeClaudeToolResult(tool, block, ev.tool_use_result));
866
- // MCP / Skill tool_result with multimodal content — recurse for image
867
- // entries so the final StreamResult carries them. Filesystem-reading
868
- // tools (Read) are skipped: their image content is a copy of an
869
- // existing file (often the user's own upload) and would otherwise be
870
- // re-rendered below the assistant text.
871
628
  if (Array.isArray(block.content) && !isClaudeFileReadingTool(tool?.name)) {
872
629
  accumulateClaudeImagesFromContent(block.content, s);
873
630
  }
@@ -881,16 +638,10 @@ export function claudeParse(ev, s) {
881
638
  s.errors = ev.errors;
882
639
  if (ev.result && !s.text.trim())
883
640
  s.text = ev.result;
884
- // A model-unavailable turn carries a normal stop_reason on its result event
885
- // (e.g. 'stop_sequence') that would otherwise clobber the 'model_error' the
886
- // synthetic handler set — preserve it so the failure stays diagnosable.
887
641
  if (s.stopReason !== 'model_error')
888
642
  s.stopReason = ev.stop_reason ?? s.stopReason;
889
643
  const u = ev.usage;
890
644
  if (u) {
891
- // Per-call semantics: the last message_start/message_delta snapshot is
892
- // authoritative. Only fall back to result.usage when nothing arrived via
893
- // stream_event (e.g. an early-exit error before any message_start).
894
645
  const cached = u.cache_read_input_tokens ?? u.cached_input_tokens;
895
646
  if (s.inputTokens == null && u.input_tokens != null)
896
647
  s.inputTokens = u.input_tokens;
@@ -906,9 +657,6 @@ export function claudeParse(ev, s) {
906
657
  const mu = ev.modelUsage;
907
658
  if (mu && typeof mu === 'object' && !s.byokContextWindow) {
908
659
  for (const info of Object.values(mu)) {
909
- // cc reports the *advertised* contextWindow on result.modelUsage; we
910
- // store the *effective* (post-reservation) window so the percent
911
- // matches cc's UM6 display formula.
912
660
  if (info?.contextWindow > 0) {
913
661
  s.contextWindow = claudeEffectiveContextWindow(info.contextWindow) ?? info.contextWindow;
914
662
  break;
@@ -918,11 +666,6 @@ export function claudeParse(ev, s) {
918
666
  }
919
667
  }
920
668
  export function createClaudeStreamState(opts) {
921
- // When BYOK is bound, the real context window (e.g. 1M for DeepSeek v4 Pro
922
- // via OpenRouter) comes from the provider's cached /models listing — cc
923
- // reports its own Claude-shaped fallback (200k) for unknown model ids, so
924
- // we lock in the real value upfront and refuse to overwrite it from cc's
925
- // event stream below.
926
669
  const byokWindow = opts.byokContextWindow && opts.byokContextWindow > 0
927
670
  ? opts.byokContextWindow
928
671
  : null;
@@ -940,16 +683,10 @@ export function createClaudeStreamState(opts) {
940
683
  outputTokens: null,
941
684
  cachedInputTokens: null,
942
685
  cacheCreationInputTokens: null,
943
- /** Output tokens from this turn's finished LLM calls — folded in when a
944
- * new call resets the per-call counter, so the turn total only climbs. */
945
686
  turnOutputTokensBase: 0,
946
- /** message.id of the LLM call whose usage `outputTokens` currently
947
- * reflects (TUI/JSONL mode, where there is no message_start marker). */
948
687
  turnUsageMsgId: null,
949
688
  contextWindow: byokWindow,
950
- /** When set, ignore cc-advertised contextWindow updates from the stream. */
951
689
  byokContextWindow: byokWindow,
952
- /** BYOK provider display name surfaced in preview meta + IM footers. */
953
690
  byokProviderName: byokProvider,
954
691
  contextUsedTokens: null,
955
692
  codexCumulative: null,
@@ -957,49 +694,19 @@ export function createClaudeStreamState(opts) {
957
694
  activity: '',
958
695
  recentActivity: [],
959
696
  plan: null,
960
- // Claude Code 2.x replaced the single `TodoWrite` plan tool with two
961
- // separate tools — `TaskCreate` (one task per call, server-assigned id)
962
- // and `TaskUpdate` (taskId + status). We maintain an ordered map and
963
- // rebuild s.plan whenever either fires so the dashboard / IM plan card
964
- // keeps showing total / current progress just like the TodoWrite era.
965
697
  claudeTaskList: new Map(),
966
698
  claudeTaskOrder: [],
967
- /** Pending TaskCreate tool_uses indexed by tool_use id — the input
968
- * carries the subject but Claude assigns the numeric task id only in
969
- * the matching tool_result, so we have to bridge the two halves. */
970
699
  pendingClaudeTaskCreates: new Map(),
971
700
  claudeToolsById: new Map(),
972
- /** Insertion order of expandable tool-call rows (parent activity tools
973
- * only — plan tools and sub-agent launches have their own cards). Feeds
974
- * `StreamPreviewMeta.toolCalls` via buildStreamPreviewMeta. */
975
701
  claudeToolCallOrder: [],
976
702
  seenClaudeToolIds: new Set(),
977
703
  subAgents: new Map(),
978
- /** Tool_use ids of Task/Agent launches with `run_in_background: true`.
979
- * Their immediate tool_result is only a launch ack — real completion
980
- * arrives later as a `<task-notification>` user event. The TUI driver
981
- * reads the launched/completed delta to keep the PTY alive until every
982
- * background agent has actually finished (they live inside the claude
983
- * process; killing it would destroy them mid-flight). */
984
704
  bgAgentLaunchedToolUseIds: new Set(),
985
- /** Subset of bgAgentLaunchedToolUseIds whose <task-notification> arrived. */
986
705
  bgAgentCompletedToolUseIds: new Set(),
987
- /** Background task id (the `agent-<id>` sidecar stem / `<task-id>` tag) →
988
- * parent tool_use id. Fallback matcher for notifications that omit the
989
- * `<tool-use-id>` tag. */
990
706
  bgTaskIdToToolUse: new Map(),
991
- /** Wall-clock ms of the most recent <task-notification> parsed this turn. */
992
707
  lastTaskNotificationAt: 0,
993
- // Image blocks accumulated during the turn (user-attached on the request
994
- // side, MCP / Skill tool_result on the response side). Surfaced in the
995
- // final StreamResult so IM channels can dispatch images at end-of-turn.
996
708
  imageBlocks: [],
997
- /** Stable dedupe keys (sha-ish data prefixes) for image blocks already
998
- * added to `imageBlocks`. Lets repeated stream_event / assistant deltas
999
- * for the same image not pile up duplicates. */
1000
709
  seenImageKeys: new Set(),
1001
- // Wired to opts.onSessionId so claudeParse can broadcast id changes the
1002
- // instant cc surfaces them (see emitSessionIdUpdate in agent/utils.ts).
1003
710
  _emitSessionId: opts.onSessionId ?? null,
1004
711
  };
1005
712
  }
@@ -1074,11 +781,6 @@ async function doClaudeInteractiveStream(opts) {
1074
781
  }
1075
782
  catch { }
1076
783
  };
1077
- // Simulated streaming smoothing — mirrors the TUI driver's TuiStreamBuffer.
1078
- // `claude -p` surfaces text via stream-json deltas that arrive in irregular
1079
- // SSE bursts; without metering, the dashboard/IM preview jumps "一卡一卡".
1080
- // Reveal `s.text` into the preview at a steady ~1000 chars/s so it reads as
1081
- // fluid typing. Display-only: the returned result still uses the full `s.text`.
1082
784
  const PRINT_STREAM_CHUNK_CHARS = 20;
1083
785
  const PRINT_STREAM_CHUNK_INTERVAL_MS = 20;
1084
786
  let displayedLen = 0;
@@ -1106,7 +808,6 @@ async function doClaudeInteractiveStream(opts) {
1106
808
  displayedLen = s.text.length;
1107
809
  rawEmit();
1108
810
  };
1109
- // New text → show what's revealed now, then keep the typewriter draining.
1110
811
  const emit = () => { rawEmit(); scheduleStreamTick(); };
1111
812
  const abortStream = () => {
1112
813
  if (interrupted || proc.killed)
@@ -1115,15 +816,6 @@ async function doClaudeInteractiveStream(opts) {
1115
816
  s.stopReason = 'interrupted';
1116
817
  closeInput();
1117
818
  agentWarn(`[abort] user interrupt, closing stdin for graceful shutdown pid=${proc.pid}`);
1118
- // Wait until the session JSONL has stopped being written before SIGTERM.
1119
- // Claude CLI streams events on stdout (which we observe in real time) BEFORE
1120
- // flushing the matching JSONL line — and its signal handler is just
1121
- // `process.exit()`, which doesn't drain the async write queue. SIGTERMing
1122
- // on a fixed window races that flush: the dashboard's live snapshot would
1123
- // show N tool calls, then once it reloads from disk the persisted turn
1124
- // regresses below N (e.g. 30 → 27). Poll the JSONL size and SIGTERM only
1125
- // after it's been stable for FILE_STABLE_MS, or after MAX_WAIT_MS as a
1126
- // hard cap (a still-active LLM stream would never go stable on its own).
1127
819
  const sessionFile = s.sessionId
1128
820
  ? path.join(getHome(), '.claude', 'projects', claudeProjectDirName(opts.workdir), `${s.sessionId}.jsonl`)
1129
821
  : null;
@@ -1141,7 +833,7 @@ async function doClaudeInteractiveStream(opts) {
1141
833
  try {
1142
834
  curSize = fs.statSync(sessionFile).size;
1143
835
  }
1144
- catch { /* file not yet created */ }
836
+ catch { }
1145
837
  }
1146
838
  if (curSize !== lastSize) {
1147
839
  lastSize = curSize;
@@ -1149,8 +841,6 @@ async function doClaudeInteractiveStream(opts) {
1149
841
  }
1150
842
  const stableFor = Date.now() - lastChangedAt;
1151
843
  const totalElapsed = Date.now() - startedAt;
1152
- // Without a JSONL path (very early abort, before sessionId is known) fall
1153
- // back to the legacy fixed grace window.
1154
844
  const shouldKill = !sessionFile
1155
845
  ? totalElapsed >= AGENT_GRACEFUL_ABORT_GRACE_MS
1156
846
  : (stableFor >= FILE_STABLE_MS || totalElapsed >= MAX_WAIT_MS);
@@ -1256,8 +946,6 @@ async function doClaudeInteractiveStream(opts) {
1256
946
  awaitingSteeredResponseStart = false;
1257
947
  steerQueued = false;
1258
948
  resetClaudeTurnState(s);
1259
- // New segment: restart the typewriter from empty so the next response
1260
- // reveals from its first character instead of jumping.
1261
949
  displayedLen = 0;
1262
950
  if (streamTickTimer) {
1263
951
  clearTimeout(streamTickTimer);
@@ -1288,7 +976,6 @@ async function doClaudeInteractiveStream(opts) {
1288
976
  else {
1289
977
  closeInput();
1290
978
  }
1291
- // Segment complete — reveal any buffered tail at once.
1292
979
  flushDisplay();
1293
980
  }
1294
981
  emit();
@@ -1328,14 +1015,7 @@ async function doClaudeInteractiveStream(opts) {
1328
1015
  s.text = s.msgs.join('\n\n');
1329
1016
  if (!s.thinking.trim() && s.thinkParts.length)
1330
1017
  s.thinking = s.thinkParts.join('\n\n');
1331
- // Reveal anything still buffered by the typewriter so the final live preview
1332
- // matches the returned message (smoothing can lag a fast final burst).
1333
1018
  flushDisplay();
1334
- // Catch the Claude CLI's synthetic "API Error: …" assistant body (transient
1335
- // Anthropic 5xx / 529 Overloaded). Without this rewrite the raw error string
1336
- // gets surfaced into the IM card as if it were Claude's reply, and the
1337
- // retry wrapper in `doClaudeStream` can't tell a transient failure apart
1338
- // from a real short reply.
1339
1019
  const apiErrorReason = detectClaudeApiError(s.text);
1340
1020
  if (apiErrorReason) {
1341
1021
  agentWarn(`[claude] upstream API error detected: ${apiErrorReason}`);
@@ -1374,10 +1054,6 @@ async function doClaudeInteractiveStream(opts) {
1374
1054
  cacheCreationInputTokens: s.cacheCreationInputTokens,
1375
1055
  contextWindow: s.contextWindow,
1376
1056
  contextUsedTokens: s.contextUsedTokens,
1377
- // Reuse the same calc as the live preview (computeContext) so the final
1378
- // footer % matches the running %. Previously this passed a fraction
1379
- // (used/window) into roundPercent, which expects a percent — divide-by-100
1380
- // bug that made the final read ~12% as ~0.1%.
1381
1057
  contextPercent: computeContext(s).contextPercent,
1382
1058
  codexCumulative: null,
1383
1059
  error,
@@ -1388,9 +1064,6 @@ async function doClaudeInteractiveStream(opts) {
1388
1064
  assistantBlocks: s.imageBlocks.length ? [...s.imageBlocks] : undefined,
1389
1065
  };
1390
1066
  }
1391
- // ---------------------------------------------------------------------------
1392
- // Stream
1393
- // ---------------------------------------------------------------------------
1394
1067
  export async function doClaudeStream(opts) {
1395
1068
  const result = opts.onSteerReady
1396
1069
  ? await doClaudeInteractiveStream(opts)
@@ -1401,15 +1074,8 @@ export async function doClaudeStream(opts) {
1401
1074
  }
1402
1075
  return result;
1403
1076
  }
1404
- // ---------------------------------------------------------------------------
1405
- // Sessions
1406
- // ---------------------------------------------------------------------------
1407
1077
  export const claudeProjectDirName = encodePathAsDirName;
1408
- /** Read native Claude Code sessions from ~/.claude/projects/{dirName}/*.jsonl */
1409
1078
  function extractClaudeTailQA(filePath) {
1410
- // Use a larger tail (1 MB) so we can reach past tool-result / assistant
1411
- // exchanges that follow the last real user question (which may be multi-MB
1412
- // due to embedded images).
1413
1079
  const lines = readTailLines(filePath, 1024 * 1024);
1414
1080
  let lastQuestion = null;
1415
1081
  let lastAnswer = null;
@@ -1434,21 +1100,13 @@ function extractClaudeTailQA(filePath) {
1434
1100
  }
1435
1101
  }
1436
1102
  }
1437
- catch { /* skip */ }
1103
+ catch { }
1438
1104
  }
1439
1105
  return { lastQuestion, lastAnswer, lastMessageText };
1440
1106
  }
1441
- // Per-file cache of the expensive content-derived fields. getNativeClaudeSessions
1442
- // runs per dashboard session-list request AND per workspace×agent in the overview
1443
- // fan-out; without this each call re-read + JSON-parsed every transcript in the
1444
- // project dir. Keyed by (mtime,size) so any append/edit re-reads just that file.
1445
- // `running`/`runState` are NOT cached — they depend on Date.now() - mtime, so they
1446
- // are recomputed per call below.
1447
1107
  const nativeClaudeContentCache = new Map();
1448
1108
  function readNativeClaudeContent(filePath, stat) {
1449
1109
  try {
1450
- // Read enough bytes to get past the system_prompt line (can be 20KB+) and
1451
- // reach the first user/assistant events for title and model extraction.
1452
1110
  const fd = fs.openSync(filePath, 'r');
1453
1111
  const buf = Buffer.alloc(65536);
1454
1112
  const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
@@ -1475,11 +1133,8 @@ function readNativeClaudeContent(filePath, stat) {
1475
1133
  if (title && model)
1476
1134
  break;
1477
1135
  }
1478
- catch { /* skip */ }
1136
+ catch { }
1479
1137
  }
1480
- // Fallback: if the first user message line is too large (e.g. contains
1481
- // base64 images) JSON.parse above will fail. Read a bigger chunk and
1482
- // regex-extract text blocks to find the actual user question.
1483
1138
  if (!title) {
1484
1139
  let scanStr = head;
1485
1140
  if (stat.size > 65536) {
@@ -1490,7 +1145,7 @@ function readNativeClaudeContent(filePath, stat) {
1490
1145
  fs.closeSync(fd2);
1491
1146
  scanStr = bigBuf.toString('utf8', 0, bigRead);
1492
1147
  }
1493
- catch { /* keep using head */ }
1148
+ catch { }
1494
1149
  }
1495
1150
  const re = /"type":"text","text":"((?:[^"\\]|\\.)*)"/g;
1496
1151
  let m;
@@ -1509,8 +1164,6 @@ function readNativeClaudeContent(filePath, stat) {
1509
1164
  break;
1510
1165
  }
1511
1166
  }
1512
- // Quick turn count: count real user messages (exclude tool_result and
1513
- // system-injected isMeta events — Skill outputs, resume prompts, etc.).
1514
1167
  let numTurns = 0;
1515
1168
  try {
1516
1169
  const raw = fs.readFileSync(filePath, 'utf8');
@@ -1523,7 +1176,7 @@ function readNativeClaudeContent(filePath, stat) {
1523
1176
  numTurns++;
1524
1177
  }
1525
1178
  }
1526
- catch { /* ignore count errors */ }
1179
+ catch { }
1527
1180
  const tailQA = extractClaudeTailQA(filePath);
1528
1181
  return {
1529
1182
  title,
@@ -1551,11 +1204,6 @@ function getNativeClaudeSessions(workdir, limit) {
1551
1204
  catch {
1552
1205
  return [];
1553
1206
  }
1554
- // Stat first (cheap), sort newest-first, then read bodies only for as many as
1555
- // can surface: `limit` is applied to a recency-sorted merge downstream, so a
1556
- // transcript older than the top-`limit` can never appear in that view. Unchanged
1557
- // files come straight from the per-file content cache, so repeated list calls
1558
- // (dashboard polls, workspace×agent overview fan-out) don't re-parse the dir.
1559
1207
  const files = [];
1560
1208
  for (const entry of entries) {
1561
1209
  if (!entry.isFile() || !entry.name.endsWith('.jsonl'))
@@ -1564,7 +1212,7 @@ function getNativeClaudeSessions(workdir, limit) {
1564
1212
  try {
1565
1213
  files.push({ sessionId: entry.name.slice(0, -6), filePath, stat: fs.statSync(filePath) });
1566
1214
  }
1567
- catch { /* skip */ }
1215
+ catch { }
1568
1216
  }
1569
1217
  files.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs);
1570
1218
  const selected = typeof limit === 'number' ? files.slice(0, Math.max(0, limit)) : files;
@@ -1611,11 +1259,6 @@ function getNativeClaudeSessions(workdir, limit) {
1611
1259
  }
1612
1260
  function getClaudeSessions(workdir, limit) {
1613
1261
  const resolvedWorkdir = path.resolve(workdir);
1614
- // Merge pikiloom-tracked sessions with native Claude sessions
1615
- // Canonical record→SessionInfo mapper (single source of truth). Hand-rolling
1616
- // this projection silently dropped pikiloom-owned metadata (thinkingEffort,
1617
- // workflowEnabled, profileId), so the merged list lost the user's per-session
1618
- // effort / Workflow / BYOK choices. Centralizing it keeps every field in sync.
1619
1262
  const pikiloomSessions = listPikiloomSessions(resolvedWorkdir, 'claude').map(managedRecordToSessionInfo);
1620
1263
  const nativeSessions = getNativeClaudeSessions(resolvedWorkdir, limit);
1621
1264
  const merged = mergeManagedAndNativeSessions(pikiloomSessions, nativeSessions);
@@ -1625,9 +1268,6 @@ function getClaudeSessions(workdir, limit) {
1625
1268
  `pikiloom=${pikiloomSessions.length} native=${nativeSessions.length} merged=${sessions.length}`);
1626
1269
  return { ok: true, sessions, error: null };
1627
1270
  }
1628
- // ---------------------------------------------------------------------------
1629
- // Session tail
1630
- // ---------------------------------------------------------------------------
1631
1271
  function extractClaudeText(content, skipSystemBlocks = false) {
1632
1272
  if (typeof content === 'string')
1633
1273
  return content;
@@ -1641,7 +1281,6 @@ function extractClaudeText(content, skipSystemBlocks = false) {
1641
1281
  parts.push(block.text);
1642
1282
  }
1643
1283
  }
1644
- // Join with blank line so consecutive text blocks render as separate markdown paragraphs.
1645
1284
  return parts.join('\n\n');
1646
1285
  }
1647
1286
  function getClaudeSessionTail(opts) {
@@ -1670,7 +1309,7 @@ function getClaudeSessionTail(opts) {
1670
1309
  allMsgs.push({ role: 'assistant', text });
1671
1310
  }
1672
1311
  }
1673
- catch { /* skip */ }
1312
+ catch { }
1674
1313
  }
1675
1314
  return { ok: true, messages: allMsgs.slice(-limit), error: null };
1676
1315
  }
@@ -1678,35 +1317,17 @@ function getClaudeSessionTail(opts) {
1678
1317
  return { ok: false, messages: [], error: e.message };
1679
1318
  }
1680
1319
  }
1681
- // ---------------------------------------------------------------------------
1682
- // Session messages (full content)
1683
- // ---------------------------------------------------------------------------
1684
- /** Build an image MessageBlock from a Claude content entry of the shape
1685
- * `{ type: 'image', source: { type: 'base64', media_type, data } }`. Returns
1686
- * null when the source is missing or the encoded size exceeds the cap. */
1687
1320
  function claudeImageBlockFromEntry(entry) {
1688
1321
  if (!entry || entry.type !== 'image' || !entry.source)
1689
1322
  return null;
1690
1323
  const source = entry.source;
1691
1324
  if (source.type !== 'base64' || typeof source.data !== 'string')
1692
1325
  return null;
1693
- // 12MB base64 ≈ 9MB binary — keep API payloads sane.
1694
1326
  if (source.data.length > 12 * 1024 * 1024)
1695
1327
  return null;
1696
1328
  const mime = (source.media_type || 'image/png').toLowerCase();
1697
1329
  return { type: 'image', content: `data:${mime};base64,${source.data}`, imageMime: mime };
1698
1330
  }
1699
- /** Extract structured content blocks from Claude message content.
1700
- * When `todoWriteToolIds` is provided, TodoWrite tool_use blocks are emitted
1701
- * as `plan` blocks and their IDs are tracked so tool_results can be skipped.
1702
- *
1703
- * When a `tool_result` block carries multimodal content (`content: [{type:'image',...}, ...]`),
1704
- * the inner image entries are emitted as siblings of the textual tool_result —
1705
- * this is the path MCP image-returning tools (mermaid-mcp, chart, dalle-mcp, …)
1706
- * travel through. Filesystem-reading tools (Claude's `Read`) are excluded
1707
- * via `toolNamesByUseId`: their image content is just an echo of an existing
1708
- * file (often the user's own attachment) and re-rendering it under the
1709
- * assistant card produces a confusing duplicate. */
1710
1331
  function extractClaudeBlocks(content, skipSystemBlocks = false, todoWriteToolIds, toolNamesByUseId) {
1711
1332
  if (typeof content === 'string')
1712
1333
  return [{ type: 'text', content }];
@@ -1725,7 +1346,6 @@ function extractClaudeBlocks(content, skipSystemBlocks = false, todoWriteToolIds
1725
1346
  blocks.push({ type: 'thinking', content: block.thinking });
1726
1347
  }
1727
1348
  else if (block.type === 'tool_use') {
1728
- // TodoWrite → emit as plan block instead of generic tool_use
1729
1349
  if (block.name === 'TodoWrite' && todoWriteToolIds) {
1730
1350
  const plan = parseTodoWriteAsPlan(block.input);
1731
1351
  if (plan) {
@@ -1744,10 +1364,6 @@ function extractClaudeBlocks(content, skipSystemBlocks = false, todoWriteToolIds
1744
1364
  ? block.content.filter((c) => c?.type === 'text').map((c) => c.text).join('\n')
1745
1365
  : '';
1746
1366
  blocks.push({ type: 'tool_result', content: resultText, toolId: block.tool_use_id });
1747
- // Recurse into multimodal tool_result content so MCP-returned images
1748
- // (and any future non-text inner types) surface alongside the textual
1749
- // tool_result rather than being silently dropped. Skip file-reading
1750
- // tools — their image content is an echo, not a new asset.
1751
1367
  const toolName = block.tool_use_id ? toolNamesByUseId?.get(block.tool_use_id) : undefined;
1752
1368
  if (Array.isArray(block.content) && !isClaudeFileReadingTool(toolName)) {
1753
1369
  for (const inner of block.content) {
@@ -1765,12 +1381,6 @@ function extractClaudeBlocks(content, skipSystemBlocks = false, todoWriteToolIds
1765
1381
  }
1766
1382
  return blocks;
1767
1383
  }
1768
- /**
1769
- * Top-level XML wrapper tags that Claude Code injects into "user" events for
1770
- * non-user-authored content: background-task results, system reminders, IDE
1771
- * state, persisted output truncations, slash-command stdout, etc. The dashboard
1772
- * should never render these as a user bubble — they're conversation infra.
1773
- */
1774
1384
  const SYSTEM_INJECTED_USER_TAGS = new Set([
1775
1385
  'task-notification',
1776
1386
  'system-reminder',
@@ -1787,41 +1397,21 @@ const SYSTEM_INJECTED_USER_TAGS = new Set([
1787
1397
  'tool-use-id',
1788
1398
  'output-file',
1789
1399
  ]);
1790
- /**
1791
- * Detect Claude CLI's boilerplate `<synthetic>` responses that surface only
1792
- * because of TUI-resume bookkeeping — they carry no information for the user.
1793
- *
1794
- * The reproducible case: every `claude --resume <id>` in interactive mode
1795
- * writes a sentinel turn into the JSONL on startup, consisting of an
1796
- * `isMeta:true` user "Continue from where you left off." plus a `<synthetic>`
1797
- * "No response requested." acknowledgment. The print-mode driver doesn't hit
1798
- * this because `-p` skips the interactive resume nudge. Filtering it out here
1799
- * keeps the dashboard timeline clean across all driver paths.
1800
- */
1801
1400
  function isClaudeSyntheticResumeNoise(text) {
1802
1401
  const t = (text || '').trim().toLowerCase();
1803
1402
  if (!t)
1804
1403
  return true;
1805
1404
  return t === 'no response requested.' || t === 'no response requested';
1806
1405
  }
1807
- /** Detect system-injected user events (compression summaries, interruption
1808
- * markers, task-notifications, IDE state, etc.) that should not render as a
1809
- * user message when parsing session JSONL. */
1810
1406
  function isSystemInjectedUserEvent(text) {
1811
1407
  const trimmed = (text || '').trim();
1812
1408
  if (!trimmed)
1813
1409
  return true;
1814
- // Interruption markers injected by Claude Code
1815
1410
  if (/^\[Request interrupted by user(?: for tool use)?\]$/i.test(trimmed))
1816
1411
  return true;
1817
- // Leading XML wrapper from a known infra tag — these are never user-authored.
1818
1412
  const leading = trimmed.match(/^<([a-z][a-z0-9_-]*)\b/i);
1819
1413
  if (leading && SYSTEM_INJECTED_USER_TAGS.has(leading[1].toLowerCase()))
1820
1414
  return true;
1821
- // Context compression summaries carry a recognizable opening marker. Detect
1822
- // by that marker, not length — long, legitimate user prompts (multi-paragraph
1823
- // briefs, pasted documents) routinely exceed any size threshold and must
1824
- // render as real user turns.
1825
1415
  const lower = trimmed.toLowerCase();
1826
1416
  const markers = ['continued from a previous', 'summary below covers', 'earlier portion of the conversation', 'here is a summary of', 'conversation summary'];
1827
1417
  return markers.some(m => lower.includes(m));
@@ -1835,39 +1425,16 @@ function getClaudeSessionMessages(opts) {
1835
1425
  try {
1836
1426
  const content = fs.readFileSync(filePath, 'utf-8');
1837
1427
  const lines = content.split('\n').filter(l => l.trim());
1838
- // Parse raw events, merging consecutive same-role events into one message.
1839
- // Claude JSONL writes one event per content block (thinking, text, tool_use, tool_result).
1840
- // Consecutive assistant events form a single assistant message.
1841
- // User events with tool_result are system-injected and should be hidden;
1842
- // only user events with actual user text start a new user message.
1843
1428
  const allMsgs = [];
1844
1429
  const richMsgs = [];
1845
1430
  let pendingRole = null;
1846
1431
  let pendingTextParts = [];
1847
1432
  let pendingBlocks = [];
1848
- /** Latest assistant-event usage snapshot — overwritten on each LLM call within a
1849
- * turn so the flushed RichMessage carries the final call's context state, matching
1850
- * the live `StreamPreviewMeta` semantics. */
1851
1433
  let pendingUsage = null;
1852
- /** Per-call output tokens keyed by message.id — events of one call share an
1853
- * id and carry running totals, so last-write-wins per id and the turn's
1854
- * cumulative output is the sum across ids (→ `turnOutputTokens`). */
1855
1434
  let pendingCallOutputs = new Map();
1856
1435
  const todoWriteToolIds = new Set();
1857
- /**
1858
- * Sub-agent blocks live in `pendingBlocks` like any other block but we keep
1859
- * a side-table of references keyed by the Task tool_use_id so subsequent
1860
- * sub-agent assistant events (which carry parent_tool_use_id) can mutate
1861
- * the captured `subAgent` payload in place — that way the rendered turn
1862
- * shows the sub-agent's full tool stream without polluting the parent's
1863
- * tool list.
1864
- */
1865
1436
  const subAgentBlocksById = new Map();
1866
- /** Tool ids belonging to sub-agents — their tool_results in user events are skipped from the parent activity. */
1867
1437
  const subAgentToolIds = new Set();
1868
- /** Tool name keyed by tool_use_id — populated from assistant tool_use
1869
- * events so the user-event tool_result loop can filter image content for
1870
- * filesystem-reading tools (Read). */
1871
1438
  const toolNamesByUseId = new Map();
1872
1439
  const flush = () => {
1873
1440
  if (!pendingRole)
@@ -1899,8 +1466,6 @@ function getClaudeSessionMessages(opts) {
1899
1466
  const ev = JSON.parse(raw);
1900
1467
  const parentToolUseId = (typeof ev.parent_tool_use_id === 'string' && ev.parent_tool_use_id) ? ev.parent_tool_use_id : null;
1901
1468
  if (parentToolUseId) {
1902
- // Sub-agent emission — fold tool calls into the matching sub_agent block
1903
- // and never let them surface as siblings of the parent's blocks.
1904
1469
  const block = subAgentBlocksById.get(parentToolUseId);
1905
1470
  if (!block?.subAgent)
1906
1471
  continue;
@@ -1927,15 +1492,6 @@ function getClaudeSessionMessages(opts) {
1927
1492
  continue;
1928
1493
  }
1929
1494
  if (ev.type === 'user') {
1930
- // System-injected meta events (Skill tool results, /command stdout,
1931
- // resume prompts) come through with `type:user` + `isMeta:true`. The
1932
- // accompanying `message.content` is plain text (NOT a tool_result
1933
- // block), so the regular `isToolResult` check below misses them and
1934
- // they would otherwise render as a fake user bubble — splitting the
1935
- // conversation into a phantom new turn (visible as a fresh
1936
- // "Claude Code" divider mid-session). Re-attach the text to the
1937
- // originating tool's activity feed when sourceToolUseID is known,
1938
- // otherwise drop silently.
1939
1495
  if (ev.isMeta === true) {
1940
1496
  if (pendingRole === 'assistant') {
1941
1497
  const toolUseId = typeof ev.sourceToolUseID === 'string' ? ev.sourceToolUseID : '';
@@ -1958,14 +1514,8 @@ function getClaudeSessionMessages(opts) {
1958
1514
  const toolUseId = block.tool_use_id;
1959
1515
  if (todoWriteToolIds.has(toolUseId))
1960
1516
  continue;
1961
- // Sub-agent inner tool results — already accounted for by the sub_agent block.
1962
1517
  if (subAgentToolIds.has(toolUseId))
1963
1518
  continue;
1964
- // Top-level tool_result for a sub-agent (Task / Agent) — close
1965
- // out its lifecycle. The result content text is the sub-agent's
1966
- // full final answer; surface it on the sub_agent block so the
1967
- // dedicated card can render it instead of leaking into the
1968
- // parent's tool_result feed.
1969
1519
  const subBlock = subAgentBlocksById.get(toolUseId);
1970
1520
  if (subBlock?.subAgent) {
1971
1521
  subBlock.subAgent.status = block?.is_error ? 'failed' : 'done';
@@ -1986,12 +1536,6 @@ function getClaudeSessionMessages(opts) {
1986
1536
  if (resultText) {
1987
1537
  pendingBlocks.push({ type: 'tool_result', content: resultText, toolId: toolUseId });
1988
1538
  }
1989
- // Multimodal tool_result content from MCP servers can include
1990
- // image entries — surface them as siblings of the textual
1991
- // tool_result so the rendered turn carries the image. Skip
1992
- // filesystem-reading tools (Read): their image content is an
1993
- // echo of an existing file (e.g. the user's own attachment)
1994
- // and would otherwise be duplicated below the assistant text.
1995
1539
  const toolName = toolNamesByUseId.get(toolUseId);
1996
1540
  if (Array.isArray(block.content) && !isClaudeFileReadingTool(toolName)) {
1997
1541
  for (const inner of block.content) {
@@ -2013,19 +1557,6 @@ function getClaudeSessionMessages(opts) {
2013
1557
  const rawText = stripInjectedPrompts(extractClaudeText(ev.message?.content, true));
2014
1558
  const userBlocks = extractClaudeBlocks(ev.message?.content, true);
2015
1559
  const imageBlocks = userBlocks.filter(b => b.type === 'image');
2016
- // TUI mode (claude-tui.ts) persists user `content` as a plain string
2017
- // with leading `@/abs/path/image.png` mentions — that's how the TUI
2018
- // ingests local images (it can't accept stream-json image blocks like
2019
- // `-p` mode). The `extractClaudeBlocks` call above yields no image
2020
- // blocks for that shape; lift the mentions into structured image
2021
- // blocks via the shared pipeline so the dashboard renders thumbnails
2022
- // instead of raw paths. Also resolves the "first message drops the
2023
- // image" + "queued message drops the image" symptoms because the
2024
- // optimisticBridgesImages bridge (SessionPanel) was previously
2025
- // falling open: the server-side user text contained the @-path while
2026
- // the optimistic pendingPrompt did not, so the bridge's text-equality
2027
- // check failed and the no-image server bubble replaced the
2028
- // optimistic one.
2029
1560
  let displayText = rawText;
2030
1561
  if (typeof ev.message?.content === 'string' && imageBlocks.length === 0) {
2031
1562
  const recoveredPaths = new Set();
@@ -2036,9 +1567,6 @@ function getClaudeSessionMessages(opts) {
2036
1567
  imageBlocks.push(block);
2037
1568
  recoveredPaths.add(absPath);
2038
1569
  }
2039
- // Only strip the mentions we successfully turned into image blocks;
2040
- // leave unresolved ones (file deleted/moved) in the text so the
2041
- // user sees what was attached even when we can't render it.
2042
1570
  if (recoveredPaths.size) {
2043
1571
  displayText = rawText.replace(CLAUDE_AT_MENTION_IMAGE_RE, (full, leading, p) => recoveredPaths.has(p) ? (leading || '') : full);
2044
1572
  }
@@ -2051,21 +1579,8 @@ function getClaudeSessionMessages(opts) {
2051
1579
  }
2052
1580
  }
2053
1581
  else if (ev.type === 'assistant') {
2054
- // `model:"<synthetic>"` is Claude CLI's out-of-band channel — emitted
2055
- // when the runtime needs to tell the user something *as if* the
2056
- // assistant spoke ("No response requested.", "There's an issue with
2057
- // the selected model…", etc.). Surface it as a `system_notice` block
2058
- // rather than a real text reply: the content stays visible (so model
2059
- // errors and other meaningful feedback aren't lost), but it renders
2060
- // as a notice tile instead of impersonating a Claude turn.
2061
1582
  if (ev.message?.model === '<synthetic>') {
2062
1583
  const noticeText = extractClaudeText(ev.message?.content, true).trim();
2063
- // Suppress TUI-resume startup noise. When `claude --resume <id>`
2064
- // boots in interactive mode it injects a sentinel turn — an
2065
- // `isMeta:true` user "Continue from where you left off." followed
2066
- // by a `<synthetic>` "No response requested." acknowledgment.
2067
- // This is harmless internal book-keeping; rendering it as a
2068
- // yellow notice on every TUI-mode turn just pollutes the UI.
2069
1584
  if (isClaudeSyntheticResumeNoise(noticeText))
2070
1585
  continue;
2071
1586
  if (pendingRole === 'user')
@@ -2089,8 +1604,6 @@ function getClaudeSessionMessages(opts) {
2089
1604
  cacheCreation: numOrNull(u.cache_creation_input_tokens),
2090
1605
  model: typeof ev.message?.model === 'string' ? ev.message.model : prevModel,
2091
1606
  };
2092
- // Per-call output for the turn-cumulative counter. Same-id events
2093
- // carry running totals → keep the last value per call.
2094
1607
  const output = numOrNull(u.output_tokens);
2095
1608
  if (output != null) {
2096
1609
  const msgId = typeof ev.message?.id === 'string' && ev.message.id ? ev.message.id : '(no-id)';
@@ -2100,9 +1613,6 @@ function getClaudeSessionMessages(opts) {
2100
1613
  const text = extractClaudeText(ev.message?.content, true);
2101
1614
  if (text)
2102
1615
  pendingTextParts.push(text);
2103
- // Record tool names from this assistant turn before extracting blocks
2104
- // so any tool_result that follows in a later user event can be
2105
- // attributed to the right tool (Read → skip image recursion, etc.).
2106
1616
  const assistantContents = Array.isArray(ev.message?.content) ? ev.message.content : [];
2107
1617
  for (const inner of assistantContents) {
2108
1618
  if (inner?.type === 'tool_use' && typeof inner.id === 'string' && typeof inner.name === 'string') {
@@ -2110,10 +1620,6 @@ function getClaudeSessionMessages(opts) {
2110
1620
  }
2111
1621
  }
2112
1622
  const blocks = extractClaudeBlocks(ev.message?.content, true, todoWriteToolIds, toolNamesByUseId);
2113
- // Convert sub-agent tool_use blocks into sub_agent placeholders we
2114
- // can later mutate. Claude Code surfaces the Task tool as `Agent` in
2115
- // its v2 stream format; accept both names so older sessions still
2116
- // parse correctly.
2117
1623
  const contents = Array.isArray(ev.message?.content) ? ev.message.content : [];
2118
1624
  for (let i = 0; i < blocks.length; i++) {
2119
1625
  const block = blocks[i];
@@ -2139,15 +1645,9 @@ function getClaudeSessionMessages(opts) {
2139
1645
  pendingBlocks.push(...blocks);
2140
1646
  }
2141
1647
  }
2142
- catch { /* skip malformed lines */ }
1648
+ catch { }
2143
1649
  }
2144
1650
  flush();
2145
- // Hydrate sub_agent blocks from sidecar files. Claude Code stores each
2146
- // sub-agent's full transcript in
2147
- // ~/.claude/projects/<dir>/<session-id>/subagents/agent-<id>.jsonl
2148
- // alongside an `agent-<id>.meta.json` carrying the agentType. The parent
2149
- // session only records the Agent tool_use + tool_result; without this step
2150
- // the sub-agent card has no tool list.
2151
1651
  const subAgentsDir = path.join(projectDir, opts.sessionId, 'subagents');
2152
1652
  if (fs.existsSync(subAgentsDir))
2153
1653
  hydrateSubAgentBlocksFromSidecar(richMsgs, subAgentsDir);
@@ -2157,12 +1657,6 @@ function getClaudeSessionMessages(opts) {
2157
1657
  return { ok: false, messages: [], totalTurns: 0, error: e.message };
2158
1658
  }
2159
1659
  }
2160
- /**
2161
- * Walk a session's `subagents/` directory and merge each sidecar's tool stream
2162
- * onto the matching `sub_agent` block (matched by description, the only stable
2163
- * shared field between parent and child sessions). Best-effort — silent on any
2164
- * I/O or parse failure.
2165
- */
2166
1660
  function hydrateSubAgentBlocksFromSidecar(richMsgs, subAgentsDir) {
2167
1661
  let entries;
2168
1662
  try {
@@ -2184,7 +1678,7 @@ function hydrateSubAgentBlocksFromSidecar(richMsgs, subAgentsDir) {
2184
1678
  metaKind = typeof meta?.agentType === 'string' ? meta.agentType : null;
2185
1679
  metaDescription = typeof meta?.description === 'string' ? meta.description : null;
2186
1680
  }
2187
- catch { /* meta optional */ }
1681
+ catch { }
2188
1682
  const tools = [];
2189
1683
  let model = null;
2190
1684
  try {
@@ -2242,31 +1736,15 @@ function hydrateSubAgentBlocksFromSidecar(richMsgs, subAgentsDir) {
2242
1736
  }
2243
1737
  }
2244
1738
  }
2245
- // ---------------------------------------------------------------------------
2246
- // Models
2247
- // ---------------------------------------------------------------------------
2248
1739
  const CLAUDE_MODELS = [
2249
1740
  { id: 'claude-fable-5', alias: 'fable' },
2250
1741
  { id: 'claude-opus-4-8', alias: 'opus' },
2251
1742
  { id: 'claude-sonnet-4-6', alias: 'sonnet' },
2252
1743
  { id: 'claude-haiku-4-5-20251001', alias: 'haiku' },
2253
1744
  ];
2254
- // ---------------------------------------------------------------------------
2255
- // Usage
2256
- // ---------------------------------------------------------------------------
2257
- // The account-usage query below hits api.anthropic.com/api/oauth/usage, which is
2258
- // itself rate-limited. The dashboard rebuilds agent status ~every 30s (plus a
2259
- // forced refresh on usage-ring hover), and querying that often trips the
2260
- // endpoint's 429 — which (since we treat a query error as "unknown") blanks the
2261
- // header usage ring entirely. Quota windows (5h/7d) move slowly, so we query at
2262
- // most once per this interval and serve the last good result in between
2263
- // (including across transient 429s), decoupling usage cadence from how often
2264
- // agent status is rebuilt.
2265
1745
  const CLAUDE_USAGE_QUERY_TTL_MS = 5 * 60_000;
2266
1746
  const claudeUsageCache = { lastGood: null, lastAttemptAt: 0 };
2267
1747
  function getClaudeOAuthToken() {
2268
- // `security` is macOS-only; other platforms store Claude creds differently
2269
- // (DPAPI on Windows, libsecret on Linux) and Claude Code manages those itself.
2270
1748
  if (!IS_MAC)
2271
1749
  return null;
2272
1750
  try {
@@ -2294,10 +1772,6 @@ function getClaudeUsageFromOAuth() {
2294
1772
  const capturedAt = new Date().toISOString();
2295
1773
  const apiError = data?.error;
2296
1774
  if (apiError && typeof apiError === 'object') {
2297
- // The usage query endpoint itself returned an error (e.g. 429 rate
2298
- // limit on the query API). This does NOT reflect the user's actual
2299
- // Claude usage status, so fall through to telemetry instead of
2300
- // reporting a misleading "limit_reached".
2301
1775
  return null;
2302
1776
  }
2303
1777
  const makeWindow = (label, entry) => {
@@ -2401,7 +1875,6 @@ function getClaudeUsageFromTelemetry(home, model) {
2401
1875
  const status = normalizeUsageStatus(chosen.status);
2402
1876
  const resetAfterSeconds = chosen.hoursTillReset == null ? null : Math.max(0, Math.round(chosen.hoursTillReset * 3600));
2403
1877
  const resetAt = resetAfterSeconds == null ? null : new Date(chosen.capturedAtMs + resetAfterSeconds * 1000).toISOString();
2404
- // Build a locale-neutral label from capture age (e.g. "3h ago", "2d ago")
2405
1878
  const ageMs = Date.now() - chosen.capturedAtMs;
2406
1879
  const ageMins = Math.round(ageMs / 60_000);
2407
1880
  const ageLabel = ageMins < 1 ? '<1m ago' : ageMins < 60 ? `${ageMins}m ago` : ageMins < 1440 ? `${Math.round(ageMins / 60)}h ago` : `${Math.round(ageMins / 1440)}d ago`;
@@ -2414,23 +1887,15 @@ function claudeSessionTranscriptPath(workdir, sessionId) {
2414
1887
  return '';
2415
1888
  return path.join(home, '.claude', 'projects', encodePathAsDirName(workdir), `${sessionId}.jsonl`);
2416
1889
  }
2417
- /**
2418
- * Scan a claude session transcript for the latest native /goal state. Returns
2419
- * null when no `goal_status` attachment is present.
2420
- */
2421
1890
  export function getClaudeNativeGoal(workdir, sessionId) {
2422
1891
  const file = claudeSessionTranscriptPath(workdir, sessionId);
2423
1892
  if (!file || !fs.existsSync(file))
2424
1893
  return null;
2425
- // Goal status lines are tiny attachments. Walk the tail (1 MB) to find the
2426
- // last one — tail covers all realistic session sizes without parsing every
2427
- // line of a long transcript.
2428
1894
  const lines = readTailLines(file, 1024 * 1024);
2429
1895
  let latest = null;
2430
1896
  for (const raw of lines) {
2431
1897
  if (!raw || raw[0] !== '{')
2432
1898
  continue;
2433
- // Cheap pre-filter so we only JSON.parse the relevant subset.
2434
1899
  if (!raw.includes('"goal_status"'))
2435
1900
  continue;
2436
1901
  try {
@@ -2448,65 +1913,27 @@ export function getClaudeNativeGoal(workdir, sessionId) {
2448
1913
  updatedAtMs: Number.isFinite(ts) ? ts : Date.now(),
2449
1914
  };
2450
1915
  }
2451
- catch { /* skip */ }
1916
+ catch { }
2452
1917
  }
2453
- // After auto-clear (met:true) claude still leaves the goal_status line in the
2454
- // transcript; pikiloom treats "no active goal" as null so the bridge mirrors
2455
- // the codex semantics where `goal_get` returns null after a clear.
2456
1918
  if (latest && latest.met)
2457
1919
  return null;
2458
1920
  return latest;
2459
1921
  }
2460
- /** Build the user-prompt that triggers claude's native `/goal <condition>` slash command. */
2461
1922
  export function buildClaudeSetGoalPrompt(objective) {
2462
1923
  return `/goal ${objective.trim()}`;
2463
1924
  }
2464
- /** Build the user-prompt that triggers claude's native `/goal clear` slash command. */
2465
1925
  export function buildClaudeClearGoalPrompt() {
2466
1926
  return '/goal clear';
2467
1927
  }
2468
- // ---------------------------------------------------------------------------
2469
- // Driver
2470
- // ---------------------------------------------------------------------------
2471
- /**
2472
- * Claude turns default to the real interactive TUI under PTY — usage stays
2473
- * inside the Pro/Max subscription quota. `claude -p` calls (headless / print
2474
- * mode) bill against the separate Agent SDK credit pool that Anthropic split
2475
- * out on 2026-06-15, so we keep that off the hot path.
2476
- *
2477
- * Opt out to the legacy print path with `PIKILOOM_CLAUDE_PRINT=1` (also
2478
- * accepts `=true` / `=yes` / `=on`). For backwards compat the older
2479
- * `PIKILOOM_CLAUDE_TUI=0` / `=false` / `=no` / `=off` is honoured too.
2480
- *
2481
- * When TUI startup fails (node-pty missing, prebuilt helper unusable, PTY
2482
- * allocation refused in a sandbox, …) the dispatcher automatically falls
2483
- * through to the print-mode driver so pikiloom still works — at the cost of
2484
- * the calls landing on the Agent SDK credit pool. The fallback is logged so
2485
- * users can investigate.
2486
- */
2487
1928
  export function isClaudePrintModeForced() {
2488
1929
  const print = (process.env.PIKILOOM_CLAUDE_PRINT ?? '').trim().toLowerCase();
2489
1930
  if (print === '1' || print === 'true' || print === 'yes' || print === 'on')
2490
1931
  return true;
2491
- // Legacy env var: PIKILOOM_CLAUDE_TUI=0 (or false/no/off) explicitly opts
2492
- // back to print mode. Truthy values are now the default behaviour and a
2493
- // no-op.
2494
1932
  const tui = (process.env.PIKILOOM_CLAUDE_TUI ?? '').trim().toLowerCase();
2495
1933
  if (tui === '0' || tui === 'false' || tui === 'no' || tui === 'off')
2496
1934
  return true;
2497
1935
  return false;
2498
1936
  }
2499
- /**
2500
- * Single-attempt dispatch: print mode when the resolved access mode is 'api'
2501
- * (or, when no explicit mode was threaded, when forced via env), otherwise TUI
2502
- * mode with print-mode fallback if TUI prerequisites are missing (node-pty
2503
- * absent, PTY allocation refused, …).
2504
- *
2505
- * `opts.claudeAccessMode` is authoritative when set — the bot resolves it from
2506
- * the per-agent config so a live dashboard toggle takes effect on the next
2507
- * spawned turn. Env (isClaudePrintModeForced) is only the fallback for callers
2508
- * that don't thread a mode (the `pikiloom run` one-shot path).
2509
- */
2510
1937
  async function doClaudeStreamOnce(opts) {
2511
1938
  const printMode = opts.claudeAccessMode
2512
1939
  ? opts.claudeAccessMode === 'api'
@@ -2520,19 +1947,10 @@ async function doClaudeStreamOnce(opts) {
2520
1947
  return await mod.doClaudeTuiStream(opts);
2521
1948
  }
2522
1949
  catch (err) {
2523
- // TUI prerequisite failed (node-pty missing, PTY allocation refused, etc.).
2524
- // Fall back to print mode so pikiloom stays functional — with the caveat
2525
- // that this turn lands on the Agent SDK credit pool.
2526
1950
  agentWarn(`[claude] TUI unavailable (${err?.message || err}); falling back to -p — this turn bills the Agent SDK credit pool`);
2527
1951
  return doClaudeStream(opts);
2528
1952
  }
2529
1953
  }
2530
- /**
2531
- * Backoff schedule (in ms) for retrying transient Anthropic upstream failures
2532
- * — 529 Overloaded, 5xx, gateway timeouts. Total wait budget ~30s before we
2533
- * surface the failure to the user. Non-retryable errors (auth, quota,
2534
- * context-length) skip the loop and fail fast.
2535
- */
2536
1954
  const CLAUDE_API_RETRY_BACKOFFS_MS = [4000, 12000];
2537
1955
  function makeOverloadFriendlyResult(result, reason, attempts) {
2538
1956
  const wait = CLAUDE_API_RETRY_BACKOFFS_MS.slice(0, attempts).reduce((sum, ms) => sum + ms, 0);
@@ -2551,30 +1969,11 @@ function makeOverloadFriendlyResult(result, reason, attempts) {
2551
1969
  error: `Anthropic API error: ${reason}`,
2552
1970
  };
2553
1971
  }
2554
- /**
2555
- * Driver-entry wrapper. Detects the Claude CLI's synthetic "API Error: …"
2556
- * assistant turn and re-issues the request with backoff for retryable upstream
2557
- * conditions (Overloaded, 5xx, timeouts). Non-retryable failures surface
2558
- * immediately. After the budget is exhausted, the final result carries a
2559
- * friendly human-readable explanation in `message` so the IM card doesn't
2560
- * dump raw "API Error: Overloaded" text on the user.
2561
- */
2562
- /**
2563
- * Continuation prompt for stall recovery. The frozen process already accepted
2564
- * and partially executed the user's prompt (it sits in the transcript), so the
2565
- * resumed process must NOT receive the original prompt again — it gets an
2566
- * explicit "pick up where you left off" instead.
2567
- */
2568
1972
  const CLAUDE_STALL_RESUME_PROMPT = '[pikiloom] The previous agent process stalled mid-turn and was restarted. '
2569
1973
  + 'Continue the task from where it left off — do not start over or repeat work that already completed.';
2570
- /** At most one automatic resume per turn; a second stall surfaces to the user. */
2571
1974
  const CLAUDE_STALL_RESUME_LIMIT = 1;
2572
1975
  async function doClaudeWithRetry(opts) {
2573
1976
  let lastResult = await doClaudeStreamOnce(opts);
2574
- // Mid-turn stall recovery. The TUI driver SIGTERMs a frozen claude process
2575
- // (stopReason 'stalled' — see decideClaudeTuiStall in claude-tui.ts) instead
2576
- // of letting the IM card spin forever. Resume the same session once with a
2577
- // continuation prompt so the turn picks up where the frozen process died.
2578
1977
  let stallResumes = 0;
2579
1978
  while (lastResult.stopReason === 'stalled'
2580
1979
  && stallResumes < CLAUDE_STALL_RESUME_LIMIT
@@ -2593,8 +1992,6 @@ async function doClaudeWithRetry(opts) {
2593
1992
  });
2594
1993
  }
2595
1994
  if (lastResult.stopReason === 'stalled') {
2596
- // Still stalled after the resume budget (or no session id to resume).
2597
- // Surface a self-explanatory failure instead of the raw error text.
2598
1995
  return {
2599
1996
  ...lastResult,
2600
1997
  ok: false,
@@ -2606,9 +2003,6 @@ async function doClaudeWithRetry(opts) {
2606
2003
  };
2607
2004
  }
2608
2005
  let attempts = 0;
2609
- // Use the error text recorded by detectClaudeApiError-driven branches to
2610
- // decide retry: lastResult.error is "Anthropic API error: <reason>" on
2611
- // detection, undefined otherwise.
2612
2006
  const reasonOf = (r) => {
2613
2007
  if (r.stopReason !== 'api_error')
2614
2008
  return null;
@@ -2631,9 +2025,6 @@ async function doClaudeWithRetry(opts) {
2631
2025
  agentWarn('[claude] retry skipped after backoff — abort signal fired');
2632
2026
  break;
2633
2027
  }
2634
- // Resume the same session so we don't restart from scratch. The previous
2635
- // attempt may have written a synthetic "API Error" assistant block into
2636
- // the JSONL; Claude resumes past it and re-answers the user's prompt.
2637
2028
  const nextOpts = {
2638
2029
  ...opts,
2639
2030
  sessionId: lastResult.sessionId || opts.sessionId,
@@ -2651,10 +2042,6 @@ class ClaudeDriver {
2651
2042
  cmd = 'claude';
2652
2043
  thinkLabel = 'Thinking';
2653
2044
  capabilities = { fork: true, modelSwitch: true, workflow: true };
2654
- // Claude Code BYOK routes through ANTHROPIC_BASE_URL — accepts both
2655
- // first-party Anthropic and any openai-compatible provider that exposes an
2656
- // Anthropic-protocol-shaped endpoint (OpenRouter `/api/v1`, DeepSeek
2657
- // `/anthropic/v1`, …). cf. src/model/injector.ts:claudeInjector.
2658
2045
  acceptedProviderKinds = ['anthropic', 'openai-compatible'];
2659
2046
  async doStream(opts) {
2660
2047
  return doClaudeWithRetry(opts);
@@ -2677,10 +2064,6 @@ class ClaudeDriver {
2677
2064
  return emptyUsage('claude', 'HOME is not set.');
2678
2065
  const telemetry = () => getClaudeUsageFromTelemetry(home, opts.model)
2679
2066
  || emptyUsage('claude', 'No recent Claude usage data found.');
2680
- // Throttle the rate-limited OAuth usage query (see CLAUDE_USAGE_QUERY_TTL_MS).
2681
- // Within the window we reuse the last good result rather than re-querying on
2682
- // every agent-status rebuild, so a transient query-API 429 can't blank the
2683
- // ring between successful polls.
2684
2067
  const now = Date.now();
2685
2068
  if (now - claudeUsageCache.lastAttemptAt < CLAUDE_USAGE_QUERY_TTL_MS) {
2686
2069
  return claudeUsageCache.lastGood ?? telemetry();
@@ -2691,8 +2074,6 @@ class ClaudeDriver {
2691
2074
  claudeUsageCache.lastGood = oauth;
2692
2075
  return oauth;
2693
2076
  }
2694
- // OAuth unavailable (non-mac, no token, or transient 429): keep showing the
2695
- // last good windows if we have any; otherwise fall back to telemetry.
2696
2077
  return claudeUsageCache.lastGood ?? telemetry();
2697
2078
  }
2698
2079
  async deleteNativeSession(workdir, sessionId) {