pikiclaw 0.3.78 → 0.3.79
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/dist/agent/drivers/claude-tui.js +93 -117
- package/dist/agent/drivers/claude.js +34 -1
- package/package.json +1 -1
|
@@ -45,7 +45,7 @@ import { encodePathAsDirName, getHome, whichSync } from '../../core/platform.js'
|
|
|
45
45
|
import { createRetainedLogSink } from '../../core/logging.js';
|
|
46
46
|
import { stripAnsiEscapes } from '../../core/utils.js';
|
|
47
47
|
import { AGENT_STREAM_HARD_KILL_GRACE_MS, CLAUDE_TUI_STALL_QUIET_MS, CLAUDE_TUI_STALL_PENDING_TOOL_MS, CLAUDE_TUI_STALL_PTY_DEAD_MS, CLAUDE_TUI_STOP_HOLD_QUIET_TTL_MS, } from '../../core/constants.js';
|
|
48
|
-
import { claudeParse, createClaudeStreamState, claudeContextWindowFromModel, claudeEffectiveContextWindow, registerClaudeBackgroundAgentLaunch, pendingClaudeBackgroundAgentCount, registerClaudeBackgroundBashLaunch, pendingClaudeBackgroundBashCount, extractClaudeBackgroundTaskId, extractClaudeWorkflowRunId, claudeEffortAndWorkflowArgs, } from './claude.js';
|
|
48
|
+
import { claudeParse, createClaudeStreamState, claudeContextWindowFromModel, claudeEffectiveContextWindow, registerClaudeBackgroundAgentLaunch, pendingClaudeBackgroundAgentCount, registerClaudeBackgroundBashLaunch, pendingClaudeBackgroundBashCount, extractClaudeBackgroundTaskId, extractClaudeWorkflowRunId, claudeEffortAndWorkflowArgs, scrubClaudeSessionContextEnv, } from './claude.js';
|
|
49
49
|
// ---------------------------------------------------------------------------
|
|
50
50
|
// Stall diagnostics (capture-only)
|
|
51
51
|
// ---------------------------------------------------------------------------
|
|
@@ -956,12 +956,16 @@ export async function doClaudeTuiStream(opts) {
|
|
|
956
956
|
// PATH inside the claude TUI's hook subprocess on every distro.
|
|
957
957
|
const nodeBin = Q(process.execPath);
|
|
958
958
|
const hookCmd = (event) => `${nodeBin} ${Q(hookPath)} ${event} ${Q(statePath)} ${Q(toolEventsPath)}`;
|
|
959
|
-
// Pre/PostToolUse hooks give us a live event stream.
|
|
960
|
-
//
|
|
961
|
-
//
|
|
962
|
-
//
|
|
963
|
-
//
|
|
964
|
-
//
|
|
959
|
+
// Pre/PostToolUse hooks give us a live tool-event stream. The transcript
|
|
960
|
+
// JSONL is itself written incrementally (events land ~0.2–1.2s after they
|
|
961
|
+
// happen — measured on 2.1.173), but the hooks still earn their keep:
|
|
962
|
+
// PreToolUse fires the instant a tool *starts* (the JSONL tool_use only
|
|
963
|
+
// proves it was requested; a long-running Bash would otherwise sit
|
|
964
|
+
// invisible), they carry agent_id for sub-agent attribution, and they are
|
|
965
|
+
// the registration point for run_in_background launches that the Stop
|
|
966
|
+
// gating in decideClaudeTuiStop depends on. The hook script writes to
|
|
967
|
+
// tool-events.jsonl via atomic appends, sidestepping the
|
|
968
|
+
// read-modify-write race that affects the shared state.json file.
|
|
965
969
|
// Pre/PostToolUse require an explicit `matcher` field — without it Claude
|
|
966
970
|
// Code's hook dispatcher silently never fires the hook (the lifecycle
|
|
967
971
|
// hooks below don't need a matcher because they aren't tool-scoped).
|
|
@@ -1157,9 +1161,13 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1157
1161
|
if (typeof v === 'string')
|
|
1158
1162
|
spawnEnv[k] = v;
|
|
1159
1163
|
}
|
|
1160
|
-
//
|
|
1161
|
-
//
|
|
1162
|
-
|
|
1164
|
+
// Strip the session-context markers a parent claude process exports to its
|
|
1165
|
+
// subprocesses (CLAUDECODE, CLAUDE_CODE_CHILD_SESSION, …). Inherited e.g.
|
|
1166
|
+
// when an agent restarted the pikiclaw daemon from inside a Claude Code
|
|
1167
|
+
// session, they flip this spawn into child-session mode — the transcript
|
|
1168
|
+
// JSONL (our only text source) is then never written locally and the turn
|
|
1169
|
+
// streams nothing. See CLAUDE_SESSION_CONTEXT_ENV_KEYS in claude.ts.
|
|
1170
|
+
scrubClaudeSessionContextEnv(spawnEnv);
|
|
1163
1171
|
// Critical: leaving ANTHROPIC_API_KEY set would route TUI through API
|
|
1164
1172
|
// billing too, defeating the whole point. Strip it unless the user
|
|
1165
1173
|
// explicitly opts back in.
|
|
@@ -1416,11 +1424,69 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1416
1424
|
let lastClearedStopAt = 0;
|
|
1417
1425
|
/** Hook-reported tools still executing: PreToolUse seen, no PostToolUse. */
|
|
1418
1426
|
const pendingHookToolIds = new Set();
|
|
1427
|
+
// Incremental main-JSONL drain — the canonical text/thinking/usage feed.
|
|
1428
|
+
// Used by both the 200ms poll tick and the post-exit final drain. Returns
|
|
1429
|
+
// true when any line was consumed so callers can emit().
|
|
1430
|
+
const drainMainJsonl = () => {
|
|
1431
|
+
if (!fs.existsSync(activeJsonlPath))
|
|
1432
|
+
return false;
|
|
1433
|
+
const inc = readJsonlIncrement(activeJsonlPath, jsonlReadOffset);
|
|
1434
|
+
jsonlReadOffset = inc.offset;
|
|
1435
|
+
let touched = false;
|
|
1436
|
+
for (const line of inc.lines) {
|
|
1437
|
+
const trimmed = line.trim();
|
|
1438
|
+
if (!trimmed || trimmed[0] !== '{')
|
|
1439
|
+
continue;
|
|
1440
|
+
lineCount++;
|
|
1441
|
+
let ev;
|
|
1442
|
+
try {
|
|
1443
|
+
ev = JSON.parse(trimmed);
|
|
1444
|
+
}
|
|
1445
|
+
catch {
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
1448
|
+
// Ignore sub-agent sidecar events — they belong to a child agent's
|
|
1449
|
+
// stream and would re-enter the parent's accumulator. claudeParse's
|
|
1450
|
+
// own sub-agent routing handles them.
|
|
1451
|
+
const isSubAgentEvent = typeof ev.parent_tool_use_id === 'string' && ev.parent_tool_use_id;
|
|
1452
|
+
if (!isSubAgentEvent && ev.type === 'assistant') {
|
|
1453
|
+
const notice = detectClaudeTuiTerminalLimitNotice(ev.message);
|
|
1454
|
+
if (notice) {
|
|
1455
|
+
// A synthetic limit banner is not substantive progress — skip the
|
|
1456
|
+
// liveness/type bookkeeping below so the limit arbitration and the
|
|
1457
|
+
// stall watchdog don't mistake it for a live model segment.
|
|
1458
|
+
noteTerminalLimitNotice(notice);
|
|
1459
|
+
touched = true;
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1462
|
+
applyAssistantStreaming(s, ev.message, streamBuf);
|
|
1463
|
+
applyAssistantUsage(s, ev.message);
|
|
1464
|
+
if (ev.message?.model && ev.message.model !== '<synthetic>' && typeof ev.message.model === 'string') {
|
|
1465
|
+
lastAssistantEventAt = Date.now();
|
|
1466
|
+
s.model = ev.message.model;
|
|
1467
|
+
applyModelContextWindow(s);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
try {
|
|
1471
|
+
callClaudeParseForTui(ev, s);
|
|
1472
|
+
}
|
|
1473
|
+
catch (e) {
|
|
1474
|
+
agentWarn(`[claude-tui] claudeParse threw on line: ${e?.message || e}`);
|
|
1475
|
+
}
|
|
1476
|
+
touched = true;
|
|
1477
|
+
lastMainJsonlEventAt = Date.now();
|
|
1478
|
+
if (typeof ev.version === 'string' && ev.version)
|
|
1479
|
+
observedClaudeVersion = ev.version;
|
|
1480
|
+
if (!isSubAgentEvent)
|
|
1481
|
+
lastMainJsonlType = classifyClaudeJsonlEvent(ev);
|
|
1482
|
+
}
|
|
1483
|
+
return touched;
|
|
1484
|
+
};
|
|
1419
1485
|
// Append-only tool-events log fed by PreToolUse / PostToolUse hooks. We
|
|
1420
|
-
// tail it with the same incremental reader the JSONL transcript uses
|
|
1421
|
-
//
|
|
1422
|
-
//
|
|
1423
|
-
//
|
|
1486
|
+
// tail it with the same incremental reader the JSONL transcript uses. Hook
|
|
1487
|
+
// events usually beat their JSONL counterpart by a second or so (and
|
|
1488
|
+
// PreToolUse fires before the tool even runs); whichever feed arrives first
|
|
1489
|
+
// wins, the other dedups via seenClaudeToolIds / seenClaudeToolResultIds.
|
|
1424
1490
|
let toolEventsReadOffset = 0;
|
|
1425
1491
|
const drainToolEvents = () => {
|
|
1426
1492
|
if (!fs.existsSync(toolEventsPath))
|
|
@@ -1604,69 +1670,18 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1604
1670
|
agentLog(`[claude-tui] prompt-submit nudge sent (no UserPromptSubmit after ${PROMPT_SUBMIT_NUDGE_MS}ms)`);
|
|
1605
1671
|
}
|
|
1606
1672
|
// JSONL tail.
|
|
1607
|
-
if (
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
continue;
|
|
1615
|
-
lineCount++;
|
|
1616
|
-
let ev;
|
|
1617
|
-
try {
|
|
1618
|
-
ev = JSON.parse(trimmed);
|
|
1619
|
-
}
|
|
1620
|
-
catch {
|
|
1621
|
-
continue;
|
|
1622
|
-
}
|
|
1623
|
-
// Ignore sub-agent sidecar events — they belong to a child agent's
|
|
1624
|
-
// stream and would re-enter the parent's accumulator. claudeParse's
|
|
1625
|
-
// own sub-agent routing handles them.
|
|
1626
|
-
const isSubAgentEvent = typeof ev.parent_tool_use_id === 'string' && ev.parent_tool_use_id;
|
|
1627
|
-
if (!isSubAgentEvent && ev.type === 'assistant') {
|
|
1628
|
-
const notice = detectClaudeTuiTerminalLimitNotice(ev.message);
|
|
1629
|
-
if (notice) {
|
|
1630
|
-
noteTerminalLimitNotice(notice);
|
|
1631
|
-
touched = true;
|
|
1632
|
-
continue;
|
|
1633
|
-
}
|
|
1634
|
-
applyAssistantStreaming(s, ev.message, streamBuf);
|
|
1635
|
-
applyAssistantUsage(s, ev.message);
|
|
1636
|
-
if (ev.message?.model && ev.message.model !== '<synthetic>' && typeof ev.message.model === 'string') {
|
|
1637
|
-
lastAssistantEventAt = Date.now();
|
|
1638
|
-
s.model = ev.message.model;
|
|
1639
|
-
applyModelContextWindow(s);
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
|
-
try {
|
|
1643
|
-
callClaudeParseForTui(ev, s);
|
|
1644
|
-
}
|
|
1645
|
-
catch (e) {
|
|
1646
|
-
agentWarn(`[claude-tui] claudeParse threw on line: ${e?.message || e}`);
|
|
1647
|
-
}
|
|
1648
|
-
touched = true;
|
|
1649
|
-
lastMainJsonlEventAt = Date.now();
|
|
1650
|
-
if (typeof ev.version === 'string' && ev.version)
|
|
1651
|
-
observedClaudeVersion = ev.version;
|
|
1652
|
-
if (!isSubAgentEvent)
|
|
1653
|
-
lastMainJsonlType = classifyClaudeJsonlEvent(ev);
|
|
1654
|
-
}
|
|
1655
|
-
if (touched) {
|
|
1656
|
-
// Emit immediately so non-text changes (tool_use, plan, activity,
|
|
1657
|
-
// thinking, usage) reach the dashboard without waiting for the
|
|
1658
|
-
// chunked stream tick. The streaming timer separately advances
|
|
1659
|
-
// s.text from the buffer over the next few ticks.
|
|
1660
|
-
emit();
|
|
1661
|
-
scheduleStreamTick();
|
|
1662
|
-
}
|
|
1673
|
+
if (drainMainJsonl()) {
|
|
1674
|
+
// Emit immediately so non-text changes (tool_use, plan, activity,
|
|
1675
|
+
// thinking, usage) reach the dashboard without waiting for the
|
|
1676
|
+
// chunked stream tick. The streaming timer separately advances
|
|
1677
|
+
// s.text from the buffer over the next few ticks.
|
|
1678
|
+
emit();
|
|
1679
|
+
scheduleStreamTick();
|
|
1663
1680
|
}
|
|
1664
|
-
// Live tool-events stream — fed by Pre/PostToolUse hooks.
|
|
1665
|
-
//
|
|
1666
|
-
//
|
|
1667
|
-
//
|
|
1668
|
-
// In practice JSONL doesn't land until Stop, so this is the only signal
|
|
1669
|
-
// that fires during a normal turn.
|
|
1681
|
+
// Live tool-events stream — fed by Pre/PostToolUse hooks. Hook and JSONL
|
|
1682
|
+
// feeds race per tool call; both record into seenClaudeToolIds /
|
|
1683
|
+
// seenClaudeToolResultIds so whichever lands first wins and the other
|
|
1684
|
+
// pass dedups naturally.
|
|
1670
1685
|
if (drainToolEvents())
|
|
1671
1686
|
emit();
|
|
1672
1687
|
// Sub-agent sidecar discovery + pump. Order matters: discovery first so a
|
|
@@ -1882,47 +1897,8 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1882
1897
|
opts.abortSignal?.removeEventListener('abort', abortStream);
|
|
1883
1898
|
// 11. Final drain — pick up anything written between the last poll and
|
|
1884
1899
|
// process exit. Claude flushes its remaining JSONL events on shutdown.
|
|
1885
|
-
if (
|
|
1886
|
-
|
|
1887
|
-
jsonlReadOffset = inc.offset;
|
|
1888
|
-
let touched = false;
|
|
1889
|
-
for (const line of inc.lines) {
|
|
1890
|
-
const trimmed = line.trim();
|
|
1891
|
-
if (!trimmed || trimmed[0] !== '{')
|
|
1892
|
-
continue;
|
|
1893
|
-
lineCount++;
|
|
1894
|
-
let ev;
|
|
1895
|
-
try {
|
|
1896
|
-
ev = JSON.parse(trimmed);
|
|
1897
|
-
}
|
|
1898
|
-
catch {
|
|
1899
|
-
continue;
|
|
1900
|
-
}
|
|
1901
|
-
const isSubAgentEvent = typeof ev.parent_tool_use_id === 'string' && ev.parent_tool_use_id;
|
|
1902
|
-
if (!isSubAgentEvent && ev.type === 'assistant') {
|
|
1903
|
-
const notice = detectClaudeTuiTerminalLimitNotice(ev.message);
|
|
1904
|
-
if (notice) {
|
|
1905
|
-
noteTerminalLimitNotice(notice);
|
|
1906
|
-
touched = true;
|
|
1907
|
-
continue;
|
|
1908
|
-
}
|
|
1909
|
-
applyAssistantStreaming(s, ev.message, streamBuf);
|
|
1910
|
-
applyAssistantUsage(s, ev.message);
|
|
1911
|
-
if (ev.message?.model && ev.message.model !== '<synthetic>' && typeof ev.message.model === 'string') {
|
|
1912
|
-
lastAssistantEventAt = Date.now();
|
|
1913
|
-
s.model = ev.message.model;
|
|
1914
|
-
applyModelContextWindow(s);
|
|
1915
|
-
}
|
|
1916
|
-
}
|
|
1917
|
-
try {
|
|
1918
|
-
callClaudeParseForTui(ev, s);
|
|
1919
|
-
}
|
|
1920
|
-
catch { }
|
|
1921
|
-
touched = true;
|
|
1922
|
-
}
|
|
1923
|
-
if (touched)
|
|
1924
|
-
emit();
|
|
1925
|
-
}
|
|
1900
|
+
if (drainMainJsonl())
|
|
1901
|
+
emit();
|
|
1926
1902
|
// Final tool-events drain — any PreToolUse / PostToolUse hooks that fired
|
|
1927
1903
|
// between the last poll tick and process exit.
|
|
1928
1904
|
if (drainToolEvents())
|
|
@@ -65,6 +65,39 @@ export function claudeEffortAndWorkflowArgs(o) {
|
|
|
65
65
|
args.push('--disallowed-tools', 'Workflow');
|
|
66
66
|
return args;
|
|
67
67
|
}
|
|
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 pikiclaw itself was
|
|
71
|
+
* launched from inside a Claude Code session — agent-driven `npm run dev`
|
|
72
|
+
* restarts, `! npx pikiclaw` 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
|
+
const CLAUDE_SESSION_CONTEXT_ENV_KEYS = [
|
|
88
|
+
'CLAUDECODE',
|
|
89
|
+
'CLAUDE_CODE_CHILD_SESSION',
|
|
90
|
+
'CLAUDE_CODE_ENTRYPOINT',
|
|
91
|
+
'CLAUDE_CODE_EXECPATH',
|
|
92
|
+
'CLAUDE_CODE_SESSION_ID',
|
|
93
|
+
'CLAUDE_CODE_SSE_PORT',
|
|
94
|
+
'CLAUDE_EFFORT',
|
|
95
|
+
'CLAUDE_PERMISSION_MODE',
|
|
96
|
+
];
|
|
97
|
+
export function scrubClaudeSessionContextEnv(env) {
|
|
98
|
+
for (const key of CLAUDE_SESSION_CONTEXT_ENV_KEYS)
|
|
99
|
+
delete env[key];
|
|
100
|
+
}
|
|
68
101
|
// ---------------------------------------------------------------------------
|
|
69
102
|
// Command & parser
|
|
70
103
|
// ---------------------------------------------------------------------------
|
|
@@ -1001,7 +1034,7 @@ async function doClaudeInteractiveStream(opts) {
|
|
|
1001
1034
|
agentLog(`[spawn] timeout: ${opts.timeout}s session: ${opts.sessionId || '(new)'}`);
|
|
1002
1035
|
agentLog(`[spawn] prompt (stdin): "${opts.prompt.slice(0, 300)}${opts.prompt.length > 300 ? '…' : ''}"`);
|
|
1003
1036
|
const spawnEnv = { ...process.env, ...(opts.extraEnv || {}) };
|
|
1004
|
-
|
|
1037
|
+
scrubClaudeSessionContextEnv(spawnEnv);
|
|
1005
1038
|
const proc = spawn(shellCmd, {
|
|
1006
1039
|
cwd: opts.workdir,
|
|
1007
1040
|
env: spawnEnv,
|
package/package.json
CHANGED