pikiloom 0.4.26 → 0.4.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/dist/assets/{AgentTab-BNJ73P0m.js → AgentTab-DtjuI8NU.js} +1 -1
- package/dashboard/dist/assets/ConnectionModal-BIx6a5o1.js +1 -0
- package/dashboard/dist/assets/{DirBrowser-D-GT-G-w.js → DirBrowser-BINh_7NB.js} +1 -1
- package/dashboard/dist/assets/{ExtensionsTab-BrJMulJR.js → ExtensionsTab-CHpd147b.js} +1 -1
- package/dashboard/dist/assets/{IMAccessTab-auPnx468.js → IMAccessTab-C4DbGxbG.js} +1 -1
- package/dashboard/dist/assets/{Modal-lYfO9m-I.js → Modal-D_PUi-5O.js} +1 -1
- package/dashboard/dist/assets/{Modals-PRdWOIY9.js → Modals-BahDEMqf.js} +1 -1
- package/dashboard/dist/assets/{Select-BhbU-rll.js → Select-Cp5VXb_2.js} +1 -1
- package/dashboard/dist/assets/SessionPanel-DZVNXptx.js +1 -0
- package/dashboard/dist/assets/SystemTab-CLznS31f.js +1 -0
- package/dashboard/dist/assets/index-CLGovkkw.js +3 -0
- package/dashboard/dist/assets/index-CXIN3nTr.css +1 -0
- package/dashboard/dist/assets/index-DcYOI6rL.js +23 -0
- package/dashboard/dist/assets/{shared-C5uXvhZJ.js → shared-C8DCTJ44.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/agent/acp-client.js +0 -59
- package/dist/agent/artifacts.js +1 -38
- package/dist/agent/auto-update.js +2 -78
- package/dist/agent/await-resume.js +0 -20
- package/dist/agent/cli/auth.js +3 -51
- package/dist/agent/cli/catalog.js +0 -3
- package/dist/agent/cli/detector.js +1 -19
- package/dist/agent/cli/index.js +0 -3
- package/dist/agent/cli/registry.js +0 -25
- package/dist/agent/driver.js +0 -14
- package/dist/agent/drivers/claude-tui.js +2 -769
- package/dist/agent/drivers/claude.js +12 -631
- package/dist/agent/drivers/codex.js +11 -162
- package/dist/agent/drivers/gemini.js +6 -114
- package/dist/agent/drivers/hermes.js +0 -128
- package/dist/agent/goal.js +0 -45
- package/dist/agent/handover.js +1 -46
- package/dist/agent/images.js +7 -133
- package/dist/agent/index.js +1 -29
- package/dist/agent/mcp/bridge.js +1 -197
- package/dist/agent/mcp/extensions.js +5 -84
- package/dist/agent/mcp/oauth.js +2 -69
- package/dist/agent/mcp/registry.js +2 -33
- package/dist/agent/mcp/session-server.js +2 -45
- package/dist/agent/mcp/tools/ask-user.js +0 -6
- package/dist/agent/mcp/tools/await-resume.js +0 -15
- package/dist/agent/mcp/tools/goal.js +0 -25
- package/dist/agent/mcp/tools/types.js +0 -5
- package/dist/agent/mcp/tools/workspace.js +0 -23
- package/dist/agent/npm.js +0 -14
- package/dist/agent/session.js +86 -196
- package/dist/agent/skill-installer.js +3 -70
- package/dist/agent/skills.js +0 -43
- package/dist/agent/stream.js +5 -148
- package/dist/agent/types.js +0 -12
- package/dist/agent/utils.js +2 -120
- package/dist/bot/bot.js +3 -468
- package/dist/bot/command-ui.js +0 -45
- package/dist/bot/commands.js +0 -51
- package/dist/bot/headless-bot.js +0 -21
- package/dist/bot/host.js +1 -5
- package/dist/bot/human-loop.js +0 -8
- package/dist/bot/menu.js +0 -3
- package/dist/bot/orchestration.js +0 -3
- package/dist/bot/render-shared.js +0 -63
- package/dist/bot/session-hub.js +14 -92
- package/dist/bot/session-status.js +1 -5
- package/dist/bot/streaming.js +0 -53
- package/dist/browser-profile.js +0 -51
- package/dist/browser-supervisor.js +0 -75
- package/dist/catalog/cli-tools.js +0 -53
- package/dist/catalog/index.js +0 -18
- package/dist/catalog/local-models.js +0 -18
- package/dist/catalog/mcp-servers.js +0 -35
- package/dist/catalog/skill-repos.js +0 -32
- package/dist/channels/base.js +0 -19
- package/dist/channels/dingtalk/bot.js +0 -3
- package/dist/channels/dingtalk/channel.js +0 -33
- package/dist/channels/discord/bot.js +0 -7
- package/dist/channels/discord/channel.js +0 -22
- package/dist/channels/feishu/bot.js +0 -71
- package/dist/channels/feishu/channel.js +0 -78
- package/dist/channels/feishu/markdown.js +0 -5
- package/dist/channels/feishu/render.js +0 -45
- package/dist/channels/health.js +0 -46
- package/dist/channels/slack/bot.js +0 -7
- package/dist/channels/slack/channel.js +1 -27
- package/dist/channels/states.js +0 -3
- package/dist/channels/telegram/bot.js +0 -60
- package/dist/channels/telegram/channel.js +2 -99
- package/dist/channels/telegram/directory.js +0 -3
- package/dist/channels/telegram/live-preview.js +0 -8
- package/dist/channels/telegram/render.js +0 -30
- package/dist/channels/wecom/bot.js +0 -3
- package/dist/channels/wecom/channel.js +0 -58
- package/dist/channels/weixin/api.js +0 -9
- package/dist/channels/weixin/bot.js +0 -88
- package/dist/channels/weixin/channel.js +0 -6
- package/dist/cli/autostart.js +1 -44
- package/dist/cli/channel-supervisor.js +1 -59
- package/dist/cli/channels.js +0 -6
- package/dist/cli/main.js +1 -145
- package/dist/cli/onboarding.js +0 -6
- package/dist/cli/run.js +0 -11
- package/dist/cli/setup-wizard.js +0 -3
- package/dist/core/config/runtime-config.js +0 -38
- package/dist/core/config/user-config.js +0 -63
- package/dist/core/config/validation.js +0 -9
- package/dist/core/constants.js +0 -268
- package/dist/core/git.js +0 -31
- package/dist/core/legacy-compat.js +1 -28
- package/dist/core/logging.js +0 -3
- package/dist/core/platform.js +0 -28
- package/dist/core/process-control.js +0 -8
- package/dist/core/secrets/index.js +0 -17
- package/dist/core/secrets/inline-seal.js +0 -7
- package/dist/core/secrets/ref.js +0 -19
- package/dist/core/secrets/resolver.js +0 -8
- package/dist/core/secrets/store.js +1 -12
- package/dist/core/utils.js +1 -37
- package/dist/core/version.js +0 -3
- package/dist/dashboard/platform.js +1 -35
- package/dist/dashboard/routes/agents.js +2 -71
- package/dist/dashboard/routes/cli.js +0 -27
- package/dist/dashboard/routes/config.js +0 -54
- package/dist/dashboard/routes/extensions.js +0 -132
- package/dist/dashboard/routes/local-models.js +0 -68
- package/dist/dashboard/routes/models.js +0 -46
- package/dist/dashboard/routes/sessions.js +0 -100
- package/dist/dashboard/runtime.js +1 -58
- package/dist/dashboard/server.js +0 -49
- package/dist/dashboard/session-control.js +2 -69
- package/dist/model/catalog.js +1 -17
- package/dist/model/index.js +0 -15
- package/dist/model/injector.js +1 -181
- package/dist/model/provider-models.js +1 -48
- package/dist/model/responses-bridge.js +3 -45
- package/dist/model/store.js +0 -17
- package/dist/model/types.js +0 -12
- package/dist/model/validation.js +1 -18
- package/dist/pikichannel/adapter-pikiloom.js +0 -31
- package/dist/pikichannel/code.js +0 -12
- package/dist/pikichannel/codec.js +0 -14
- package/dist/pikichannel/host.js +4 -33
- package/dist/pikichannel/protocol.js +0 -52
- package/dist/pikichannel/rendezvous-broker.js +5 -26
- package/dist/pikichannel/rendezvous-host.js +3 -16
- package/dist/pikichannel/server.js +6 -55
- package/dist/pikichannel/transport.js +0 -25
- package/dist/pikichannel/transports/webrtc-host.js +0 -18
- package/dist/pikichannel/transports/webrtc-shared.js +4 -34
- package/dist/pikichannel/transports/websocket-host.js +1 -14
- package/dist/pikichannel/turn.js +2 -69
- package/dist/pikichannel/web/sdk.js +14 -71
- package/package.json +1 -1
- package/dashboard/dist/assets/ConnectionModal-Dt05H2f9.js +0 -1
- package/dashboard/dist/assets/SessionPanel-_0dLWvEb.js +0 -1
- package/dashboard/dist/assets/SystemTab-CN_QrRKt.js +0 -1
- package/dashboard/dist/assets/index-CP3LYJR2.js +0 -23
- package/dashboard/dist/assets/index-C_neLnaO.js +0 -3
- 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;
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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) {
|