pikiclaw 0.3.73 → 0.3.75
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 +95 -20
- package/dist/agent/drivers/claude.js +26 -15
- 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, } from './claude.js';
|
|
48
|
+
import { claudeParse, createClaudeStreamState, claudeContextWindowFromModel, claudeEffectiveContextWindow, registerClaudeBackgroundAgentLaunch, pendingClaudeBackgroundAgentCount, registerClaudeBackgroundBashLaunch, pendingClaudeBackgroundBashCount, extractClaudeBackgroundTaskId, extractClaudeWorkflowRunId, claudeEffortAndWorkflowArgs, } from './claude.js';
|
|
49
49
|
// ---------------------------------------------------------------------------
|
|
50
50
|
// Stall diagnostics (capture-only)
|
|
51
51
|
// ---------------------------------------------------------------------------
|
|
@@ -323,6 +323,32 @@ export function detectClaudeTuiTerminalLimitNotice(msgOrText) {
|
|
|
323
323
|
return null;
|
|
324
324
|
return limitNoticeFromText(extractTextBlocks(msgOrText.content));
|
|
325
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* Evidence-based arbitration for a detected limit notice. The banner text is
|
|
328
|
+
* deliberately matched broadly (wording shifts across CLI versions and some
|
|
329
|
+
* notices are informational — "You're now using usage credits · Your session
|
|
330
|
+
* limit resets 3pm" means the turn CONTINUES on extra-usage credits), so a
|
|
331
|
+
* match alone must never fail the turn. What decides the outcome is whether
|
|
332
|
+
* the turn produced anything substantive after the banner:
|
|
333
|
+
*
|
|
334
|
+
* - 'info' — assistant text exists, or a substantive signal (non-synthetic
|
|
335
|
+
* assistant JSONL, hook tool event, sub-agent sidecar) postdates
|
|
336
|
+
* the notice. The turn is alive; the notice is informational.
|
|
337
|
+
* - 'fatal' — nothing substantive after the banner. The limit genuinely ate
|
|
338
|
+
* the turn; surface the banner text as a rate_limit failure.
|
|
339
|
+
* - 'none' — no notice was seen.
|
|
340
|
+
*
|
|
341
|
+
* Worst case of the broad matching is therefore an activity line, not a
|
|
342
|
+
* killed turn (the bug this replaced: the credits banner used to SIGTERM the
|
|
343
|
+
* process mid-answer).
|
|
344
|
+
*/
|
|
345
|
+
export function resolveClaudeTuiLimitOutcome(input) {
|
|
346
|
+
if (!input.noticeText)
|
|
347
|
+
return 'none';
|
|
348
|
+
if (input.hasOutputText || input.lastSubstantiveEventAt > input.noticeAt)
|
|
349
|
+
return 'info';
|
|
350
|
+
return 'fatal';
|
|
351
|
+
}
|
|
326
352
|
/**
|
|
327
353
|
* Detect Claude Code's startup "Bypass Permissions mode" confirmation dialog in
|
|
328
354
|
* a slice of (ANSI-stripped) PTY screen output. When pikiclaw spawns the TUI
|
|
@@ -976,8 +1002,9 @@ export async function doClaudeTuiStream(opts) {
|
|
|
976
1002
|
claudeArgs.push('--model', model);
|
|
977
1003
|
if (opts.claudePermissionMode)
|
|
978
1004
|
claudeArgs.push('--permission-mode', opts.claudePermissionMode);
|
|
979
|
-
|
|
980
|
-
|
|
1005
|
+
// Effort + Workflow gate — same source of truth as the `claude -p` driver, so
|
|
1006
|
+
// the TUI path drops the Workflow tool unless orchestration was opted in.
|
|
1007
|
+
claudeArgs.push(...claudeEffortAndWorkflowArgs(opts));
|
|
981
1008
|
if (opts.claudeAppendSystemPrompt)
|
|
982
1009
|
claudeArgs.push('--append-system-prompt', opts.claudeAppendSystemPrompt);
|
|
983
1010
|
if (opts.mcpConfigPath)
|
|
@@ -1048,6 +1075,7 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1048
1075
|
let exitCode = null;
|
|
1049
1076
|
let exitSignal = null;
|
|
1050
1077
|
let terminalLimitNotice = null;
|
|
1078
|
+
let terminalLimitNoticeAt = 0;
|
|
1051
1079
|
let proc;
|
|
1052
1080
|
const emit = () => {
|
|
1053
1081
|
try {
|
|
@@ -1069,16 +1097,20 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1069
1097
|
}
|
|
1070
1098
|
}, after);
|
|
1071
1099
|
};
|
|
1072
|
-
|
|
1100
|
+
// Record-only: a limit banner is EVIDENCE, not a verdict. Some banners are
|
|
1101
|
+
// informational (extra-usage credits kick in and the turn continues), so
|
|
1102
|
+
// killing here would shoot healthy turns. resolveClaudeTuiLimitOutcome
|
|
1103
|
+
// arbitrates later — at the stall watchdog and at result assembly — based
|
|
1104
|
+
// on whether the turn produced anything substantive after the banner.
|
|
1105
|
+
const noteTerminalLimitNotice = (notice) => {
|
|
1073
1106
|
if (terminalLimitNotice)
|
|
1074
1107
|
return;
|
|
1075
1108
|
terminalLimitNotice = notice;
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1109
|
+
terminalLimitNoticeAt = Date.now();
|
|
1110
|
+
agentWarn(`[claude-tui] limit notice observed (watching turn liveness): ${notice}`);
|
|
1111
|
+
pushRecentActivity(s.recentActivity, `Claude usage notice: ${notice}`);
|
|
1112
|
+
s.activity = s.recentActivity.join('\n');
|
|
1079
1113
|
emit();
|
|
1080
|
-
if (!processExited)
|
|
1081
|
-
killProc('SIGTERM', 1500);
|
|
1082
1114
|
};
|
|
1083
1115
|
// Simulated streaming. See TuiStreamBuffer / applyAssistantStreaming above.
|
|
1084
1116
|
const streamBuf = makeTuiStreamBuffer();
|
|
@@ -1317,7 +1349,7 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1317
1349
|
stderrCapture = stderrCapture.slice(0, 4096);
|
|
1318
1350
|
const notice = detectClaudeTuiTerminalLimitNotice(stderrCapture);
|
|
1319
1351
|
if (notice)
|
|
1320
|
-
|
|
1352
|
+
noteTerminalLimitNotice(notice);
|
|
1321
1353
|
}
|
|
1322
1354
|
});
|
|
1323
1355
|
// 7. Abort handling.
|
|
@@ -1367,6 +1399,11 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1367
1399
|
// decideClaudeTuiStall for why this exists (claude CLI mid-turn freeze).
|
|
1368
1400
|
let lastToolEventAt = start;
|
|
1369
1401
|
let lastSidecarEventAt = 0;
|
|
1402
|
+
// Last non-synthetic assistant JSONL event — substantive-progress signal
|
|
1403
|
+
// for the limit-notice arbitration (resolveClaudeTuiLimitOutcome). Distinct
|
|
1404
|
+
// from lastMainJsonlEventAt, which also counts bookkeeping lines (mode,
|
|
1405
|
+
// last-prompt, …) that land right after submit and prove nothing.
|
|
1406
|
+
let lastAssistantEventAt = 0;
|
|
1370
1407
|
let stallKilled = false;
|
|
1371
1408
|
// Stall diagnostics (capture-only) — see writeStallDiag.
|
|
1372
1409
|
let observedClaudeVersion = '';
|
|
@@ -1590,13 +1627,14 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1590
1627
|
if (!isSubAgentEvent && ev.type === 'assistant') {
|
|
1591
1628
|
const notice = detectClaudeTuiTerminalLimitNotice(ev.message);
|
|
1592
1629
|
if (notice) {
|
|
1593
|
-
|
|
1630
|
+
noteTerminalLimitNotice(notice);
|
|
1594
1631
|
touched = true;
|
|
1595
1632
|
continue;
|
|
1596
1633
|
}
|
|
1597
1634
|
applyAssistantStreaming(s, ev.message, streamBuf);
|
|
1598
1635
|
applyAssistantUsage(s, ev.message);
|
|
1599
1636
|
if (ev.message?.model && ev.message.model !== '<synthetic>' && typeof ev.message.model === 'string') {
|
|
1637
|
+
lastAssistantEventAt = Date.now();
|
|
1600
1638
|
s.model = ev.message.model;
|
|
1601
1639
|
applyModelContextWindow(s);
|
|
1602
1640
|
}
|
|
@@ -1788,15 +1826,34 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1788
1826
|
screenSample: stallScreen.sample,
|
|
1789
1827
|
});
|
|
1790
1828
|
if (!s.errors) {
|
|
1791
|
-
//
|
|
1792
|
-
//
|
|
1793
|
-
//
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1829
|
+
// Limit-notice arbitration first: a turn that showed a limit banner
|
|
1830
|
+
// and then produced nothing substantive didn't freeze — the limit
|
|
1831
|
+
// ate it. Label it rate_limit with the banner's own text (which
|
|
1832
|
+
// carries the reset time) so the user gets the real reason, and so
|
|
1833
|
+
// doClaudeWithRetry doesn't auto-resume into the same wall.
|
|
1834
|
+
const limitOutcome = resolveClaudeTuiLimitOutcome({
|
|
1835
|
+
noticeText: terminalLimitNotice,
|
|
1836
|
+
noticeAt: terminalLimitNoticeAt,
|
|
1837
|
+
lastSubstantiveEventAt: Math.max(lastAssistantEventAt, lastToolEventAt, lastSidecarEventAt),
|
|
1838
|
+
hasOutputText: !!s.text.trim(),
|
|
1839
|
+
});
|
|
1840
|
+
if (limitOutcome === 'fatal') {
|
|
1841
|
+
s.stopReason = 'rate_limit';
|
|
1842
|
+
s.errors = [terminalLimitNotice];
|
|
1843
|
+
}
|
|
1844
|
+
else {
|
|
1845
|
+
// Be honest about which kind of stall this is. looksLikePrompt here
|
|
1846
|
+
// means the auto-answer (detectClaudeProceedPrompt) did NOT clear an
|
|
1847
|
+
// interactive prompt — so it's a blocking dialog, not the CLI freeze.
|
|
1848
|
+
s.errors = [stallScreen.looksLikePrompt
|
|
1849
|
+
? `Claude blocked mid-turn on an interactive prompt (PTY quiet ${ptyQuietS}s) that auto-answer couldn't clear. Terminated for auto-resume.`
|
|
1850
|
+
: `Claude process went silent mid-turn for ${quietMin}m (no JSONL, hook, or sub-agent events; PTY quiet ${ptyQuietS}s) — known claude CLI freeze. Terminated for auto-resume.`];
|
|
1851
|
+
}
|
|
1797
1852
|
}
|
|
1798
|
-
agentWarn(`[claude-tui] stall detected: no progress for ${quietMin}m (pendingTools=${pendingHookToolIds.size}, ptyQuiet=${ptyQuietS}s) — terminating TUI pid=${proc.pid} for auto-resume`);
|
|
1799
|
-
pushRecentActivity(s.recentActivity,
|
|
1853
|
+
agentWarn(`[claude-tui] stall detected: no progress for ${quietMin}m (pendingTools=${pendingHookToolIds.size}, ptyQuiet=${ptyQuietS}s) — terminating TUI pid=${proc.pid}${s.stopReason === 'rate_limit' ? ' (usage limit)' : ' for auto-resume'}`);
|
|
1854
|
+
pushRecentActivity(s.recentActivity, s.stopReason === 'rate_limit'
|
|
1855
|
+
? 'Usage limit blocked the turn — stopping'
|
|
1856
|
+
: `Agent stalled (${quietMin}m silent) — restarting turn`);
|
|
1800
1857
|
s.activity = s.recentActivity.join('\n');
|
|
1801
1858
|
emit();
|
|
1802
1859
|
killProc('SIGTERM');
|
|
@@ -1845,13 +1902,14 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1845
1902
|
if (!isSubAgentEvent && ev.type === 'assistant') {
|
|
1846
1903
|
const notice = detectClaudeTuiTerminalLimitNotice(ev.message);
|
|
1847
1904
|
if (notice) {
|
|
1848
|
-
|
|
1905
|
+
noteTerminalLimitNotice(notice);
|
|
1849
1906
|
touched = true;
|
|
1850
1907
|
continue;
|
|
1851
1908
|
}
|
|
1852
1909
|
applyAssistantStreaming(s, ev.message, streamBuf);
|
|
1853
1910
|
applyAssistantUsage(s, ev.message);
|
|
1854
1911
|
if (ev.message?.model && ev.message.model !== '<synthetic>' && typeof ev.message.model === 'string') {
|
|
1912
|
+
lastAssistantEventAt = Date.now();
|
|
1855
1913
|
s.model = ev.message.model;
|
|
1856
1914
|
applyModelContextWindow(s);
|
|
1857
1915
|
}
|
|
@@ -1908,6 +1966,23 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1908
1966
|
if (!s.errors)
|
|
1909
1967
|
s.errors = [`Anthropic API error: ${apiErrorReason}`];
|
|
1910
1968
|
}
|
|
1969
|
+
// Limit-notice arbitration (see resolveClaudeTuiLimitOutcome). Covers the
|
|
1970
|
+
// paths the stall watchdog never reaches: the TUI painted a limit banner,
|
|
1971
|
+
// then Stop fired on an empty turn or the process exited — nothing
|
|
1972
|
+
// substantive ever followed the banner, so the limit ate the turn. A banner
|
|
1973
|
+
// followed by real output stays informational (already in the activity log).
|
|
1974
|
+
if (!interrupted && !timedOut && !s.errors) {
|
|
1975
|
+
const limitOutcome = resolveClaudeTuiLimitOutcome({
|
|
1976
|
+
noticeText: terminalLimitNotice,
|
|
1977
|
+
noticeAt: terminalLimitNoticeAt,
|
|
1978
|
+
lastSubstantiveEventAt: Math.max(lastAssistantEventAt, lastToolEventAt, lastSidecarEventAt),
|
|
1979
|
+
hasOutputText: !!s.text.trim(),
|
|
1980
|
+
});
|
|
1981
|
+
if (limitOutcome === 'fatal') {
|
|
1982
|
+
s.stopReason = 'rate_limit';
|
|
1983
|
+
s.errors = [terminalLimitNotice];
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1911
1986
|
const errorText = joinErrorMessages(s.errors);
|
|
1912
1987
|
// "ok" requires: process exited cleanly (or via our own SIGTERM after Stop
|
|
1913
1988
|
// hook fired, which yields a non-zero exit), no errors from the parser, no
|
|
@@ -42,6 +42,29 @@ function claudeUsesStreamJsonInput(o) {
|
|
|
42
42
|
return !!o.attachments?.length || !!o.onSteerReady;
|
|
43
43
|
}
|
|
44
44
|
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 pikiclaw 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
|
+
export function claudeEffortAndWorkflowArgs(o) {
|
|
60
|
+
const args = [];
|
|
61
|
+
const ultraEffort = o.thinkingEffort === 'ultra';
|
|
62
|
+
if (o.thinkingEffort)
|
|
63
|
+
args.push('--effort', ultraEffort ? 'max' : o.thinkingEffort);
|
|
64
|
+
if (!o.claudeWorkflowEnabled && !ultraEffort)
|
|
65
|
+
args.push('--disallowed-tools', 'Workflow');
|
|
66
|
+
return args;
|
|
67
|
+
}
|
|
45
68
|
// ---------------------------------------------------------------------------
|
|
46
69
|
// Command & parser
|
|
47
70
|
// ---------------------------------------------------------------------------
|
|
@@ -70,21 +93,9 @@ function claudeCmd(o) {
|
|
|
70
93
|
if (o.attachments?.length)
|
|
71
94
|
o._stdinOverride = buildClaudeUserMessage(o.prompt, o.attachments);
|
|
72
95
|
}
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
// break — the CLI, and so it never suppresses the Workflow tool below.
|
|
77
|
-
const ultraEffort = o.thinkingEffort === 'ultra';
|
|
78
|
-
if (o.thinkingEffort)
|
|
79
|
-
args.push('--effort', ultraEffort ? 'max' : o.thinkingEffort);
|
|
80
|
-
// Multi-agent Workflow gate. The Workflow tool is always present in the
|
|
81
|
-
// toolset and triggers on a bare "workflow" keyword — combined with the
|
|
82
|
-
// bypassPermissions mode pikiclaw runs by default, that means an offhand
|
|
83
|
-
// mention could auto-spawn a fleet of sub-agents. Unless orchestration is
|
|
84
|
-
// explicitly enabled, drop the tool entirely so it can't fire at all. When
|
|
85
|
-
// enabled, the bot injects a standing opt-in directive via the system prompt.
|
|
86
|
-
if (!o.claudeWorkflowEnabled && !ultraEffort)
|
|
87
|
-
args.push('--disallowed-tools', 'Workflow');
|
|
96
|
+
// Effort + Workflow gate — shared with the TUI driver (claude-tui.ts) so the
|
|
97
|
+
// two spawn paths can never drift. See claudeEffortAndWorkflowArgs.
|
|
98
|
+
args.push(...claudeEffortAndWorkflowArgs(o));
|
|
88
99
|
if (o.claudeAppendSystemPrompt)
|
|
89
100
|
args.push('--append-system-prompt', o.claudeAppendSystemPrompt);
|
|
90
101
|
if (o.mcpConfigPath)
|
package/package.json
CHANGED