pikiclaw 0.3.69 → 0.3.70
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.
|
@@ -356,6 +356,35 @@ export function detectClaudeBypassPrompt(screen) {
|
|
|
356
356
|
&& t.includes('yes,iaccept')
|
|
357
357
|
&& t.includes('no,exit');
|
|
358
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* Capture-only classifier for the stall watchdog. When the turn goes quiet we
|
|
361
|
+
* cannot tell from timing alone whether the TUI is (a) frozen mid-turn (the
|
|
362
|
+
* known CLI bug — PTY dead), (b) just thinking for a long time (PTY repaints a
|
|
363
|
+
* spinner), or (c) blocked on an interactive prompt that bypass mode does NOT
|
|
364
|
+
* suppress and that's waiting for input it will never get (trust-a-new-folder,
|
|
365
|
+
* a "Do you want to proceed?" confirmation, an expired-login prompt, …). The
|
|
366
|
+
* raw PTY screen is the only thing that disambiguates them, and we don't
|
|
367
|
+
* otherwise persist it — so on a stall we record a compact stripped sample plus
|
|
368
|
+
* a conservative "looks like an interactive prompt" flag. Changes no control
|
|
369
|
+
* flow; it exists purely to make the next stall diagnosable from data.
|
|
370
|
+
*/
|
|
371
|
+
export function classifyStallScreen(screen) {
|
|
372
|
+
if (typeof screen !== 'string' || !screen)
|
|
373
|
+
return { looksLikePrompt: false, sample: '' };
|
|
374
|
+
const stripped = stripAnsiEscapes(screen);
|
|
375
|
+
const sample = stripped.replace(/\s+/g, ' ').trim().slice(-400);
|
|
376
|
+
// Claude positions words with cursor moves, so the live screen is spaceless;
|
|
377
|
+
// match against the despaced form (see detectClaudeBypassPrompt).
|
|
378
|
+
const ds = stripped.replace(/\s+/g, '').toLowerCase();
|
|
379
|
+
const looksLikePrompt = ds.includes('esctocancel') // claude's confirm-dialog footer ("Enter to confirm · Esc to cancel")
|
|
380
|
+
|| ds.includes('doyouwant')
|
|
381
|
+
|| ds.includes('wouldyoulike')
|
|
382
|
+
|| ds.includes('trustthisfolder')
|
|
383
|
+
|| ds.includes('yes,iaccept')
|
|
384
|
+
|| ds.includes('(y/n)')
|
|
385
|
+
|| (ds.includes('❯') && ds.includes('1.') && ds.includes('2.')); // numbered select with cursor
|
|
386
|
+
return { looksLikePrompt, sample };
|
|
387
|
+
}
|
|
359
388
|
/**
|
|
360
389
|
* Extract text / thinking blocks from an assistant JSONL event and route them:
|
|
361
390
|
* text → the chunked stream buffer (slow drain), thinking → `s.thinking`
|
|
@@ -1601,6 +1630,9 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1601
1630
|
stallDiagPtyAliveWhileQuiet = true;
|
|
1602
1631
|
if (nowMs - lastStallDiagHeartbeatAt >= STALL_DIAG_HEARTBEAT_INTERVAL_MS) {
|
|
1603
1632
|
lastStallDiagHeartbeatAt = nowMs;
|
|
1633
|
+
// Snapshot the screen so a quiet stretch can later be classified as
|
|
1634
|
+
// a frozen stream vs a long think vs a blocking interactive prompt.
|
|
1635
|
+
const screenInfo = classifyStallScreen(screenTail);
|
|
1604
1636
|
writeStallDiag({
|
|
1605
1637
|
kind: 'quiet',
|
|
1606
1638
|
sessionId: activeSessionId,
|
|
@@ -1616,6 +1648,8 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1616
1648
|
pendingHookTools: pendingHookToolIds.size,
|
|
1617
1649
|
pendingBgAgents: pendingBgForStall,
|
|
1618
1650
|
pendingBgBash: pendingClaudeBackgroundBashCount(s),
|
|
1651
|
+
looksLikePrompt: screenInfo.looksLikePrompt,
|
|
1652
|
+
screenSample: screenInfo.sample,
|
|
1619
1653
|
});
|
|
1620
1654
|
}
|
|
1621
1655
|
}
|
|
@@ -1631,6 +1665,7 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1631
1665
|
const quietMin = Math.round((Date.now() - lastProgressAt) / 60_000);
|
|
1632
1666
|
const ptyQuietS = Math.round((Date.now() - lastPtyDataAt) / 1000);
|
|
1633
1667
|
s.stopReason = 'stalled';
|
|
1668
|
+
const stallScreen = classifyStallScreen(screenTail);
|
|
1634
1669
|
writeStallDiag({
|
|
1635
1670
|
kind: 'stall',
|
|
1636
1671
|
sessionId: activeSessionId,
|
|
@@ -1645,6 +1680,11 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1645
1680
|
lastJsonlType: lastMainJsonlType,
|
|
1646
1681
|
pendingHookTools: pendingHookToolIds.size,
|
|
1647
1682
|
pendingBgAgents: pendingBgForStall,
|
|
1683
|
+
// looksLikePrompt=true here is the signal that the "stall" was really
|
|
1684
|
+
// a blocking interactive prompt waiting for input bypass can't skip —
|
|
1685
|
+
// the mid-turn dialog-hang hypothesis, confirmable from screenSample.
|
|
1686
|
+
looksLikePrompt: stallScreen.looksLikePrompt,
|
|
1687
|
+
screenSample: stallScreen.sample,
|
|
1648
1688
|
});
|
|
1649
1689
|
if (!s.errors) {
|
|
1650
1690
|
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.`];
|
|
@@ -276,12 +276,6 @@ function renderSubAgentsForPreview(meta) {
|
|
|
276
276
|
}
|
|
277
277
|
return lines.join('\n');
|
|
278
278
|
}
|
|
279
|
-
/** After this much wall-clock, a still-running turn shows a text-only "still
|
|
280
|
-
* working" banner (see StreamPreviewData.longRunHint) so a long silent
|
|
281
|
-
* operation (held background task, slow command) doesn't read as a frozen
|
|
282
|
-
* card. Deliberately above the chunked-stream cadence so quick turns never
|
|
283
|
-
* flash it. */
|
|
284
|
-
const LONG_RUN_HINT_AFTER_MS = 60_000;
|
|
285
279
|
export function extractStreamPreviewData(input) {
|
|
286
280
|
const maxBody = 2400;
|
|
287
281
|
const display = input.bodyText.trim();
|
|
@@ -299,13 +293,6 @@ export function extractStreamPreviewData(input) {
|
|
|
299
293
|
// freshly-opened card doesn't flash "0s".
|
|
300
294
|
const elapsedMs = Math.max(0, input.elapsedMs);
|
|
301
295
|
const thinkingProgressText = elapsedMs >= 1000 ? fmtCompactUptime(elapsedMs) : null;
|
|
302
|
-
// After a turn has run a while, a long silent operation (a held background
|
|
303
|
-
// task, a slow command) can make the card look frozen. Surface a text-only
|
|
304
|
-
// "still working" line so the user knows it's alive and can switch away. No
|
|
305
|
-
// elapsed time here — the footer keeps the single clock, so no second timer.
|
|
306
|
-
const longRunHint = elapsedMs >= LONG_RUN_HINT_AFTER_MS
|
|
307
|
-
? '⏳ Still working — the result will update in this card'
|
|
308
|
-
: null;
|
|
309
296
|
return {
|
|
310
297
|
display,
|
|
311
298
|
rawThinking,
|
|
@@ -318,6 +305,5 @@ export function extractStreamPreviewData(input) {
|
|
|
318
305
|
thinkSnippet,
|
|
319
306
|
preview,
|
|
320
307
|
thinkingProgressText,
|
|
321
|
-
longRunHint,
|
|
322
308
|
};
|
|
323
309
|
}
|
|
@@ -246,8 +246,6 @@ function buildPreviewMarkdown(input, options) {
|
|
|
246
246
|
// heartbeat, so the card still visibly advances.
|
|
247
247
|
parts.push(`**${data.label}**`);
|
|
248
248
|
}
|
|
249
|
-
if (data.longRunHint)
|
|
250
|
-
parts.push(data.longRunHint);
|
|
251
249
|
if (options?.includeFooter !== false) {
|
|
252
250
|
parts.push(formatPreviewFooter(input.agent, input.elapsedMs, input.meta ?? null, {
|
|
253
251
|
model: input.model,
|
|
@@ -345,8 +345,6 @@ export function buildStreamPreviewHtml(input) {
|
|
|
345
345
|
// heartbeat, so the card still visibly advances.
|
|
346
346
|
parts.push(`<blockquote><b>${escapeHtml(data.label)}</b></blockquote>`);
|
|
347
347
|
}
|
|
348
|
-
if (data.longRunHint)
|
|
349
|
-
parts.push(`<i>${escapeHtml(data.longRunHint)}</i>`);
|
|
350
348
|
parts.push(formatPreviewFooterHtml(input.agent, input.elapsedMs, input.meta ?? null, {
|
|
351
349
|
model: input.model,
|
|
352
350
|
effort: input.effort,
|
package/package.json
CHANGED