pikiclaw 0.3.78 → 0.3.80

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.
@@ -45,7 +45,7 @@ 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
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';
48
- import { claudeParse, createClaudeStreamState, claudeContextWindowFromModel, claudeEffectiveContextWindow, registerClaudeBackgroundAgentLaunch, pendingClaudeBackgroundAgentCount, registerClaudeBackgroundBashLaunch, pendingClaudeBackgroundBashCount, extractClaudeBackgroundTaskId, extractClaudeWorkflowRunId, claudeEffortAndWorkflowArgs, } from './claude.js';
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)
51
51
  // ---------------------------------------------------------------------------
@@ -956,12 +956,16 @@ export async function doClaudeTuiStream(opts) {
956
956
  // PATH inside the claude TUI's hook subprocess on every distro.
957
957
  const nodeBin = Q(process.execPath);
958
958
  const hookCmd = (event) => `${nodeBin} ${Q(hookPath)} ${event} ${Q(statePath)} ${Q(toolEventsPath)}`;
959
- // Pre/PostToolUse hooks give us a live event stream. Claude Code 2.x
960
- // buffers the JSONL transcript and only flushes it when Stop fires, so
961
- // without these hooks the dashboard / IM see absolutely no progress
962
- // during a 30s+ turn. The hook script writes to tool-events.jsonl via
963
- // atomic appends, sidestepping the read-modify-write race that affects
964
- // the shared state.json file.
959
+ // Pre/PostToolUse hooks give us a live tool-event stream. The transcript
960
+ // JSONL is itself written incrementally (events land ~0.2–1.2s after they
961
+ // happen measured on 2.1.173), but the hooks still earn their keep:
962
+ // PreToolUse fires the instant a tool *starts* (the JSONL tool_use only
963
+ // proves it was requested; a long-running Bash would otherwise sit
964
+ // invisible), they carry agent_id for sub-agent attribution, and they are
965
+ // the registration point for run_in_background launches that the Stop
966
+ // gating in decideClaudeTuiStop depends on. The hook script writes to
967
+ // tool-events.jsonl via atomic appends, sidestepping the
968
+ // read-modify-write race that affects the shared state.json file.
965
969
  // Pre/PostToolUse require an explicit `matcher` field — without it Claude
966
970
  // Code's hook dispatcher silently never fires the hook (the lifecycle
967
971
  // hooks below don't need a matcher because they aren't tool-scoped).
@@ -1157,9 +1161,13 @@ export async function doClaudeTuiStream(opts) {
1157
1161
  if (typeof v === 'string')
1158
1162
  spawnEnv[k] = v;
1159
1163
  }
1160
- // CLAUDECODE is set automatically by the parent claude process when calling
1161
- // children clear it so this is treated as a fresh top-level invocation.
1162
- delete spawnEnv.CLAUDECODE;
1164
+ // Strip the session-context markers a parent claude process exports to its
1165
+ // subprocesses (CLAUDECODE, CLAUDE_CODE_CHILD_SESSION, …). Inherited e.g.
1166
+ // when an agent restarted the pikiclaw daemon from inside a Claude Code
1167
+ // session, they flip this spawn into child-session mode — the transcript
1168
+ // JSONL (our only text source) is then never written locally and the turn
1169
+ // streams nothing. See CLAUDE_SESSION_CONTEXT_ENV_KEYS in claude.ts.
1170
+ scrubClaudeSessionContextEnv(spawnEnv);
1163
1171
  // Critical: leaving ANTHROPIC_API_KEY set would route TUI through API
1164
1172
  // billing too, defeating the whole point. Strip it unless the user
1165
1173
  // explicitly opts back in.
@@ -1416,11 +1424,69 @@ export async function doClaudeTuiStream(opts) {
1416
1424
  let lastClearedStopAt = 0;
1417
1425
  /** Hook-reported tools still executing: PreToolUse seen, no PostToolUse. */
1418
1426
  const pendingHookToolIds = new Set();
1427
+ // Incremental main-JSONL drain — the canonical text/thinking/usage feed.
1428
+ // Used by both the 200ms poll tick and the post-exit final drain. Returns
1429
+ // true when any line was consumed so callers can emit().
1430
+ const drainMainJsonl = () => {
1431
+ if (!fs.existsSync(activeJsonlPath))
1432
+ return false;
1433
+ const inc = readJsonlIncrement(activeJsonlPath, jsonlReadOffset);
1434
+ jsonlReadOffset = inc.offset;
1435
+ let touched = false;
1436
+ for (const line of inc.lines) {
1437
+ const trimmed = line.trim();
1438
+ if (!trimmed || trimmed[0] !== '{')
1439
+ continue;
1440
+ lineCount++;
1441
+ let ev;
1442
+ try {
1443
+ ev = JSON.parse(trimmed);
1444
+ }
1445
+ catch {
1446
+ continue;
1447
+ }
1448
+ // Ignore sub-agent sidecar events — they belong to a child agent's
1449
+ // stream and would re-enter the parent's accumulator. claudeParse's
1450
+ // own sub-agent routing handles them.
1451
+ const isSubAgentEvent = typeof ev.parent_tool_use_id === 'string' && ev.parent_tool_use_id;
1452
+ if (!isSubAgentEvent && ev.type === 'assistant') {
1453
+ const notice = detectClaudeTuiTerminalLimitNotice(ev.message);
1454
+ if (notice) {
1455
+ // A synthetic limit banner is not substantive progress — skip the
1456
+ // liveness/type bookkeeping below so the limit arbitration and the
1457
+ // stall watchdog don't mistake it for a live model segment.
1458
+ noteTerminalLimitNotice(notice);
1459
+ touched = true;
1460
+ continue;
1461
+ }
1462
+ applyAssistantStreaming(s, ev.message, streamBuf);
1463
+ applyAssistantUsage(s, ev.message);
1464
+ if (ev.message?.model && ev.message.model !== '<synthetic>' && typeof ev.message.model === 'string') {
1465
+ lastAssistantEventAt = Date.now();
1466
+ s.model = ev.message.model;
1467
+ applyModelContextWindow(s);
1468
+ }
1469
+ }
1470
+ try {
1471
+ callClaudeParseForTui(ev, s);
1472
+ }
1473
+ catch (e) {
1474
+ agentWarn(`[claude-tui] claudeParse threw on line: ${e?.message || e}`);
1475
+ }
1476
+ touched = true;
1477
+ lastMainJsonlEventAt = Date.now();
1478
+ if (typeof ev.version === 'string' && ev.version)
1479
+ observedClaudeVersion = ev.version;
1480
+ if (!isSubAgentEvent)
1481
+ lastMainJsonlType = classifyClaudeJsonlEvent(ev);
1482
+ }
1483
+ return touched;
1484
+ };
1419
1485
  // Append-only tool-events log fed by PreToolUse / PostToolUse hooks. We
1420
- // tail it with the same incremental reader the JSONL transcript uses, so
1421
- // tool calls + plan changes surface live during the turn even while the
1422
- // canonical JSONL stays empty (Claude Code 2.x buffers the whole transcript
1423
- // until the Stop hook fires).
1486
+ // tail it with the same incremental reader the JSONL transcript uses. Hook
1487
+ // events usually beat their JSONL counterpart by a second or so (and
1488
+ // PreToolUse fires before the tool even runs); whichever feed arrives first
1489
+ // wins, the other dedups via seenClaudeToolIds / seenClaudeToolResultIds.
1424
1490
  let toolEventsReadOffset = 0;
1425
1491
  const drainToolEvents = () => {
1426
1492
  if (!fs.existsSync(toolEventsPath))
@@ -1604,69 +1670,18 @@ export async function doClaudeTuiStream(opts) {
1604
1670
  agentLog(`[claude-tui] prompt-submit nudge sent (no UserPromptSubmit after ${PROMPT_SUBMIT_NUDGE_MS}ms)`);
1605
1671
  }
1606
1672
  // JSONL tail.
1607
- if (fs.existsSync(activeJsonlPath)) {
1608
- const inc = readJsonlIncrement(activeJsonlPath, jsonlReadOffset);
1609
- jsonlReadOffset = inc.offset;
1610
- let touched = false;
1611
- for (const line of inc.lines) {
1612
- const trimmed = line.trim();
1613
- if (!trimmed || trimmed[0] !== '{')
1614
- continue;
1615
- lineCount++;
1616
- let ev;
1617
- try {
1618
- ev = JSON.parse(trimmed);
1619
- }
1620
- catch {
1621
- continue;
1622
- }
1623
- // Ignore sub-agent sidecar events — they belong to a child agent's
1624
- // stream and would re-enter the parent's accumulator. claudeParse's
1625
- // own sub-agent routing handles them.
1626
- const isSubAgentEvent = typeof ev.parent_tool_use_id === 'string' && ev.parent_tool_use_id;
1627
- if (!isSubAgentEvent && ev.type === 'assistant') {
1628
- const notice = detectClaudeTuiTerminalLimitNotice(ev.message);
1629
- if (notice) {
1630
- noteTerminalLimitNotice(notice);
1631
- touched = true;
1632
- continue;
1633
- }
1634
- applyAssistantStreaming(s, ev.message, streamBuf);
1635
- applyAssistantUsage(s, ev.message);
1636
- if (ev.message?.model && ev.message.model !== '<synthetic>' && typeof ev.message.model === 'string') {
1637
- lastAssistantEventAt = Date.now();
1638
- s.model = ev.message.model;
1639
- applyModelContextWindow(s);
1640
- }
1641
- }
1642
- try {
1643
- callClaudeParseForTui(ev, s);
1644
- }
1645
- catch (e) {
1646
- agentWarn(`[claude-tui] claudeParse threw on line: ${e?.message || e}`);
1647
- }
1648
- touched = true;
1649
- lastMainJsonlEventAt = Date.now();
1650
- if (typeof ev.version === 'string' && ev.version)
1651
- observedClaudeVersion = ev.version;
1652
- if (!isSubAgentEvent)
1653
- lastMainJsonlType = classifyClaudeJsonlEvent(ev);
1654
- }
1655
- if (touched) {
1656
- // Emit immediately so non-text changes (tool_use, plan, activity,
1657
- // thinking, usage) reach the dashboard without waiting for the
1658
- // chunked stream tick. The streaming timer separately advances
1659
- // s.text from the buffer over the next few ticks.
1660
- emit();
1661
- scheduleStreamTick();
1662
- }
1673
+ if (drainMainJsonl()) {
1674
+ // Emit immediately so non-text changes (tool_use, plan, activity,
1675
+ // thinking, usage) reach the dashboard without waiting for the
1676
+ // chunked stream tick. The streaming timer separately advances
1677
+ // s.text from the buffer over the next few ticks.
1678
+ emit();
1679
+ scheduleStreamTick();
1663
1680
  }
1664
- // Live tool-events stream — fed by Pre/PostToolUse hooks. Order matters:
1665
- // we drain hooks BEFORE the JSONL tail above already ran so any hook
1666
- // events that beat their JSONL counterpart are recorded in
1667
- // seenClaudeToolIds first; subsequent JSONL pass deduplicates naturally.
1668
- // In practice JSONL doesn't land until Stop, so this is the only signal
1669
- // that fires during a normal turn.
1681
+ // Live tool-events stream — fed by Pre/PostToolUse hooks. Hook and JSONL
1682
+ // feeds race per tool call; both record into seenClaudeToolIds /
1683
+ // seenClaudeToolResultIds so whichever lands first wins and the other
1684
+ // pass dedups naturally.
1670
1685
  if (drainToolEvents())
1671
1686
  emit();
1672
1687
  // Sub-agent sidecar discovery + pump. Order matters: discovery first so a
@@ -1882,47 +1897,8 @@ export async function doClaudeTuiStream(opts) {
1882
1897
  opts.abortSignal?.removeEventListener('abort', abortStream);
1883
1898
  // 11. Final drain — pick up anything written between the last poll and
1884
1899
  // process exit. Claude flushes its remaining JSONL events on shutdown.
1885
- if (fs.existsSync(activeJsonlPath)) {
1886
- const inc = readJsonlIncrement(activeJsonlPath, jsonlReadOffset);
1887
- jsonlReadOffset = inc.offset;
1888
- let touched = false;
1889
- for (const line of inc.lines) {
1890
- const trimmed = line.trim();
1891
- if (!trimmed || trimmed[0] !== '{')
1892
- continue;
1893
- lineCount++;
1894
- let ev;
1895
- try {
1896
- ev = JSON.parse(trimmed);
1897
- }
1898
- catch {
1899
- continue;
1900
- }
1901
- const isSubAgentEvent = typeof ev.parent_tool_use_id === 'string' && ev.parent_tool_use_id;
1902
- if (!isSubAgentEvent && ev.type === 'assistant') {
1903
- const notice = detectClaudeTuiTerminalLimitNotice(ev.message);
1904
- if (notice) {
1905
- noteTerminalLimitNotice(notice);
1906
- touched = true;
1907
- continue;
1908
- }
1909
- applyAssistantStreaming(s, ev.message, streamBuf);
1910
- applyAssistantUsage(s, ev.message);
1911
- if (ev.message?.model && ev.message.model !== '<synthetic>' && typeof ev.message.model === 'string') {
1912
- lastAssistantEventAt = Date.now();
1913
- s.model = ev.message.model;
1914
- applyModelContextWindow(s);
1915
- }
1916
- }
1917
- try {
1918
- callClaudeParseForTui(ev, s);
1919
- }
1920
- catch { }
1921
- touched = true;
1922
- }
1923
- if (touched)
1924
- emit();
1925
- }
1900
+ if (drainMainJsonl())
1901
+ emit();
1926
1902
  // Final tool-events drain — any PreToolUse / PostToolUse hooks that fired
1927
1903
  // between the last poll tick and process exit.
1928
1904
  if (drainToolEvents())
@@ -65,6 +65,39 @@ export function claudeEffortAndWorkflowArgs(o) {
65
65
  args.push('--disallowed-tools', 'Workflow');
66
66
  return args;
67
67
  }
68
+ /**
69
+ * Env keys the claude CLI exports to its own subprocesses (Bash tool, hooks)
70
+ * to mark them as children of a running session. If pikiclaw itself was
71
+ * launched from inside a Claude Code session — agent-driven `npm run dev`
72
+ * restarts, `! npx pikiclaw` typed into the TUI, the self-bootstrap path —
73
+ * these leak into the daemon's environment and every claude it spawns
74
+ * inherits them. A claude started with `CLAUDE_CODE_CHILD_SESSION` set runs
75
+ * in child-session mode: it mirrors transcript persistence to its (absent)
76
+ * SDK parent instead of writing `~/.claude/projects/<dir>/<id>.jsonl`.
77
+ * The TUI driver tails that JSONL as its only text source, so a contaminated
78
+ * spawn streams nothing, returns "(no textual response)", and loses the whole
79
+ * turn on SIGTERM. Verified on 2.1.173: with these vars set the transcript
80
+ * never grows past the ai-title line; with them scrubbed every event lands
81
+ * 0.2–1.2s after it happens.
82
+ *
83
+ * Deliberately a closed list: config-style vars users set on purpose
84
+ * (CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_MAX_OUTPUT_TOKENS, …) must survive.
85
+ * Shared by both spawn paths (`claude -p` here and the PTY/TUI driver).
86
+ */
87
+ const CLAUDE_SESSION_CONTEXT_ENV_KEYS = [
88
+ 'CLAUDECODE',
89
+ 'CLAUDE_CODE_CHILD_SESSION',
90
+ 'CLAUDE_CODE_ENTRYPOINT',
91
+ 'CLAUDE_CODE_EXECPATH',
92
+ 'CLAUDE_CODE_SESSION_ID',
93
+ 'CLAUDE_CODE_SSE_PORT',
94
+ 'CLAUDE_EFFORT',
95
+ 'CLAUDE_PERMISSION_MODE',
96
+ ];
97
+ export function scrubClaudeSessionContextEnv(env) {
98
+ for (const key of CLAUDE_SESSION_CONTEXT_ENV_KEYS)
99
+ delete env[key];
100
+ }
68
101
  // ---------------------------------------------------------------------------
69
102
  // Command & parser
70
103
  // ---------------------------------------------------------------------------
@@ -1001,7 +1034,7 @@ async function doClaudeInteractiveStream(opts) {
1001
1034
  agentLog(`[spawn] timeout: ${opts.timeout}s session: ${opts.sessionId || '(new)'}`);
1002
1035
  agentLog(`[spawn] prompt (stdin): "${opts.prompt.slice(0, 300)}${opts.prompt.length > 300 ? '…' : ''}"`);
1003
1036
  const spawnEnv = { ...process.env, ...(opts.extraEnv || {}) };
1004
- delete spawnEnv.CLAUDECODE;
1037
+ scrubClaudeSessionContextEnv(spawnEnv);
1005
1038
  const proc = spawn(shellCmd, {
1006
1039
  cwd: opts.workdir,
1007
1040
  env: spawnEnv,
@@ -25,7 +25,7 @@ export { Q, agentLog, agentWarn, agentError, dedupeStrings, numberOrNull, normal
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 ───────────────────────────────────────────
28
- export { detectAgentBin, listAgents, run, doStream, listModels, resolveAgentModels, getUsage, getAgentBoundModelId, setAgentBoundModelId, } from './stream.js';
28
+ export { detectAgentBin, listAgents, resolveDefaultAgent, run, doStream, listModels, resolveAgentModels, getUsage, getAgentBoundModelId, setAgentBoundModelId, } from './stream.js';
29
29
  // ── Re-export: driver registry ──────────────────────────────────────────────
30
30
  export { registerDriver, getDriver, getDriverCapabilities, allDrivers, allDriverIds, hasDriver, shutdownAllDrivers, } from './driver.js';
31
31
  // ── Re-export: skills ───────────────────────────────────────────────────────
@@ -8,7 +8,7 @@ import path from 'node:path';
8
8
  import { restartManagedBrowser } from '../browser-supervisor.js';
9
9
  import { terminateProcessTree } from '../core/process-control.js';
10
10
  import { AGENT_DETECT_TIMEOUTS, AGENT_STREAM_HARD_KILL_GRACE_MS } from '../core/constants.js';
11
- import { getDriver, allDrivers, getAcceptedProviderKinds } from './driver.js';
11
+ import { getDriver, allDrivers, getAcceptedProviderKinds, hasDriver } from './driver.js';
12
12
  import { resolveAgentInjection, getActiveProfile, getActiveProfileId, getProvider, updateProfile, listProfiles, } from '../model/index.js';
13
13
  import { Q, agentLog, agentWarn, agentError, joinErrorMessages, normalizeErrorMessage, buildStreamPreviewMeta, computeContext, shortValue, isPendingSessionId, dedupeStrings, normalizeStreamPreviewPlan, } from './utils.js';
14
14
  import { saveSessionRecord, setSessionRunState, applySessionRunResult, ensureSessionWorkspace, importFilesIntoWorkspace, syncManagedSessionIdentity, summarizePromptTitle, recordFork, } from './session.js';
@@ -154,6 +154,33 @@ export function detectAgentBin(cmd, agent, options = {}) {
154
154
  export function listAgents(options = {}) {
155
155
  return { agents: allDrivers().map(d => detectAgentBin(d.cmd, d.id, options)) };
156
156
  }
157
+ /**
158
+ * Resolve the *effective* default agent for new conversations.
159
+ *
160
+ * The stored value is only a *preference* — a new conversation can run only an
161
+ * agent whose CLI is actually installed. So when the preference's CLI isn't
162
+ * installed, we clamp to the first installed agent (in driver-registration
163
+ * order: claude → codex → gemini → hermes) instead of surfacing an uninstalled
164
+ * default the user can't run. When the preference *is* installed it always
165
+ * wins, so machines with the historical 'codex' default are unaffected. When
166
+ * nothing is installed we keep the prior behaviour (honour a valid preference,
167
+ * else 'codex') so the result is always defined.
168
+ *
169
+ * Resolution is derived, never persisted: if the user later installs their
170
+ * preferred agent, the original preference is honoured again automatically.
171
+ * `agents` is injected (defaults to live detection) so the resolution is a pure
172
+ * function of (preference, install-state) and trivially testable.
173
+ */
174
+ export function resolveDefaultAgent(preferred, agents = listAgents().agents) {
175
+ const want = typeof preferred === 'string' ? preferred.trim().toLowerCase() : '';
176
+ const wantValid = !!want && hasDriver(want);
177
+ const installed = agents.filter(a => a.installed).map(a => a.agent);
178
+ if (wantValid && installed.includes(want))
179
+ return want;
180
+ if (installed.length)
181
+ return installed[0];
182
+ return wantValid ? want : 'codex';
183
+ }
157
184
  // ---------------------------------------------------------------------------
158
185
  // Shared CLI spawn framework (used by driver-claude.ts, driver-gemini.ts)
159
186
  // ---------------------------------------------------------------------------
package/dist/bot/bot.js CHANGED
@@ -7,7 +7,7 @@ import os from 'node:os';
7
7
  import path from 'node:path';
8
8
  import { execSync, spawn } from 'node:child_process';
9
9
  import { getActiveUserConfig, loadWorkspaces, onUserConfigChange, resolveUserWorkdir, setUserWorkdir, updateUserConfig } from '../core/config/user-config.js';
10
- import { doStream, ensureManagedSession, findManagedThreadSession, getSessionStoredConfig, getUsage, initializeProjectSkills, listAgents, resolveAgentModels, listSkills, stageSessionFiles, reconcileOrphanedRunningSessions, getAgentBoundModelId, setAgentBoundModelId, collapseSkillPrompt, readGoal, accountTurn, shouldContinueAfterTurn, renderContinuationPrompt, renderBudgetLimitPrompt, bumpContinuationCount, pauseGoal, resumeGoal, setGoal as setGoalState, clearGoal as clearGoalState, setCodexGoal, getCodexGoal, clearCodexGoal, pauseCodexGoal, resumeCodexGoal, getClaudeNativeGoal, buildClaudeSetGoalPrompt, buildClaudeClearGoalPrompt, isPendingSessionId, } from '../agent/index.js';
10
+ import { doStream, ensureManagedSession, findManagedThreadSession, getSessionStoredConfig, getUsage, initializeProjectSkills, listAgents, resolveAgentModels, resolveDefaultAgent, listSkills, stageSessionFiles, reconcileOrphanedRunningSessions, getAgentBoundModelId, setAgentBoundModelId, collapseSkillPrompt, readGoal, accountTurn, shouldContinueAfterTurn, renderContinuationPrompt, renderBudgetLimitPrompt, bumpContinuationCount, pauseGoal, resumeGoal, setGoal as setGoalState, clearGoal as clearGoalState, setCodexGoal, getCodexGoal, clearCodexGoal, pauseCodexGoal, resumeCodexGoal, getClaudeNativeGoal, buildClaudeSetGoalPrompt, buildClaudeClearGoalPrompt, isPendingSessionId, } from '../agent/index.js';
11
11
  import { compactForHandover, describeHandoverRef } from '../agent/handover.js';
12
12
  import { getActiveProfileId, setActiveProfile } from '../model/index.js';
13
13
  import { querySessions, querySessionTail, updateSession, } from './session-hub.js';
@@ -2229,7 +2229,11 @@ export class Bot {
2229
2229
  else if (nextWorkdir !== this.workdir) {
2230
2230
  this.switchWorkdir(nextWorkdir, { persist: false });
2231
2231
  }
2232
- const nextDefaultAgent = normalizeAgent(String(config.defaultAgent || 'codex').trim().toLowerCase() || 'codex');
2232
+ // The configured value is a *preference* (baseline 'codex' when unset);
2233
+ // clamp it to an installed agent so a fresh machine whose preferred CLI
2234
+ // isn't installed still routes new conversations to one that can actually
2235
+ // run, instead of surfacing an uninstalled default.
2236
+ const nextDefaultAgent = resolveDefaultAgent(config.defaultAgent || 'codex', listAgents().agents);
2233
2237
  if (opts.initial)
2234
2238
  this.defaultAgent = nextDefaultAgent;
2235
2239
  else if (nextDefaultAgent !== this.defaultAgent)
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { EventEmitter } from 'node:events';
9
9
  import { applyChannelEnvFallback, loadUserConfig, resolveUserWorkdir } from '../core/config/user-config.js';
10
- import { listAgents } from '../agent/index.js';
10
+ import { listAgents, resolveDefaultAgent } from '../agent/index.js';
11
11
  import { collectSetupState } from '../cli/onboarding.js';
12
12
  import { validateDingtalkConfig, validateDiscordConfig, validateFeishuConfig, validateSlackConfig, validateTelegramConfig, validateWecomConfig, validateWeixinConfig, } from '../core/config/validation.js';
13
13
  import { shouldCacheChannelStates } from '../channels/states.js';
@@ -205,8 +205,11 @@ class Runtime {
205
205
  getRuntimeDefaultAgent(config) {
206
206
  if (this.botRef)
207
207
  return this.botRef.defaultAgent;
208
- const raw = String(this.runtimePrefs.defaultAgent || config.defaultAgent || 'codex').trim().toLowerCase();
209
- return this.isAgent(raw) ? raw : 'codex';
208
+ // No bot yet (e.g. setup flow): resolve the stored preference (baseline
209
+ // 'codex' when unset) against installed CLIs so the dashboard never
210
+ // surfaces an uninstalled default the user can't run.
211
+ const preferred = this.runtimePrefs.defaultAgent || config.defaultAgent || 'codex';
212
+ return resolveDefaultAgent(preferred, listAgents().agents);
210
213
  }
211
214
  setModelEnv(agent, value) {
212
215
  setAgentModelEnv(agent, value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pikiclaw",
3
- "version": "0.3.78",
3
+ "version": "0.3.80",
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": {