pikiclaw 0.3.73 → 0.3.74
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 +91 -17
- package/package.json +1 -1
|
@@ -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
|
|
@@ -1048,6 +1074,7 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1048
1074
|
let exitCode = null;
|
|
1049
1075
|
let exitSignal = null;
|
|
1050
1076
|
let terminalLimitNotice = null;
|
|
1077
|
+
let terminalLimitNoticeAt = 0;
|
|
1051
1078
|
let proc;
|
|
1052
1079
|
const emit = () => {
|
|
1053
1080
|
try {
|
|
@@ -1069,16 +1096,20 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1069
1096
|
}
|
|
1070
1097
|
}, after);
|
|
1071
1098
|
};
|
|
1072
|
-
|
|
1099
|
+
// Record-only: a limit banner is EVIDENCE, not a verdict. Some banners are
|
|
1100
|
+
// informational (extra-usage credits kick in and the turn continues), so
|
|
1101
|
+
// killing here would shoot healthy turns. resolveClaudeTuiLimitOutcome
|
|
1102
|
+
// arbitrates later — at the stall watchdog and at result assembly — based
|
|
1103
|
+
// on whether the turn produced anything substantive after the banner.
|
|
1104
|
+
const noteTerminalLimitNotice = (notice) => {
|
|
1073
1105
|
if (terminalLimitNotice)
|
|
1074
1106
|
return;
|
|
1075
1107
|
terminalLimitNotice = notice;
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1108
|
+
terminalLimitNoticeAt = Date.now();
|
|
1109
|
+
agentWarn(`[claude-tui] limit notice observed (watching turn liveness): ${notice}`);
|
|
1110
|
+
pushRecentActivity(s.recentActivity, `Claude usage notice: ${notice}`);
|
|
1111
|
+
s.activity = s.recentActivity.join('\n');
|
|
1079
1112
|
emit();
|
|
1080
|
-
if (!processExited)
|
|
1081
|
-
killProc('SIGTERM', 1500);
|
|
1082
1113
|
};
|
|
1083
1114
|
// Simulated streaming. See TuiStreamBuffer / applyAssistantStreaming above.
|
|
1084
1115
|
const streamBuf = makeTuiStreamBuffer();
|
|
@@ -1317,7 +1348,7 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1317
1348
|
stderrCapture = stderrCapture.slice(0, 4096);
|
|
1318
1349
|
const notice = detectClaudeTuiTerminalLimitNotice(stderrCapture);
|
|
1319
1350
|
if (notice)
|
|
1320
|
-
|
|
1351
|
+
noteTerminalLimitNotice(notice);
|
|
1321
1352
|
}
|
|
1322
1353
|
});
|
|
1323
1354
|
// 7. Abort handling.
|
|
@@ -1367,6 +1398,11 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1367
1398
|
// decideClaudeTuiStall for why this exists (claude CLI mid-turn freeze).
|
|
1368
1399
|
let lastToolEventAt = start;
|
|
1369
1400
|
let lastSidecarEventAt = 0;
|
|
1401
|
+
// Last non-synthetic assistant JSONL event — substantive-progress signal
|
|
1402
|
+
// for the limit-notice arbitration (resolveClaudeTuiLimitOutcome). Distinct
|
|
1403
|
+
// from lastMainJsonlEventAt, which also counts bookkeeping lines (mode,
|
|
1404
|
+
// last-prompt, …) that land right after submit and prove nothing.
|
|
1405
|
+
let lastAssistantEventAt = 0;
|
|
1370
1406
|
let stallKilled = false;
|
|
1371
1407
|
// Stall diagnostics (capture-only) — see writeStallDiag.
|
|
1372
1408
|
let observedClaudeVersion = '';
|
|
@@ -1590,13 +1626,14 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1590
1626
|
if (!isSubAgentEvent && ev.type === 'assistant') {
|
|
1591
1627
|
const notice = detectClaudeTuiTerminalLimitNotice(ev.message);
|
|
1592
1628
|
if (notice) {
|
|
1593
|
-
|
|
1629
|
+
noteTerminalLimitNotice(notice);
|
|
1594
1630
|
touched = true;
|
|
1595
1631
|
continue;
|
|
1596
1632
|
}
|
|
1597
1633
|
applyAssistantStreaming(s, ev.message, streamBuf);
|
|
1598
1634
|
applyAssistantUsage(s, ev.message);
|
|
1599
1635
|
if (ev.message?.model && ev.message.model !== '<synthetic>' && typeof ev.message.model === 'string') {
|
|
1636
|
+
lastAssistantEventAt = Date.now();
|
|
1600
1637
|
s.model = ev.message.model;
|
|
1601
1638
|
applyModelContextWindow(s);
|
|
1602
1639
|
}
|
|
@@ -1788,15 +1825,34 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1788
1825
|
screenSample: stallScreen.sample,
|
|
1789
1826
|
});
|
|
1790
1827
|
if (!s.errors) {
|
|
1791
|
-
//
|
|
1792
|
-
//
|
|
1793
|
-
//
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1828
|
+
// Limit-notice arbitration first: a turn that showed a limit banner
|
|
1829
|
+
// and then produced nothing substantive didn't freeze — the limit
|
|
1830
|
+
// ate it. Label it rate_limit with the banner's own text (which
|
|
1831
|
+
// carries the reset time) so the user gets the real reason, and so
|
|
1832
|
+
// doClaudeWithRetry doesn't auto-resume into the same wall.
|
|
1833
|
+
const limitOutcome = resolveClaudeTuiLimitOutcome({
|
|
1834
|
+
noticeText: terminalLimitNotice,
|
|
1835
|
+
noticeAt: terminalLimitNoticeAt,
|
|
1836
|
+
lastSubstantiveEventAt: Math.max(lastAssistantEventAt, lastToolEventAt, lastSidecarEventAt),
|
|
1837
|
+
hasOutputText: !!s.text.trim(),
|
|
1838
|
+
});
|
|
1839
|
+
if (limitOutcome === 'fatal') {
|
|
1840
|
+
s.stopReason = 'rate_limit';
|
|
1841
|
+
s.errors = [terminalLimitNotice];
|
|
1842
|
+
}
|
|
1843
|
+
else {
|
|
1844
|
+
// Be honest about which kind of stall this is. looksLikePrompt here
|
|
1845
|
+
// means the auto-answer (detectClaudeProceedPrompt) did NOT clear an
|
|
1846
|
+
// interactive prompt — so it's a blocking dialog, not the CLI freeze.
|
|
1847
|
+
s.errors = [stallScreen.looksLikePrompt
|
|
1848
|
+
? `Claude blocked mid-turn on an interactive prompt (PTY quiet ${ptyQuietS}s) that auto-answer couldn't clear. Terminated for auto-resume.`
|
|
1849
|
+
: `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.`];
|
|
1850
|
+
}
|
|
1797
1851
|
}
|
|
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,
|
|
1852
|
+
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'}`);
|
|
1853
|
+
pushRecentActivity(s.recentActivity, s.stopReason === 'rate_limit'
|
|
1854
|
+
? 'Usage limit blocked the turn — stopping'
|
|
1855
|
+
: `Agent stalled (${quietMin}m silent) — restarting turn`);
|
|
1800
1856
|
s.activity = s.recentActivity.join('\n');
|
|
1801
1857
|
emit();
|
|
1802
1858
|
killProc('SIGTERM');
|
|
@@ -1845,13 +1901,14 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1845
1901
|
if (!isSubAgentEvent && ev.type === 'assistant') {
|
|
1846
1902
|
const notice = detectClaudeTuiTerminalLimitNotice(ev.message);
|
|
1847
1903
|
if (notice) {
|
|
1848
|
-
|
|
1904
|
+
noteTerminalLimitNotice(notice);
|
|
1849
1905
|
touched = true;
|
|
1850
1906
|
continue;
|
|
1851
1907
|
}
|
|
1852
1908
|
applyAssistantStreaming(s, ev.message, streamBuf);
|
|
1853
1909
|
applyAssistantUsage(s, ev.message);
|
|
1854
1910
|
if (ev.message?.model && ev.message.model !== '<synthetic>' && typeof ev.message.model === 'string') {
|
|
1911
|
+
lastAssistantEventAt = Date.now();
|
|
1855
1912
|
s.model = ev.message.model;
|
|
1856
1913
|
applyModelContextWindow(s);
|
|
1857
1914
|
}
|
|
@@ -1908,6 +1965,23 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1908
1965
|
if (!s.errors)
|
|
1909
1966
|
s.errors = [`Anthropic API error: ${apiErrorReason}`];
|
|
1910
1967
|
}
|
|
1968
|
+
// Limit-notice arbitration (see resolveClaudeTuiLimitOutcome). Covers the
|
|
1969
|
+
// paths the stall watchdog never reaches: the TUI painted a limit banner,
|
|
1970
|
+
// then Stop fired on an empty turn or the process exited — nothing
|
|
1971
|
+
// substantive ever followed the banner, so the limit ate the turn. A banner
|
|
1972
|
+
// followed by real output stays informational (already in the activity log).
|
|
1973
|
+
if (!interrupted && !timedOut && !s.errors) {
|
|
1974
|
+
const limitOutcome = resolveClaudeTuiLimitOutcome({
|
|
1975
|
+
noticeText: terminalLimitNotice,
|
|
1976
|
+
noticeAt: terminalLimitNoticeAt,
|
|
1977
|
+
lastSubstantiveEventAt: Math.max(lastAssistantEventAt, lastToolEventAt, lastSidecarEventAt),
|
|
1978
|
+
hasOutputText: !!s.text.trim(),
|
|
1979
|
+
});
|
|
1980
|
+
if (limitOutcome === 'fatal') {
|
|
1981
|
+
s.stopReason = 'rate_limit';
|
|
1982
|
+
s.errors = [terminalLimitNotice];
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1911
1985
|
const errorText = joinErrorMessages(s.errors);
|
|
1912
1986
|
// "ok" requires: process exited cleanly (or via our own SIGTERM after Stop
|
|
1913
1987
|
// hook fired, which yields a non-zero exit), no errors from the parser, no
|
package/package.json
CHANGED