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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pikiclaw",
3
- "version": "0.3.69",
3
+ "version": "0.3.70",
4
4
  "description": "Put the world's smartest AI agents in your pocket. Command local Claude & Gemini via IM. | 让最好用的 IM 变成你电脑上的顶级 Agent 控制台",
5
5
  "type": "module",
6
6
  "bin": {