pikiclaw 0.3.82 → 0.3.84

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.
@@ -40,11 +40,11 @@ import fs from 'node:fs';
40
40
  import path from 'node:path';
41
41
  import { randomUUID } from 'node:crypto';
42
42
  import { tmpdir } from 'node:os';
43
- import { Q, agentLog, agentWarn, buildStreamPreviewMeta, computeContext, joinErrorMessages, emitSessionIdUpdate, normalizeClaudeModelId, pushRecentActivity, summarizeClaudeToolUse, summarizeClaudeToolResult, previewToolCallInput, previewToolCallResult, detectClaudeApiError, } from '../utils.js';
43
+ import { Q, agentLog, agentWarn, buildStreamPreviewMeta, computeContext, joinErrorMessages, emitSessionIdUpdate, normalizeClaudeModelId, pushRecentActivity, summarizeClaudeToolUse, summarizeClaudeToolResult, previewToolCallInput, previewToolCallResult, detectClaudeApiError, detectClaudeModelError, claudeModelErrorMessage, } from '../utils.js';
44
44
  import { encodePathAsDirName, getHome, whichSync } from '../../core/platform.js';
45
45
  import { createRetainedLogSink } from '../../core/logging.js';
46
46
  import { stripAnsiEscapes } from '../../core/utils.js';
47
- 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 { 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, CLAUDE_TUI_MODEL_ERROR_SETTLE_MS, } from '../../core/constants.js';
48
48
  import { claudeParse, createClaudeStreamState, claudeContextWindowFromModel, claudeEffectiveContextWindow, registerClaudeBackgroundAgentLaunch, pendingClaudeBackgroundAgentCount, registerClaudeBackgroundBashLaunch, pendingClaudeBackgroundBashCount, extractClaudeBackgroundTaskId, extractClaudeWorkflowRunId, claudeEffortAndWorkflowArgs, scrubClaudeSessionContextEnv, } from './claude.js';
49
49
  // ---------------------------------------------------------------------------
50
50
  // Stall diagnostics (capture-only)
@@ -1080,6 +1080,7 @@ export async function doClaudeTuiStream(opts) {
1080
1080
  let exitSignal = null;
1081
1081
  let terminalLimitNotice = null;
1082
1082
  let terminalLimitNoticeAt = 0;
1083
+ let terminalModelError = null;
1083
1084
  let proc;
1084
1085
  const emit = () => {
1085
1086
  try {
@@ -1116,6 +1117,36 @@ export async function doClaudeTuiStream(opts) {
1116
1117
  s.activity = s.recentActivity.join('\n');
1117
1118
  emit();
1118
1119
  };
1120
+ // Selected-model-unavailable notice (404 model_not_found). Unlike the limit
1121
+ // banner this is terminal AND invisible to every structured signal: the TUI
1122
+ // paints it to the PTY screen, writes nothing to the JSONL, and fires no Stop
1123
+ // hook — so the turn would otherwise idle at the REPL until the 3–10 min stall
1124
+ // watchdog kills it with a misleading "CLI freeze" message. We surface the
1125
+ // real reason and end the turn now. The banner is still EVIDENCE, not a bare
1126
+ // verdict: a short settle confirms nothing substantive followed (cross-
1127
+ // validating the screen scrape) before we kill — never granting a lone text
1128
+ // match the authority to shoot a healthy turn.
1129
+ const noteTerminalModelError = (notice) => {
1130
+ if (terminalModelError)
1131
+ return;
1132
+ terminalModelError = notice;
1133
+ agentWarn(`[claude-tui] model unavailable observed (settling before terminate): ${notice}`);
1134
+ pushRecentActivity(s.recentActivity, notice);
1135
+ s.activity = s.recentActivity.join('\n');
1136
+ emit();
1137
+ setTimeout(() => {
1138
+ if (processExited || interrupted)
1139
+ return;
1140
+ const hadOutput = !!s.text.trim()
1141
+ || lastAssistantEventAt > 0 || lastSidecarEventAt > 0 || lastToolEventAt > start;
1142
+ if (hadOutput) {
1143
+ agentWarn('[claude-tui] model-unavailable banner was followed by real output — not terminating');
1144
+ return;
1145
+ }
1146
+ agentWarn('[claude-tui] model unavailable confirmed (no JSONL/tool/Stop activity) — terminating turn');
1147
+ killProc('SIGTERM');
1148
+ }, CLAUDE_TUI_MODEL_ERROR_SETTLE_MS);
1149
+ };
1119
1150
  // Simulated streaming. See TuiStreamBuffer / applyAssistantStreaming above.
1120
1151
  const streamBuf = makeTuiStreamBuffer();
1121
1152
  const scheduleStreamTick = () => {
@@ -1359,6 +1390,13 @@ export async function doClaudeTuiStream(opts) {
1359
1390
  if (notice)
1360
1391
  noteTerminalLimitNotice(notice);
1361
1392
  }
1393
+ // Selected-model-unavailable notice — see noteTerminalModelError. The TUI
1394
+ // only paints this to the screen (no JSONL, no Stop hook), so the live
1395
+ // screen tail is the sole signal. detectClaudeModelError is whitespace-
1396
+ // insensitive so it survives the TUI's char-by-char paint.
1397
+ if (!terminalModelError && detectClaudeModelError(screenTail)) {
1398
+ noteTerminalModelError(claudeModelErrorMessage(s.model || opts.claudeModel || null));
1399
+ }
1362
1400
  });
1363
1401
  // 7. Abort handling.
1364
1402
  const abortStream = () => {
@@ -1841,28 +1879,39 @@ export async function doClaudeTuiStream(opts) {
1841
1879
  screenSample: stallScreen.sample,
1842
1880
  });
1843
1881
  if (!s.errors) {
1844
- // Limit-notice arbitration first: a turn that showed a limit banner
1845
- // and then produced nothing substantive didn't freeze the limit
1846
- // ate it. Label it rate_limit with the banner's own text (which
1847
- // carries the reset time) so the user gets the real reason, and so
1848
- // doClaudeWithRetry doesn't auto-resume into the same wall.
1849
- const limitOutcome = resolveClaudeTuiLimitOutcome({
1850
- noticeText: terminalLimitNotice,
1851
- noticeAt: terminalLimitNoticeAt,
1852
- lastSubstantiveEventAt: Math.max(lastAssistantEventAt, lastToolEventAt, lastSidecarEventAt),
1853
- hasOutputText: !!s.text.trim(),
1854
- });
1855
- if (limitOutcome === 'fatal') {
1856
- s.stopReason = 'rate_limit';
1857
- s.errors = [terminalLimitNotice];
1882
+ if (terminalModelError && !s.text.trim()) {
1883
+ // Model-unavailable arbitration first: if the stall watchdog beat the
1884
+ // settle-timer (noteTerminalModelError) to the kill, the turn didn't
1885
+ // freeze the selected model is disabled / no-access. Surface the real
1886
+ // reason; stopReason 'model_error' is non-retryable so doClaudeWithRetry
1887
+ // won't auto-resume into the same dead model.
1888
+ s.stopReason = 'model_error';
1889
+ s.errors = [terminalModelError];
1858
1890
  }
1859
1891
  else {
1860
- // Be honest about which kind of stall this is. looksLikePrompt here
1861
- // means the auto-answer (detectClaudeProceedPrompt) did NOT clear an
1862
- // interactive prompt so it's a blocking dialog, not the CLI freeze.
1863
- s.errors = [stallScreen.looksLikePrompt
1864
- ? `Claude blocked mid-turn on an interactive prompt (PTY quiet ${ptyQuietS}s) that auto-answer couldn't clear. Terminated for auto-resume.`
1865
- : `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.`];
1892
+ // Limit-notice arbitration: a turn that showed a limit banner and then
1893
+ // produced nothing substantive didn't freeze the limit ate it. Label
1894
+ // it rate_limit with the banner's own text (which carries the reset
1895
+ // time) so the user gets the real reason, and so doClaudeWithRetry
1896
+ // doesn't auto-resume into the same wall.
1897
+ const limitOutcome = resolveClaudeTuiLimitOutcome({
1898
+ noticeText: terminalLimitNotice,
1899
+ noticeAt: terminalLimitNoticeAt,
1900
+ lastSubstantiveEventAt: Math.max(lastAssistantEventAt, lastToolEventAt, lastSidecarEventAt),
1901
+ hasOutputText: !!s.text.trim(),
1902
+ });
1903
+ if (limitOutcome === 'fatal') {
1904
+ s.stopReason = 'rate_limit';
1905
+ s.errors = [terminalLimitNotice];
1906
+ }
1907
+ else {
1908
+ // Be honest about which kind of stall this is. looksLikePrompt here
1909
+ // means the auto-answer (detectClaudeProceedPrompt) did NOT clear an
1910
+ // interactive prompt — so it's a blocking dialog, not the CLI freeze.
1911
+ s.errors = [stallScreen.looksLikePrompt
1912
+ ? `Claude blocked mid-turn on an interactive prompt (PTY quiet ${ptyQuietS}s) that auto-answer couldn't clear. Terminated for auto-resume.`
1913
+ : `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.`];
1914
+ }
1866
1915
  }
1867
1916
  }
1868
1917
  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'}`);
@@ -1942,6 +1991,16 @@ export async function doClaudeTuiStream(opts) {
1942
1991
  if (!s.errors)
1943
1992
  s.errors = [`Anthropic API error: ${apiErrorReason}`];
1944
1993
  }
1994
+ // Model-unavailable arbitration: the TUI painted the "selected model is
1995
+ // unavailable" banner (noteTerminalModelError) and the turn produced nothing
1996
+ // — no JSONL, no Stop hook. The early settle-timer's SIGTERM (or any exit)
1997
+ // brought us here; surface the real reason instead of a bare "(no textual
1998
+ // response)". stopReason 'model_error' is non-retryable (doClaudeWithRetry
1999
+ // only auto-resumes 'stalled'), so we never loop on the same dead model.
2000
+ if (!interrupted && !s.errors && terminalModelError && !s.text.trim()) {
2001
+ s.stopReason = 'model_error';
2002
+ s.errors = [terminalModelError];
2003
+ }
1945
2004
  // Limit-notice arbitration (see resolveClaudeTuiLimitOutcome). Covers the
1946
2005
  // paths the stall watchdog never reaches: the TUI painted a limit banner,
1947
2006
  // then Stop fired on an empty turn or the process exited — nothing
@@ -8,7 +8,7 @@ import { createInterface } from 'node:readline';
8
8
  import { registerDriver } from '../driver.js';
9
9
  import {
10
10
  // shared helpers
11
- Q, run, agentError, agentLog, agentWarn, buildStreamPreviewMeta, computeContext, pushRecentActivity, summarizeClaudeToolUse, summarizeClaudeToolResult, joinErrorMessages, parseTodoWriteAsPlan, previewToolCallInput, previewToolCallResult, detectClaudeApiError, isRetryableClaudeApiError, emitSessionIdUpdate, IMAGE_EXTS, mimeForExt, listPikiclawSessions, mergeManagedAndNativeSessions, readTailLines, stripInjectedPrompts, sanitizeSessionUserPreviewText, SESSION_PREVIEW_IMAGE_PLACEHOLDER_RE, CLAUDE_AT_MENTION_IMAGE_RE, extractClaudeAtMentionImagePaths, attachAgentImage, applyTurnWindow, shortValue, roundPercent, modelFamily, normalizeClaudeModelId, emptyUsage, normalizeUsageStatus, collapseSkillPrompt, } from '../index.js';
11
+ Q, run, agentError, agentLog, agentWarn, buildStreamPreviewMeta, computeContext, pushRecentActivity, summarizeClaudeToolUse, summarizeClaudeToolResult, joinErrorMessages, parseTodoWriteAsPlan, previewToolCallInput, previewToolCallResult, detectClaudeApiError, isRetryableClaudeApiError, detectClaudeModelError, claudeModelErrorMessage, emitSessionIdUpdate, IMAGE_EXTS, mimeForExt, listPikiclawSessions, mergeManagedAndNativeSessions, readTailLines, stripInjectedPrompts, sanitizeSessionUserPreviewText, SESSION_PREVIEW_IMAGE_PLACEHOLDER_RE, CLAUDE_AT_MENTION_IMAGE_RE, extractClaudeAtMentionImagePaths, attachAgentImage, applyTurnWindow, shortValue, roundPercent, modelFamily, normalizeClaudeModelId, emptyUsage, normalizeUsageStatus, collapseSkillPrompt, } from '../index.js';
12
12
  import { AGENT_STREAM_HARD_KILL_GRACE_MS, AGENT_GRACEFUL_ABORT_GRACE_MS, SESSION_RUNNING_THRESHOLD_MS } from '../../core/constants.js';
13
13
  import { terminateProcessTree } from '../../core/process-control.js';
14
14
  import { getHome, IS_MAC, encodePathAsDirName } from '../../core/platform.js';
@@ -652,8 +652,22 @@ export function claudeParse(ev, s) {
652
652
  // model error, …), not real Claude output. The historical jsonl reader
653
653
  // converts them into `system_notice` blocks; on the live stream we just
654
654
  // drop them so they don't pollute s.text / s.thinking.
655
- if (msg.model === '<synthetic>')
655
+ if (msg.model === '<synthetic>') {
656
+ // …except the "selected model is unavailable" notice (404 model_not_found):
657
+ // a hard, non-retryable failure. The result event's text fallback below
658
+ // also catches it, but recording s.errors here upgrades the turn from a
659
+ // bare "(no textual response)" reply to a clear error + non-retryable
660
+ // stopReason (so doClaudeWithRetry won't loop on the same dead model).
661
+ if (!s.errors) {
662
+ const synthText = (msg.content || [])
663
+ .filter((b) => b?.type === 'text').map((b) => b.text || '').join(' ');
664
+ if (ev.error === 'model_not_found' || detectClaudeModelError(synthText)) {
665
+ s.stopReason = 'model_error';
666
+ s.errors = [claudeModelErrorMessage(s.model)];
667
+ }
668
+ }
656
669
  return;
670
+ }
657
671
  const contents = msg.content || [];
658
672
  const th = contents.filter((b) => b?.type === 'thinking').map((b) => b.thinking || '').join('\n\n');
659
673
  const tx = contents.filter((b) => b?.type === 'text').map((b) => b.text || '').join('\n\n');
@@ -867,7 +881,11 @@ export function claudeParse(ev, s) {
867
881
  s.errors = ev.errors;
868
882
  if (ev.result && !s.text.trim())
869
883
  s.text = ev.result;
870
- s.stopReason = ev.stop_reason ?? s.stopReason;
884
+ // A model-unavailable turn carries a normal stop_reason on its result event
885
+ // (e.g. 'stop_sequence') that would otherwise clobber the 'model_error' the
886
+ // synthetic handler set — preserve it so the failure stays diagnosable.
887
+ if (s.stopReason !== 'model_error')
888
+ s.stopReason = ev.stop_reason ?? s.stopReason;
871
889
  const u = ev.usage;
872
890
  if (u) {
873
891
  // Per-call semantics: the last message_start/message_delta snapshot is
@@ -2171,6 +2189,16 @@ const CLAUDE_MODELS = [
2171
2189
  // ---------------------------------------------------------------------------
2172
2190
  // Usage
2173
2191
  // ---------------------------------------------------------------------------
2192
+ // The account-usage query below hits api.anthropic.com/api/oauth/usage, which is
2193
+ // itself rate-limited. The dashboard rebuilds agent status ~every 30s (plus a
2194
+ // forced refresh on usage-ring hover), and querying that often trips the
2195
+ // endpoint's 429 — which (since we treat a query error as "unknown") blanks the
2196
+ // header usage ring entirely. Quota windows (5h/7d) move slowly, so we query at
2197
+ // most once per this interval and serve the last good result in between
2198
+ // (including across transient 429s), decoupling usage cadence from how often
2199
+ // agent status is rebuilt.
2200
+ const CLAUDE_USAGE_QUERY_TTL_MS = 5 * 60_000;
2201
+ const claudeUsageCache = { lastGood: null, lastAttemptAt: 0 };
2174
2202
  function getClaudeOAuthToken() {
2175
2203
  // `security` is macOS-only; other platforms store Claude creds differently
2176
2204
  // (DPAPI on Windows, libsecret on Linux) and Claude Code manages those itself.
@@ -2573,9 +2601,25 @@ class ClaudeDriver {
2573
2601
  const home = getHome();
2574
2602
  if (!home)
2575
2603
  return emptyUsage('claude', 'HOME is not set.');
2576
- return getClaudeUsageFromOAuth()
2577
- || getClaudeUsageFromTelemetry(home, opts.model)
2604
+ const telemetry = () => getClaudeUsageFromTelemetry(home, opts.model)
2578
2605
  || emptyUsage('claude', 'No recent Claude usage data found.');
2606
+ // Throttle the rate-limited OAuth usage query (see CLAUDE_USAGE_QUERY_TTL_MS).
2607
+ // Within the window we reuse the last good result rather than re-querying on
2608
+ // every agent-status rebuild, so a transient query-API 429 can't blank the
2609
+ // ring between successful polls.
2610
+ const now = Date.now();
2611
+ if (now - claudeUsageCache.lastAttemptAt < CLAUDE_USAGE_QUERY_TTL_MS) {
2612
+ return claudeUsageCache.lastGood ?? telemetry();
2613
+ }
2614
+ claudeUsageCache.lastAttemptAt = now;
2615
+ const oauth = getClaudeUsageFromOAuth();
2616
+ if (oauth) {
2617
+ claudeUsageCache.lastGood = oauth;
2618
+ return oauth;
2619
+ }
2620
+ // OAuth unavailable (non-mac, no token, or transient 429): keep showing the
2621
+ // last good windows if we have any; otherwise fall back to telemetry.
2622
+ return claudeUsageCache.lastGood ?? telemetry();
2579
2623
  }
2580
2624
  async deleteNativeSession(workdir, sessionId) {
2581
2625
  const file = claudeSessionTranscriptPath(workdir, sessionId);
@@ -21,7 +21,7 @@ export { IMAGE_EXTS } from './types.js';
21
21
  // ── Re-export: image pipeline ──────────────────────────────────────────────
22
22
  export { attachAgentImage, attachInlineImage, materializeImage, rewriteImageBlocksForTransport, resolveAllowedAttachmentPath, allowedAttachmentRoots, decodeAttachmentPathParam, sessionAttachmentsDir, codexHome, } from './images.js';
23
23
  // ── Re-export: utilities ────────────────────────────────────────────────────
24
- export { Q, agentLog, agentWarn, agentError, dedupeStrings, numberOrNull, normalizeStreamPreviewPlan, parseTodoWriteAsPlan, normalizeActivityLine, pushRecentActivity, detectClaudeApiError, isRetryableClaudeApiError, firstNonEmptyLine, shortValue, normalizeErrorMessage, joinErrorMessages, appendSystemPrompt, mimeForExt, computeContext, buildStreamPreviewMeta, summarizeClaudeToolUse, summarizeClaudeToolResult, previewToolCallInput, previewToolCallResult, roundPercent, toIsoFromEpochSeconds, normalizeUsageStatus, labelFromWindowMinutes, usageWindowFromRateLimit, parseJsonTail, modelFamily, normalizeClaudeModelId, emptyUsage, readTailLines, stripInjectedPrompts, sanitizeSessionUserPreviewText, SESSION_PREVIEW_IMAGE_PLACEHOLDER_RE, CLAUDE_AT_MENTION_IMAGE_RE, extractClaudeAtMentionImagePaths, stripClaudeAtMentionImages, isPendingSessionId, emitSessionIdUpdate, sessionListDisplayTitle, } from './utils.js';
24
+ export { Q, agentLog, agentWarn, agentError, dedupeStrings, numberOrNull, normalizeStreamPreviewPlan, parseTodoWriteAsPlan, normalizeActivityLine, pushRecentActivity, detectClaudeApiError, isRetryableClaudeApiError, detectClaudeModelError, claudeModelErrorMessage, firstNonEmptyLine, shortValue, normalizeErrorMessage, joinErrorMessages, appendSystemPrompt, mimeForExt, computeContext, buildStreamPreviewMeta, summarizeClaudeToolUse, summarizeClaudeToolResult, previewToolCallInput, previewToolCallResult, roundPercent, toIsoFromEpochSeconds, normalizeUsageStatus, labelFromWindowMinutes, usageWindowFromRateLimit, parseJsonTail, modelFamily, normalizeClaudeModelId, emptyUsage, readTailLines, stripInjectedPrompts, sanitizeSessionUserPreviewText, SESSION_PREVIEW_IMAGE_PLACEHOLDER_RE, CLAUDE_AT_MENTION_IMAGE_RE, extractClaudeAtMentionImagePaths, stripClaudeAtMentionImages, isPendingSessionId, emitSessionIdUpdate, sessionListDisplayTitle, } from './utils.js';
25
25
  // ── Re-export: session management ───────────────────────────────────────────
26
26
  export { updateSessionMeta, promoteSessionId, recordFork, listPikiclawSessions, findPikiclawSession, getSessionStoredConfig, ensureManagedSession, findManagedThreadSession, stageSessionFiles, mergeManagedAndNativeSessions, getSessions, getSessionTail, getSessionMessages, applyTurnWindow, applyTurnFilter, classifySession, deriveUserStatus, exportSession, importSession, deleteAgentSession, isProcessAlive, isRunningSessionStale, reconcileOrphanedRunningSessions, } from './session.js';
27
27
  // ── Re-export: stream & detection ───────────────────────────────────────────
@@ -204,6 +204,37 @@ export function isRetryableClaudeApiError(reason) {
204
204
  return false;
205
205
  return /overloaded|overload|timeout|timed out|500|502|503|504|529|temporar|gateway|connection|network|internal (server )?error/i.test(r);
206
206
  }
207
+ /**
208
+ * Detect Claude Code's "selected model is unavailable" notice — emitted when
209
+ * the requested `--model` id is disabled / not provisioned for the account (a
210
+ * 404 model_not_found). Its delivery differs by mode:
211
+ * - `-p`/stream-json: a `<synthetic>` assistant event carrying
212
+ * `error:"model_not_found"` plus the banner text, and a `result` event with
213
+ * `is_error` — both reach the parser.
214
+ * - TUI: the banner is *only* painted to the PTY screen. It is never written
215
+ * to the transcript JSONL and fires no Stop hook (verified 2026-06-13), so
216
+ * the screen scrape is the sole signal and the turn would otherwise hang
217
+ * until the stall watchdog (3–10 min).
218
+ *
219
+ * Matching is whitespace-insensitive on purpose: the TUI renders the banner
220
+ * character-by-character with cursor positioning, so after ANSI stripping the
221
+ * words lose their spaces and wrap arbitrarily ("issuewiththeselectedmodel").
222
+ * Collapsing whitespace on both sides makes the match survive that rendering.
223
+ * Callers compose the user-facing message via `claudeModelErrorMessage` with
224
+ * the concrete model id they hold.
225
+ */
226
+ export function detectClaudeModelError(text) {
227
+ if (!text)
228
+ return false;
229
+ const collapsed = text.replace(/\s+/g, '').toLowerCase();
230
+ return collapsed.includes('issuewiththeselectedmodel')
231
+ || collapsed.includes('maynotexistoryoumaynothaveaccess');
232
+ }
233
+ /** User-facing message for an unavailable / no-access model (see {@link detectClaudeModelError}). */
234
+ export function claudeModelErrorMessage(model) {
235
+ const id = (model || '').trim();
236
+ return `The selected model${id ? ` (${id})` : ''} is unavailable — it may not exist, or this account doesn't have access to it. Switch to a different model in pikiclaw settings.`;
237
+ }
207
238
  export function appendSystemPrompt(base, extra) {
208
239
  const lhs = String(base || '').trim();
209
240
  const rhs = String(extra || '').trim();
@@ -318,6 +318,15 @@ export const CLAUDE_TUI_STALL_PENDING_TOOL_MS = 30 * 60_000;
318
318
  * refreshes the PTY signal and defers this path to the slow thresholds.
319
319
  */
320
320
  export const CLAUDE_TUI_STALL_PTY_DEAD_MS = 3 * 60_000;
321
+ /**
322
+ * Settle window after the TUI paints the "selected model is unavailable" banner
323
+ * (a 404 model_not_found). The notice is terminal — claude paints it then idles
324
+ * at the REPL forever: no JSONL is written, no Stop hook fires. We wait this
325
+ * brief window to cross-validate that nothing substantive followed (the banner
326
+ * alone is evidence, not a verdict — same discipline as resolveClaudeTuiLimitOutcome)
327
+ * before ending the turn, instead of waiting out the 3–10 minute stall watchdog.
328
+ */
329
+ export const CLAUDE_TUI_MODEL_ERROR_SETTLE_MS = 2_500;
321
330
  /**
322
331
  * TTL for the post-Stop `hold-background` path. The hold protects
323
332
  * run_in_background agents living inside the claude process — but a live
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pikiclaw",
3
- "version": "0.3.82",
3
+ "version": "0.3.84",
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": {