pikiclaw 0.3.62 → 0.3.63
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.
|
@@ -43,8 +43,8 @@ import { tmpdir } from 'node:os';
|
|
|
43
43
|
import { Q, agentLog, agentWarn, buildStreamPreviewMeta, computeContext, joinErrorMessages, emitSessionIdUpdate, normalizeClaudeModelId, pushRecentActivity, summarizeClaudeToolUse, summarizeClaudeToolResult, previewToolCallInput, previewToolCallResult, detectClaudeApiError, } from '../utils.js';
|
|
44
44
|
import { encodePathAsDirName, getHome, whichSync } from '../../core/platform.js';
|
|
45
45
|
import { stripAnsiEscapes } from '../../core/utils.js';
|
|
46
|
-
import { AGENT_STREAM_HARD_KILL_GRACE_MS, CLAUDE_TUI_STALL_QUIET_MS, CLAUDE_TUI_STALL_PENDING_TOOL_MS, } from '../../core/constants.js';
|
|
47
|
-
import { claudeParse, createClaudeStreamState, claudeContextWindowFromModel, claudeEffectiveContextWindow, registerClaudeBackgroundAgentLaunch, pendingClaudeBackgroundAgentCount, } from './claude.js';
|
|
46
|
+
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';
|
|
47
|
+
import { claudeParse, createClaudeStreamState, claudeContextWindowFromModel, claudeEffectiveContextWindow, registerClaudeBackgroundAgentLaunch, pendingClaudeBackgroundAgentCount, registerClaudeBackgroundBashLaunch, pendingClaudeBackgroundBashCount, extractClaudeBackgroundTaskId, } from './claude.js';
|
|
48
48
|
async function loadPty() {
|
|
49
49
|
// Dynamic import keeps node-pty an optional dependency — if it's not
|
|
50
50
|
// installed the print-mode dispatcher in claude.ts will catch the throw
|
|
@@ -391,6 +391,12 @@ export function applyHookToolEvent(ev, s) {
|
|
|
391
391
|
s.claudeToolsById.set(toolUseId, { name: toolName, summary: desc || kind || 'Sub-agent' });
|
|
392
392
|
return true;
|
|
393
393
|
}
|
|
394
|
+
// Background Bash — register like a backgrounded agent so the turn's Stop
|
|
395
|
+
// holds the PTY open until its <task-notification> lands, instead of
|
|
396
|
+
// SIGTERMing the still-running command (and its future report-back turn).
|
|
397
|
+
if (toolName === 'Bash' && ev.tool_input?.run_in_background === true) {
|
|
398
|
+
registerClaudeBackgroundBashLaunch(s, toolUseId);
|
|
399
|
+
}
|
|
394
400
|
const summary = summarizeClaudeToolUse(toolName, ev.tool_input || {});
|
|
395
401
|
pushRecentActivity(s.recentActivity, summary);
|
|
396
402
|
s.seenClaudeToolIds.add(toolUseId);
|
|
@@ -457,6 +463,14 @@ export function applyHookToolEvent(ev, s) {
|
|
|
457
463
|
s.activity = s.recentActivity.join('\n');
|
|
458
464
|
}
|
|
459
465
|
}
|
|
466
|
+
// Background Bash launch ack → map task id → tool_use for notification
|
|
467
|
+
// resolution (bash notifications usually omit <tool-use-id>).
|
|
468
|
+
if (toolName === 'Bash' && s.bgBashToolUseIds?.has(toolUseId)
|
|
469
|
+
&& !s.bgAgentCompletedToolUseIds?.has(toolUseId)) {
|
|
470
|
+
const taskId = extractClaudeBackgroundTaskId(ev.tool_response);
|
|
471
|
+
if (taskId && !s.bgTaskIdToToolUse.has(taskId))
|
|
472
|
+
s.bgTaskIdToToolUse.set(taskId, toolUseId);
|
|
473
|
+
}
|
|
460
474
|
s.seenClaudeToolResultIds.add(toolUseId);
|
|
461
475
|
return true;
|
|
462
476
|
}
|
|
@@ -630,10 +644,23 @@ const BG_RESETTLE_QUIET_MS = 30_000;
|
|
|
630
644
|
* is still expected. Hold until a fresh Stop or BG_RESETTLE_QUIET_MS of
|
|
631
645
|
* JSONL silence.
|
|
632
646
|
* - `terminate`: the Stop is the genuine end of the turn.
|
|
647
|
+
*
|
|
648
|
+
* The `hold-background` path carries a quiet-TTL: a genuinely-running
|
|
649
|
+
* background agent keeps emitting hook/sidecar/JSONL traffic, so a hold whose
|
|
650
|
+
* every channel has been silent past CLAUDE_TUI_STOP_HOLD_QUIET_TTL_MS is a
|
|
651
|
+
* phantom (lost <task-notification> / completion never observed). Releasing
|
|
652
|
+
* it as a normal Stop keeps the turn's clean semantics — letting the stall
|
|
653
|
+
* watchdog reap it instead would mislabel a finished turn 'stalled' and
|
|
654
|
+
* inject a confusing auto-resume prompt into the next turn.
|
|
633
655
|
*/
|
|
634
656
|
export function decideClaudeTuiStop(input) {
|
|
635
|
-
if (input.pendingBackgroundAgents > 0)
|
|
657
|
+
if (input.pendingBackgroundAgents > 0) {
|
|
658
|
+
const ttl = input.holdQuietTtlMs ?? CLAUDE_TUI_STOP_HOLD_QUIET_TTL_MS;
|
|
659
|
+
const lastActivityAt = Math.max(input.stoppedAt, input.lastJsonlEventAt, input.lastTaskNotificationAt, input.lastHookOrSidecarEventAt ?? 0);
|
|
660
|
+
if (input.now - lastActivityAt > ttl)
|
|
661
|
+
return 'terminate'; // 幽灵 hold:全通道静默超 TTL
|
|
636
662
|
return 'hold-background';
|
|
663
|
+
}
|
|
637
664
|
const stopIsStale = input.lastTaskNotificationAt > 0 && input.lastTaskNotificationAt >= input.stoppedAt;
|
|
638
665
|
if (stopIsStale) {
|
|
639
666
|
const quietMs = input.resettleQuietMs ?? BG_RESETTLE_QUIET_MS;
|
|
@@ -656,8 +683,22 @@ export function decideClaudeTuiStop(input) {
|
|
|
656
683
|
* the freeze can also hit mid-execution, but a legitimately long foreground
|
|
657
684
|
* command must not get shot — claude's own Bash timeout fires PostToolUse
|
|
658
685
|
* well inside CLAUDE_TUI_STALL_PENDING_TOOL_MS.
|
|
686
|
+
*
|
|
687
|
+
* Fast path: `lastPtyDataAt` is raw PTY output (any repaint frame counts). A
|
|
688
|
+
* healthy TUI animates continuously mid-turn — spinner, stream ticks, status
|
|
689
|
+
* line — so PTY byte-silence is the cheapest possible "event loop is dead"
|
|
690
|
+
* detector. When BOTH the PTY and all structured signals have been silent
|
|
691
|
+
* past `ptyDeadMs`, declare the stall immediately instead of waiting out the
|
|
692
|
+
* 10/30-minute quiet thresholds. Long thinking and long foreground commands
|
|
693
|
+
* keep painting frames, which routes them to the slow thresholds as before.
|
|
659
694
|
*/
|
|
660
695
|
export function decideClaudeTuiStall(input) {
|
|
696
|
+
const ptyAt = input.lastPtyDataAt ?? 0;
|
|
697
|
+
if (ptyAt > 0) {
|
|
698
|
+
const ptyDeadMs = input.ptyDeadMs ?? CLAUDE_TUI_STALL_PTY_DEAD_MS;
|
|
699
|
+
if (input.now - Math.max(ptyAt, input.lastProgressAt) > ptyDeadMs)
|
|
700
|
+
return 'stall';
|
|
701
|
+
}
|
|
661
702
|
const threshold = input.pendingToolCount > 0
|
|
662
703
|
? (input.pendingToolMs ?? CLAUDE_TUI_STALL_PENDING_TOOL_MS)
|
|
663
704
|
: (input.quietMs ?? CLAUDE_TUI_STALL_QUIET_MS);
|
|
@@ -934,9 +975,15 @@ export async function doClaudeTuiStream(opts) {
|
|
|
934
975
|
}
|
|
935
976
|
agentLog(`[claude-tui] pid=${proc.pid}`);
|
|
936
977
|
const dbg = process.env.PIKICLAW_CLAUDE_TUI_DEBUG === '1';
|
|
978
|
+
/** Wall-clock of the last raw PTY byte — stall watchdog fast-path signal. */
|
|
979
|
+
let lastPtyDataAt = Date.now();
|
|
937
980
|
proc.onData((data) => {
|
|
938
981
|
// We deliberately do not parse the TUI screen output. The JSONL is the
|
|
939
982
|
// canonical source of structured events. Stash bytes only when debugging.
|
|
983
|
+
// Raw byte arrival doubles as the cheapest liveness signal: a healthy TUI
|
|
984
|
+
// repaints continuously mid-turn, so PTY silence = event loop dead — feeds
|
|
985
|
+
// the stall watchdog's fast path (decideClaudeTuiStall.lastPtyDataAt).
|
|
986
|
+
lastPtyDataAt = Date.now();
|
|
940
987
|
if (dbg) {
|
|
941
988
|
try {
|
|
942
989
|
fs.appendFileSync(ptyLogPath, data);
|
|
@@ -1007,6 +1054,8 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1007
1054
|
let lastToolEventAt = start;
|
|
1008
1055
|
let lastSidecarEventAt = 0;
|
|
1009
1056
|
let stallKilled = false;
|
|
1057
|
+
/** Last state.stoppedAt for which pendingHookToolIds was reconciled. */
|
|
1058
|
+
let lastClearedStopAt = 0;
|
|
1010
1059
|
/** Hook-reported tools still executing: PreToolUse seen, no PostToolUse. */
|
|
1011
1060
|
const pendingHookToolIds = new Set();
|
|
1012
1061
|
// Append-only tool-events log fed by PreToolUse / PostToolUse hooks. We
|
|
@@ -1264,17 +1313,40 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1264
1313
|
// has reported its <task-notification> AND the latest Stop is fresher than
|
|
1265
1314
|
// the latest notification (i.e. the model's wrap-up segment finished).
|
|
1266
1315
|
if (state.stoppedAt && !stopHookFired) {
|
|
1316
|
+
// A fired Stop means no foreground tool is genuinely mid-flight any
|
|
1317
|
+
// more. Surviving entries in pendingHookToolIds are lost PostToolUse
|
|
1318
|
+
// hook events (MCP flap / hook timeout ate them) — clearing here stops
|
|
1319
|
+
// them from silently pushing the stall watchdog onto the 30-minute
|
|
1320
|
+
// pending-tool threshold for the rest of the turn.
|
|
1321
|
+
if (state.stoppedAt !== lastClearedStopAt) {
|
|
1322
|
+
lastClearedStopAt = state.stoppedAt;
|
|
1323
|
+
if (pendingHookToolIds.size) {
|
|
1324
|
+
agentWarn(`[claude-tui] Stop fired with ${pendingHookToolIds.size} unmatched PreToolUse event(s) — clearing (lost PostToolUse hooks)`);
|
|
1325
|
+
pendingHookToolIds.clear();
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1267
1328
|
const pendingBg = pendingClaudeBackgroundAgentCount(s);
|
|
1268
1329
|
const decision = decideClaudeTuiStop({
|
|
1269
1330
|
stoppedAt: state.stoppedAt,
|
|
1270
1331
|
pendingBackgroundAgents: pendingBg,
|
|
1271
1332
|
lastTaskNotificationAt: s.lastTaskNotificationAt || 0,
|
|
1272
1333
|
lastJsonlEventAt: lastMainJsonlEventAt,
|
|
1334
|
+
lastHookOrSidecarEventAt: Math.max(lastToolEventAt, lastSidecarEventAt),
|
|
1335
|
+
// Background *Bash* is silent by nature (no sidecar/hook traffic while
|
|
1336
|
+
// it runs) — give it the long pending-tool budget; agent-only holds
|
|
1337
|
+
// keep the default TTL (live agents emit sidecar events constantly).
|
|
1338
|
+
holdQuietTtlMs: pendingClaudeBackgroundBashCount(s) > 0
|
|
1339
|
+
? CLAUDE_TUI_STALL_PENDING_TOOL_MS
|
|
1340
|
+
: undefined,
|
|
1273
1341
|
now: Date.now(),
|
|
1274
1342
|
});
|
|
1275
1343
|
if (decision === 'terminate') {
|
|
1276
1344
|
stopHookFired = true;
|
|
1277
1345
|
stopHookSeenAt = Date.now();
|
|
1346
|
+
if (pendingBg > 0) {
|
|
1347
|
+
// 幽灵 hold 释放:计数说还有后台 agent,但所有通道静默已超 TTL。
|
|
1348
|
+
agentWarn(`[claude-tui] releasing phantom hold — ${pendingBg} background agent(s) still counted pending but every channel quiet past TTL; treating Stop as final`);
|
|
1349
|
+
}
|
|
1278
1350
|
agentLog(`[claude-tui] Stop hook fired — draining JSONL for ${POST_STOP_DRAIN_MS}ms before SIGTERM`);
|
|
1279
1351
|
}
|
|
1280
1352
|
else if (decision === 'hold-background' && pendingBg !== lastLoggedPendingBg) {
|
|
@@ -1300,19 +1372,32 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1300
1372
|
// once so the turn continues instead of spinning forever in the IM card.
|
|
1301
1373
|
if (!stopHookFired && !timedOut && !interrupted && !stallKilled) {
|
|
1302
1374
|
const lastProgressAt = Math.max(start, lastMainJsonlEventAt, lastToolEventAt, lastSidecarEventAt, state.stoppedAt || 0, state.promptSubmittedAt || 0);
|
|
1375
|
+
// Pending background work (agents + bash) extends the stall budget the
|
|
1376
|
+
// same way a pending foreground tool does: a silent 15-minute background
|
|
1377
|
+
// build must not get shot by the 10-minute quiet threshold. The PTY
|
|
1378
|
+
// fast path still catches true process freezes within minutes.
|
|
1379
|
+
const pendingBgForStall = pendingClaudeBackgroundAgentCount(s);
|
|
1380
|
+
// PTY fast path is for *mid-turn* freezes only. While the TUI idles in a
|
|
1381
|
+
// post-Stop background hold it legitimately paints nothing — a static
|
|
1382
|
+
// screen there is healthy, not frozen. Stop being the freshest signal is
|
|
1383
|
+
// exactly that hold state → disarm the fast path (0 = unavailable).
|
|
1384
|
+
const nonStopProgressAt = Math.max(start, lastMainJsonlEventAt, lastToolEventAt, lastSidecarEventAt, state.promptSubmittedAt || 0);
|
|
1385
|
+
const inPostStopHold = !!state.stoppedAt && state.stoppedAt >= nonStopProgressAt;
|
|
1303
1386
|
const stallDecision = decideClaudeTuiStall({
|
|
1304
1387
|
now: Date.now(),
|
|
1305
1388
|
lastProgressAt,
|
|
1306
|
-
pendingToolCount: pendingHookToolIds.size,
|
|
1389
|
+
pendingToolCount: pendingHookToolIds.size + pendingBgForStall,
|
|
1390
|
+
lastPtyDataAt: inPostStopHold ? 0 : lastPtyDataAt,
|
|
1307
1391
|
});
|
|
1308
1392
|
if (stallDecision === 'stall') {
|
|
1309
1393
|
stallKilled = true;
|
|
1310
1394
|
const quietMin = Math.round((Date.now() - lastProgressAt) / 60_000);
|
|
1395
|
+
const ptyQuietS = Math.round((Date.now() - lastPtyDataAt) / 1000);
|
|
1311
1396
|
s.stopReason = 'stalled';
|
|
1312
1397
|
if (!s.errors) {
|
|
1313
|
-
s.errors = [`Claude process went silent mid-turn for ${quietMin}m (no JSONL, hook, or sub-agent events) — known claude CLI freeze. Terminated for auto-resume.`];
|
|
1398
|
+
s.errors = [`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.`];
|
|
1314
1399
|
}
|
|
1315
|
-
agentWarn(`[claude-tui] stall detected: no progress for ${quietMin}m (pendingTools=${pendingHookToolIds.size}) — terminating TUI pid=${proc.pid} for auto-resume`);
|
|
1400
|
+
agentWarn(`[claude-tui] stall detected: no progress for ${quietMin}m (pendingTools=${pendingHookToolIds.size}, ptyQuiet=${ptyQuietS}s) — terminating TUI pid=${proc.pid} for auto-resume`);
|
|
1316
1401
|
pushRecentActivity(s.recentActivity, `Agent stalled (${quietMin}m silent) — restarting turn`);
|
|
1317
1402
|
s.activity = s.recentActivity.join('\n');
|
|
1318
1403
|
emit();
|
|
@@ -328,6 +328,8 @@ function ensureClaudeBgAgentState(s) {
|
|
|
328
328
|
s.bgAgentLaunchedToolUseIds = new Set();
|
|
329
329
|
if (!s.bgAgentCompletedToolUseIds)
|
|
330
330
|
s.bgAgentCompletedToolUseIds = new Set();
|
|
331
|
+
if (!s.bgBashToolUseIds)
|
|
332
|
+
s.bgBashToolUseIds = new Set();
|
|
331
333
|
if (!s.bgTaskIdToToolUse)
|
|
332
334
|
s.bgTaskIdToToolUse = new Map();
|
|
333
335
|
if (typeof s.lastTaskNotificationAt !== 'number')
|
|
@@ -341,7 +343,27 @@ export function registerClaudeBackgroundAgentLaunch(s, toolUseId) {
|
|
|
341
343
|
ensureClaudeBgAgentState(s);
|
|
342
344
|
s.bgAgentLaunchedToolUseIds.add(id);
|
|
343
345
|
}
|
|
344
|
-
/**
|
|
346
|
+
/**
|
|
347
|
+
* Record a `Bash` tool_use launched with `run_in_background: true`.
|
|
348
|
+
*
|
|
349
|
+
* Background Bash lives *inside the claude process* exactly like a
|
|
350
|
+
* backgrounded sub-agent: its tool_result is a launch ack, the real
|
|
351
|
+
* completion arrives later as a `<task-notification>` which re-invokes the
|
|
352
|
+
* model in the same process. Before this registration existed only Task/Agent
|
|
353
|
+
* launches counted as "pending background work" — a turn that backgrounded a
|
|
354
|
+
* Bash command would hit Stop, decideClaudeTuiStop saw pending=0 and
|
|
355
|
+
* terminated the PTY, killing the command and its future report-back turn
|
|
356
|
+
* (the「claude 后台任务一停止就被掐死」failure).
|
|
357
|
+
*/
|
|
358
|
+
export function registerClaudeBackgroundBashLaunch(s, toolUseId) {
|
|
359
|
+
const id = String(toolUseId || '').trim();
|
|
360
|
+
if (!id)
|
|
361
|
+
return;
|
|
362
|
+
ensureClaudeBgAgentState(s);
|
|
363
|
+
s.bgAgentLaunchedToolUseIds.add(id);
|
|
364
|
+
s.bgBashToolUseIds.add(id);
|
|
365
|
+
}
|
|
366
|
+
/** Launched background tasks (agents + bash) whose <task-notification> hasn't arrived yet. */
|
|
345
367
|
export function pendingClaudeBackgroundAgentCount(s) {
|
|
346
368
|
const launched = s?.bgAgentLaunchedToolUseIds;
|
|
347
369
|
if (!launched?.size)
|
|
@@ -354,6 +376,51 @@ export function pendingClaudeBackgroundAgentCount(s) {
|
|
|
354
376
|
}
|
|
355
377
|
return pending;
|
|
356
378
|
}
|
|
379
|
+
/** Pending background *Bash* tasks specifically. Unlike agents (whose sidecar
|
|
380
|
+
* JSONL keeps emitting events while alive), a background command is silent by
|
|
381
|
+
* nature — callers use this to pick a longer hold/stall budget. */
|
|
382
|
+
export function pendingClaudeBackgroundBashCount(s) {
|
|
383
|
+
const bash = s?.bgBashToolUseIds;
|
|
384
|
+
if (!bash?.size)
|
|
385
|
+
return 0;
|
|
386
|
+
const completed = s?.bgAgentCompletedToolUseIds;
|
|
387
|
+
let pending = 0;
|
|
388
|
+
for (const id of bash) {
|
|
389
|
+
if (!completed?.has(id))
|
|
390
|
+
pending++;
|
|
391
|
+
}
|
|
392
|
+
return pending;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Pull the background task id out of a launch ack. Claude Code's backgrounded
|
|
396
|
+
* Bash tool_result reads like "Command running in background with ID: bash_3
|
|
397
|
+
* (output: …)" — the id is what the later <task-notification> carries (its
|
|
398
|
+
* <tool-use-id> is often omitted for bash), so mapping id → tool_use here is
|
|
399
|
+
* what lets applyClaudeTaskNotification resolve the completion.
|
|
400
|
+
*/
|
|
401
|
+
export function extractClaudeBackgroundTaskId(content) {
|
|
402
|
+
let text = '';
|
|
403
|
+
if (typeof content === 'string')
|
|
404
|
+
text = content;
|
|
405
|
+
else if (Array.isArray(content)) {
|
|
406
|
+
text = content
|
|
407
|
+
.filter((b) => b?.type === 'text' && typeof b.text === 'string')
|
|
408
|
+
.map((b) => b.text)
|
|
409
|
+
.join('\n');
|
|
410
|
+
}
|
|
411
|
+
else if (content && typeof content === 'object') {
|
|
412
|
+
try {
|
|
413
|
+
text = JSON.stringify(content);
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (!text || !/background/i.test(text))
|
|
420
|
+
return null;
|
|
421
|
+
const m = text.match(/\b(?:ID|id)\s*[::]?\s*[`"']?([A-Za-z0-9][A-Za-z0-9_-]{1,63})/);
|
|
422
|
+
return m ? m[1] : null;
|
|
423
|
+
}
|
|
357
424
|
/**
|
|
358
425
|
* Parse a `<task-notification>` wrapper out of a user event's content.
|
|
359
426
|
* Shape (observed, Claude Code 2.x):
|
|
@@ -583,6 +650,12 @@ export function claudeParse(ev, s) {
|
|
|
583
650
|
s.claudeToolsById.set(toolId, { name: toolName, summary: subAgent.description || 'Run task' });
|
|
584
651
|
continue;
|
|
585
652
|
}
|
|
653
|
+
// Background Bash — same in-process lifecycle as a backgrounded agent:
|
|
654
|
+
// launch ack now, <task-notification> later. Register so the TUI driver
|
|
655
|
+
// holds the PTY open instead of SIGTERMing the command mid-flight.
|
|
656
|
+
if (toolName === 'Bash' && block?.input?.run_in_background === true) {
|
|
657
|
+
registerClaudeBackgroundBashLaunch(s, toolId);
|
|
658
|
+
}
|
|
586
659
|
const tool = {
|
|
587
660
|
name: toolName,
|
|
588
661
|
summary: summarizeClaudeToolUse(block?.name, block?.input || {}),
|
|
@@ -667,6 +740,15 @@ export function claudeParse(ev, s) {
|
|
|
667
740
|
tool.result = previewToolCallResult(block?.content);
|
|
668
741
|
tool.status = block?.is_error ? 'failed' : 'done';
|
|
669
742
|
}
|
|
743
|
+
// Background Bash launch ack → map its task id to the tool_use so the
|
|
744
|
+
// later <task-notification> (which usually omits <tool-use-id> for bash)
|
|
745
|
+
// can resolve and decrement the pending count.
|
|
746
|
+
if (tool?.name === 'Bash' && s.bgBashToolUseIds?.has(toolId)
|
|
747
|
+
&& !s.bgAgentCompletedToolUseIds?.has(toolId)) {
|
|
748
|
+
const taskId = extractClaudeBackgroundTaskId(block?.content);
|
|
749
|
+
if (taskId && !s.bgTaskIdToToolUse.has(taskId))
|
|
750
|
+
s.bgTaskIdToToolUse.set(taskId, toolId);
|
|
751
|
+
}
|
|
670
752
|
pushRecentActivity(s.recentActivity, summarizeClaudeToolResult(tool, block, ev.tool_use_result));
|
|
671
753
|
// MCP / Skill tool_result with multimodal content — recurse for image
|
|
672
754
|
// entries so the final StreamResult carries them. Filesystem-reading
|
package/dist/core/constants.js
CHANGED
|
@@ -306,6 +306,29 @@ export const CLAUDE_TUI_STALL_QUIET_MS = 10 * 60_000;
|
|
|
306
306
|
* silent for this long means the freeze hit mid-execution.
|
|
307
307
|
*/
|
|
308
308
|
export const CLAUDE_TUI_STALL_PENDING_TOOL_MS = 30 * 60_000;
|
|
309
|
+
/**
|
|
310
|
+
* Fast-path stall: a healthy claude TUI repaints continuously while a turn is
|
|
311
|
+
* in flight (spinner frames, stream ticks, status line) — the PTY never goes
|
|
312
|
+
* byte-silent for minutes. If NO PTY output arrives for this long AND every
|
|
313
|
+
* structured signal is equally quiet, the process event loop itself is gone
|
|
314
|
+
* (the 2.1.160 mid-turn freeze: attachment lands → next API call never
|
|
315
|
+
* assembles). Declare the stall now instead of waiting out the 10/30-minute
|
|
316
|
+
* quiet thresholds — turns a 10-30 分钟「卡死」into a ~3 分钟自愈。
|
|
317
|
+
* False-positive safe: long thinking / long Bash keep painting frames, which
|
|
318
|
+
* refreshes the PTY signal and defers this path to the slow thresholds.
|
|
319
|
+
*/
|
|
320
|
+
export const CLAUDE_TUI_STALL_PTY_DEAD_MS = 3 * 60_000;
|
|
321
|
+
/**
|
|
322
|
+
* TTL for the post-Stop `hold-background` path. The hold protects
|
|
323
|
+
* run_in_background agents living inside the claude process — but a live
|
|
324
|
+
* agent keeps emitting hook/sidecar/JSONL traffic. If the hold sees no
|
|
325
|
+
* activity on ANY channel for this long, the pending count is phantom (lost
|
|
326
|
+
* <task-notification>, agents already finished): release as a NORMAL Stop.
|
|
327
|
+
* Without this TTL the stall watchdog eventually fires instead, mislabels the
|
|
328
|
+
* cleanly-finished turn 'stalled', and injects a confusing auto-resume prompt
|
|
329
|
+
* (the「回合明明答完了还被注入 Continue」symptom).
|
|
330
|
+
*/
|
|
331
|
+
export const CLAUDE_TUI_STOP_HOLD_QUIET_TTL_MS = 10 * 60_000;
|
|
309
332
|
/** Codex-specific grace period added to the user-configured timeout. */
|
|
310
333
|
export const CODEX_STREAM_HARD_KILL_GRACE_MS = 5_000;
|
|
311
334
|
/**
|
package/package.json
CHANGED