pikiloom 0.4.8 → 0.4.9
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.
|
@@ -916,6 +916,20 @@ export function decideClaudeTuiStop(input) {
|
|
|
916
916
|
}
|
|
917
917
|
return 'terminate';
|
|
918
918
|
}
|
|
919
|
+
/**
|
|
920
|
+
* True for the `im_ask_user` MCP tool in any form — Claude Code names MCP tools
|
|
921
|
+
* `mcp__<server>__<tool>` (here `mcp__pikiloom__im_ask_user`), so match the
|
|
922
|
+
* namespaced suffix as well as the bare name (robust to the server name
|
|
923
|
+
* changing). The driver tails Pre/PostToolUse hooks by id but must also know
|
|
924
|
+
* *which* pending tool is the one that blocks on the user, so the stall
|
|
925
|
+
* watchdog can stay armed for every other tool and disarm only for a live
|
|
926
|
+
* question. See decideClaudeTuiStall(`awaitingUserReply`).
|
|
927
|
+
*/
|
|
928
|
+
export function isAskUserToolName(name) {
|
|
929
|
+
if (typeof name !== 'string' || !name)
|
|
930
|
+
return false;
|
|
931
|
+
return name === 'im_ask_user' || name.endsWith('__im_ask_user');
|
|
932
|
+
}
|
|
919
933
|
/**
|
|
920
934
|
* Decide whether the turn has gone dead. claude CLI is known to freeze
|
|
921
935
|
* mid-turn (observed 2026-06-02 on 2.1.160): after a tool_result lands the
|
|
@@ -937,8 +951,20 @@ export function decideClaudeTuiStop(input) {
|
|
|
937
951
|
* past `ptyDeadMs`, declare the stall immediately instead of waiting out the
|
|
938
952
|
* 10/30-minute quiet thresholds. Long thinking and long foreground commands
|
|
939
953
|
* keep painting frames, which routes them to the slow thresholds as before.
|
|
954
|
+
*
|
|
955
|
+
* `awaitingUserReply` is the one state that is quiet BY DESIGN, not because the
|
|
956
|
+
* process froze: an `im_ask_user` MCP call is in flight (PreToolUse seen, no
|
|
957
|
+
* PostToolUse) and the turn is blocked on the user's reply — the `/ask-user`
|
|
958
|
+
* callback never times out. Every signal (JSONL, hooks, PTY) goes silent while
|
|
959
|
+
* the user thinks, which is indistinguishable by timing alone from a freeze, so
|
|
960
|
+
* the watchdog must never stall here. The hard turn deadline remains the
|
|
961
|
+
* backstop if the user never answers; PostToolUse re-arms the watchdog the
|
|
962
|
+
* instant the answer lands. Without this, the last question regularly gets
|
|
963
|
+
* SIGTERMed mid-wait and mislabelled a "CLI freeze".
|
|
940
964
|
*/
|
|
941
965
|
export function decideClaudeTuiStall(input) {
|
|
966
|
+
if (input.awaitingUserReply)
|
|
967
|
+
return 'wait';
|
|
942
968
|
const ptyAt = input.lastPtyDataAt ?? 0;
|
|
943
969
|
if (ptyAt > 0) {
|
|
944
970
|
const ptyDeadMs = input.ptyDeadMs ?? CLAUDE_TUI_STALL_PTY_DEAD_MS;
|
|
@@ -1557,6 +1583,13 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1557
1583
|
let lastClearedStopAt = 0;
|
|
1558
1584
|
/** Hook-reported tools still executing: PreToolUse seen, no PostToolUse. */
|
|
1559
1585
|
const pendingHookToolIds = new Set();
|
|
1586
|
+
/** Subset of pendingHookToolIds that are `im_ask_user` calls — the turn is
|
|
1587
|
+
* intentionally blocked on the user's reply (the /ask-user callback never
|
|
1588
|
+
* times out), so the stall watchdog must stay disarmed while any are in
|
|
1589
|
+
* flight. Cleared by the answer (PostToolUse) or the turn ending (Stop). */
|
|
1590
|
+
const pendingAskUserToolIds = new Set();
|
|
1591
|
+
/** Edge-logged so the "watchdog disarmed" line fires on transitions only. */
|
|
1592
|
+
let loggedAwaitingUser = false;
|
|
1560
1593
|
// Incremental main-JSONL drain — the canonical text/thinking/usage feed.
|
|
1561
1594
|
// Used by both the 200ms poll tick and the post-exit final drain. Returns
|
|
1562
1595
|
// true when any line was consumed so callers can emit().
|
|
@@ -1643,10 +1676,17 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1643
1676
|
lastToolEventAt = Date.now();
|
|
1644
1677
|
const hookToolId = typeof ev?.tool_use_id === 'string' ? ev.tool_use_id : '';
|
|
1645
1678
|
if (hookToolId) {
|
|
1646
|
-
if (ev?.event === 'PreToolUse')
|
|
1679
|
+
if (ev?.event === 'PreToolUse') {
|
|
1647
1680
|
pendingHookToolIds.add(hookToolId);
|
|
1648
|
-
|
|
1681
|
+
// im_ask_user blocks the turn on the user — track it separately so the
|
|
1682
|
+
// stall watchdog can tell an intentional question-wait from a freeze.
|
|
1683
|
+
if (isAskUserToolName(ev?.tool_name))
|
|
1684
|
+
pendingAskUserToolIds.add(hookToolId);
|
|
1685
|
+
}
|
|
1686
|
+
else if (ev?.event === 'PostToolUse') {
|
|
1649
1687
|
pendingHookToolIds.delete(hookToolId);
|
|
1688
|
+
pendingAskUserToolIds.delete(hookToolId);
|
|
1689
|
+
}
|
|
1650
1690
|
}
|
|
1651
1691
|
// A Task PreToolUse and the first sub-agent tool PreToolUse can land in
|
|
1652
1692
|
// the same tick batch. If the sub-agent's hook arrives before we've
|
|
@@ -1866,6 +1906,8 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1866
1906
|
agentWarn(`[claude-tui] Stop fired with ${pendingHookToolIds.size} unmatched PreToolUse event(s) — clearing (lost PostToolUse hooks)`);
|
|
1867
1907
|
pendingHookToolIds.clear();
|
|
1868
1908
|
}
|
|
1909
|
+
// A fired Stop ends the turn — any tracked ask-user is moot.
|
|
1910
|
+
pendingAskUserToolIds.clear();
|
|
1869
1911
|
}
|
|
1870
1912
|
const pendingBg = pendingClaudeBackgroundAgentCount(s);
|
|
1871
1913
|
const decision = decideClaudeTuiStop({
|
|
@@ -1925,6 +1967,19 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1925
1967
|
// exactly that hold state → disarm the fast path (0 = unavailable).
|
|
1926
1968
|
const nonStopProgressAt = Math.max(start, lastMainJsonlEventAt, lastToolEventAt, lastSidecarEventAt, state.promptSubmittedAt || 0);
|
|
1927
1969
|
const inPostStopHold = !!state.stoppedAt && state.stoppedAt >= nonStopProgressAt;
|
|
1970
|
+
// An im_ask_user call is in flight → the turn is blocked on the user's
|
|
1971
|
+
// reply, not frozen (the /ask-user callback never times out). Disarm the
|
|
1972
|
+
// stall watchdog AND the freeze diagnostics until the user answers
|
|
1973
|
+
// (PostToolUse clears the id). Otherwise the quiet wait trips
|
|
1974
|
+
// decideClaudeTuiStall, classifies 'unknown', and gets SIGTERMed +
|
|
1975
|
+
// mislabelled a "CLI freeze" — the last-question hang the user reported.
|
|
1976
|
+
const awaitingUserReply = pendingAskUserToolIds.size > 0;
|
|
1977
|
+
if (awaitingUserReply !== loggedAwaitingUser) {
|
|
1978
|
+
loggedAwaitingUser = awaitingUserReply;
|
|
1979
|
+
if (awaitingUserReply) {
|
|
1980
|
+
agentLog(`[claude-tui] im_ask_user in flight — stall watchdog disarmed until the user replies pid=${proc.pid}`);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1928
1983
|
// Chokepoint answer-retry grace. We sent the affirmative key at a stuck dialog; give it time
|
|
1929
1984
|
// to clear. If the screen is no longer a blocking dialog, the answer took — disarm and re-arm
|
|
1930
1985
|
// so a later prompt in the same turn can also get a chokepoint retry. If it is STILL a dialog
|
|
@@ -1948,7 +2003,7 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1948
2003
|
if (!stallKilled) {
|
|
1949
2004
|
const nowMs = Date.now();
|
|
1950
2005
|
const quietMs = nowMs - lastProgressAt;
|
|
1951
|
-
if (quietMs >= STALL_DIAG_QUIET_THRESHOLD_MS && !inPostStopHold) {
|
|
2006
|
+
if (quietMs >= STALL_DIAG_QUIET_THRESHOLD_MS && !inPostStopHold && !awaitingUserReply) {
|
|
1952
2007
|
const ptyQuietMs = nowMs - lastPtyDataAt;
|
|
1953
2008
|
stallDiagWentQuiet = true;
|
|
1954
2009
|
if (quietMs > stallDiagMaxQuietMs)
|
|
@@ -1992,6 +2047,7 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1992
2047
|
now: Date.now(),
|
|
1993
2048
|
lastProgressAt,
|
|
1994
2049
|
pendingToolCount: pendingHookToolIds.size + pendingBgForStall,
|
|
2050
|
+
awaitingUserReply,
|
|
1995
2051
|
lastPtyDataAt: inPostStopHold ? 0 : lastPtyDataAt,
|
|
1996
2052
|
});
|
|
1997
2053
|
if (stallDecision === 'stall') {
|