open-walnut 0.4.0 → 0.4.2

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.
Files changed (32) hide show
  1. package/README.md +15 -6
  2. package/bin/open-walnut.js +14 -0
  3. package/dist/cli-fast.js +69 -46
  4. package/dist/cli.js +1264 -354
  5. package/dist/daemon-binaries/acp-worker.js.version +1 -1
  6. package/dist/web/server.js +1237 -354
  7. package/dist/web/static/assets/{OfficePreview-DUTDx4ef.js → OfficePreview-CEHO0xkd.js} +2 -2
  8. package/dist/web/static/assets/{docx-preview-Qj-aUywE.js → docx-preview-CuZwjAMS.js} +1 -1
  9. package/dist/web/static/assets/{index-xu9Gy-xP.js → index-0MyC-yGG.js} +1 -1
  10. package/dist/web/static/assets/{index-BvxwKQUT.js → index-BBjg7m-_.js} +1 -1
  11. package/dist/web/static/assets/{index-oIyzyrLY.js → index-BFF_G72i.js} +294 -294
  12. package/dist/web/static/assets/{index-CEVKci-j.js → index-BRLxNhyy.js} +1 -1
  13. package/dist/web/static/assets/{index-CeR0xElr.js → index-Bqg4_WEG.js} +1 -1
  14. package/dist/web/static/assets/{index-CCa56u0e.js → index-C2YGUYGS.js} +1 -1
  15. package/dist/web/static/assets/{index-BV9e5-KR.js → index-D1dpOEzW.js} +1 -1
  16. package/dist/web/static/assets/{index-Dv42N8tm.js → index-D9qPPm7h.js} +1 -1
  17. package/dist/web/static/assets/{index-CzP6rbk7.js → index-DAOPV34I.js} +1 -1
  18. package/dist/web/static/assets/{index-fk2aCKPB.js → index-DLjK8dGE.js} +1 -1
  19. package/dist/web/static/assets/{index-y7f5PVUr.js → index-DOXm2Aye.js} +1 -1
  20. package/dist/web/static/assets/{index-CSssA5eI.js → index-DR-LSqAg.js} +1 -1
  21. package/dist/web/static/assets/{index-DqY9Z9j5.js → index-DXtp_ZWh.js} +1 -1
  22. package/dist/web/static/assets/index-DeYCGmOR.css +32 -0
  23. package/dist/web/static/assets/{index-D6haQ3tX.js → index-DrWzb338.js} +1 -1
  24. package/dist/web/static/assets/{index-CfC-vHYG.js → index-DtRwkh8J.js} +1 -1
  25. package/dist/web/static/assets/{index-BmDSVLwR.js → index-ccs5s80D.js} +1 -1
  26. package/dist/web/static/assets/{index-Bke7ZOIJ.js → index-mWEwJcqw.js} +1 -1
  27. package/dist/web/static/assets/{index-sF1ObMS0.js → index-unnPilnK.js} +1 -1
  28. package/dist/web/static/assets/{jszip.min-C3K7iJBk.js → jszip.min-DYFwy4PE.js} +1 -1
  29. package/dist/web/static/assets/{pptx-preview.es-UhsuLLNP.js → pptx-preview.es-eu2nX9JC.js} +1 -1
  30. package/dist/web/static/index.html +2 -2
  31. package/package.json +5 -4
  32. package/dist/web/static/assets/index-Btn4AoIy.css +0 -32
@@ -10199,7 +10199,26 @@ function reapSession(sid, code, reason) {
10199
10199
  let jsonlAgeMs = null;
10200
10200
  try { jsonlAgeMs = Date.now() - fs.statSync(session.jsonlPath).mtimeMs; } catch {}
10201
10201
  const cleanExit = isTurnCompleteExit(session.jsonlPath);
10202
- if (code !== 0 && cleanExit) {
10202
+ // A DELIBERATE stop is not a failure, and the JSONL tail cannot tell us it
10203
+ // happened. Both writers (our idle scanner, and any stop RPC via cmdStop)
10204
+ // stamp intentionalStopAt before signalling; the death then lands here through
10205
+ // whichever path notices it (usually orphan-poll ESRCH \u2192 code -1). Verified
10206
+ // production case: the last line was a control_response (the reply to a
10207
+ // Walnut-issued control request \u2014 a routine shape), so isTurnCompleteExit said
10208
+ // false, exitCode stayed -1, and the projection painted a red "Error" over a
10209
+ // healthy, fully resumable session that was stopped on purpose. Checked FIRST
10210
+ // and unconditionally: the flag carries information the tail does not contain,
10211
+ // which is why isTurnCompleteExit is left alone.
10212
+ // Keep in sync with daemon-core.ts reapSession.
10213
+ const intentionalStop = session.intentionalStopAt != null;
10214
+ if (intentionalStop && code !== 0) {
10215
+ logMsg('info', 'reapSession: intentional stop, normalizing exit code', {
10216
+ sid, pid: session.pid, originalCode: code, originalReason: reason,
10217
+ jsonlAgeMs, cleanExit, intentionalStopAt: session.intentionalStopAt,
10218
+ });
10219
+ code = 0;
10220
+ reason = reason + '+intentional-stop';
10221
+ } else if (code !== 0 && cleanExit) {
10203
10222
  logMsg('info', 'reapSession: turn-complete detected, normalizing exit code', {
10204
10223
  sid, pid: session.pid, originalCode: code, originalReason: reason, jsonlAgeMs,
10205
10224
  });
@@ -13852,6 +13871,15 @@ function cmdStop(ws, id, cmd) {
13852
13871
  const pid = session.pid;
13853
13872
  logMsg('info', 'cmdStop: stopping session (process group kill)', { sid, pid });
13854
13873
 
13874
+ // Nobody sends stop by accident, so record the intent BEFORE the first signal:
13875
+ // the death lands asynchronously (orphan poll \u2192 ESRCH \u2192 code -1) and by then
13876
+ // nothing remembers this was asked for. This path is how the SERVER's idle
13877
+ // reaper kills (mgr.kill() \u2192 stop RPC), not the daemon's own scan, so it needs
13878
+ // its own stamp: a LOCAL session auto-stopped after 2h idle surfaced as
13879
+ // "Session ended unexpectedly and no cause was recorded" (2026-09-03).
13880
+ // Keep in sync with daemon-standalone.ts cmdStop.
13881
+ session.intentionalStopAt = Date.now();
13882
+
13855
13883
  // An explicit stop is a human decision \u2014 never undone by a pending auto-retry
13856
13884
  // respawning the CLI minutes later. Cancel the timer AND clear the streak so
13857
13885
  // an in-flight result line can't re-arm one either.
@@ -13888,7 +13916,7 @@ function cmdStop(ws, id, cmd) {
13888
13916
 
13889
13917
  // \u2500\u2500 Status \u2500\u2500
13890
13918
  function cmdStatus(ws, id, cmd) {
13891
- const { sid } = cmd;
13919
+ const { sid, includeArgs } = cmd;
13892
13920
  if (!sid) return sendError(ws, id, 'status: missing sid');
13893
13921
 
13894
13922
  const session = sessions.get(sid);
@@ -13919,6 +13947,11 @@ function cmdStatus(ws, id, cmd) {
13919
13947
  exitCode: session.exitCode,
13920
13948
  exitReason: session.exitReason,
13921
13949
  pendingCtrl: session.pendingCtrl,
13950
+ // Opt-in: the argv the live process was launched with (a fork copies it to
13951
+ // reproduce the parent's prompt-cache prefix). Off by default because the
13952
+ // health monitor polls status for every session and the argv carries the
13953
+ // multi-KB --append-system-prompt.
13954
+ ...(includeArgs && session.args && session.args.length > 0 ? { args: session.args } : {}),
13922
13955
  });
13923
13956
  }
13924
13957
 
@@ -15964,6 +15997,14 @@ function scanIdleSessions() {
15964
15997
  }
15965
15998
  const idleMinutes = Math.round(idleMs / 60000);
15966
15999
  logMsg('warn', 'idle scan: killing idle session (no subscribers, no output)', { sid, pid, idleMinutes, protectedBy: prot.source, thresholdMs: prot.killMs, detail: prot.detail });
16000
+ // Record the intent BEFORE signalling: the reap arrives asynchronously
16001
+ // (the orphan poll sees ESRCH and reports 'orphan-poll-dead', code -1), so
16002
+ // by then nothing remembers that WE asked for this. Without the stamp
16003
+ // reapSession can only consult the JSONL tail, which says "not a clean turn
16004
+ // end" for any session whose last line isn't a type:result \u2014 and the
16005
+ // projection then shows a red Error for our own routine reclamation.
16006
+ // Keep in sync with daemon-standalone.ts scanIdleSessions.
16007
+ session.intentionalStopAt = now;
15967
16008
  killSessionProcessGroup(pid, sid);
15968
16009
  }
15969
16010
  }
@@ -16647,7 +16688,20 @@ function createDaemonCore(deps) {
16647
16688
  jsonlAgeMs = clock() - fs114.statSync(session2.jsonlPath).mtimeMs;
16648
16689
  } catch {
16649
16690
  }
16650
- if (cleanExit && code !== 0) {
16691
+ const intentionalStop = session2.intentionalStopAt != null;
16692
+ if (intentionalStop && code !== 0) {
16693
+ logger2("info", "reapSession: intentional stop, normalizing exit code", {
16694
+ sid,
16695
+ pid: session2.pid,
16696
+ originalCode: code,
16697
+ originalReason: reason,
16698
+ jsonlAgeMs,
16699
+ cleanExit,
16700
+ intentionalStopAt: session2.intentionalStopAt
16701
+ });
16702
+ code = 0;
16703
+ reason = reason + "+intentional-stop";
16704
+ } else if (cleanExit && code !== 0) {
16651
16705
  logger2("info", "reapSession: turn-complete detected, normalizing exit code", {
16652
16706
  sid,
16653
16707
  pid: session2.pid,
@@ -17437,6 +17491,7 @@ var init_daemon_core = __esm({
17437
17491
  var claude_cli_detect_exports = {};
17438
17492
  __export(claude_cli_detect_exports, {
17439
17493
  _CLAUDE_CREDENTIALS_HOME: () => _CLAUDE_CREDENTIALS_HOME,
17494
+ describeClaudeCliAuth: () => describeClaudeCliAuth,
17440
17495
  detectClaudeCli: () => detectClaudeCli,
17441
17496
  hasClaudeSubscriptionAuth: () => hasClaudeSubscriptionAuth,
17442
17497
  isClaudeCliInstalled: () => isClaudeCliInstalled,
@@ -17497,9 +17552,32 @@ function detectClaudeCli() {
17497
17552
  return {
17498
17553
  installed: installed2,
17499
17554
  subscriptionAuth,
17500
- subscriptionReady: installed2 && subscriptionAuth
17555
+ subscriptionReady: installed2 && subscriptionAuth,
17556
+ ready: installed2,
17557
+ auth: installed2 ? describeClaudeCliAuth() : { mode: "unknown", label: "not installed" }
17501
17558
  };
17502
17559
  }
17560
+ function describeClaudeCliAuth(env3 = process.env, settingsEnv = readClaudeSettingsEnv()) {
17561
+ const merged = { ...env3, ...settingsEnv };
17562
+ const truthy = (v3) => !!v3 && v3 !== "0" && v3.toLowerCase() !== "false";
17563
+ if (truthy(merged.CLAUDE_CODE_USE_BEDROCK)) {
17564
+ const region = merged.AWS_REGION || merged.AWS_DEFAULT_REGION;
17565
+ return { mode: "bedrock", label: region ? `Bedrock (${region})` : "Bedrock" };
17566
+ }
17567
+ if (truthy(merged.CLAUDE_CODE_USE_VERTEX)) return { mode: "vertex", label: "Vertex AI" };
17568
+ if (merged.ANTHROPIC_API_KEY || merged.ANTHROPIC_AUTH_TOKEN) return { mode: "api-key", label: "an Anthropic API key" };
17569
+ if (hasClaudeSubscriptionAuth()) return { mode: "subscription", label: "your Claude subscription" };
17570
+ return { mode: "unknown", label: "its own login" };
17571
+ }
17572
+ function readClaudeSettingsEnv() {
17573
+ try {
17574
+ const raw = fs17.readFileSync(path18.join(os4.homedir(), ".claude", "settings.json"), "utf8");
17575
+ const env3 = JSON.parse(raw).env ?? {};
17576
+ return Object.fromEntries(Object.entries(env3).filter(([, v3]) => typeof v3 === "string"));
17577
+ } catch {
17578
+ return {};
17579
+ }
17580
+ }
17503
17581
  var KEYCHAIN_OAUTH_SERVICE, _CLAUDE_CREDENTIALS_HOME;
17504
17582
  var init_claude_cli_detect = __esm({
17505
17583
  "src/core/claude-cli-detect.ts"() {
@@ -18118,6 +18196,22 @@ var init_local_daemon = __esm({
18118
18196
  });
18119
18197
 
18120
18198
  // src/core/session-error-kind.ts
18199
+ var session_error_kind_exports = {};
18200
+ __export(session_error_kind_exports, {
18201
+ INFRA_STATUS_REASONS: () => INFRA_STATUS_REASONS,
18202
+ RESCUABLE_STOPPED_WINDOW_MS: () => RESCUABLE_STOPPED_WINDOW_MS,
18203
+ TERMINAL_STATUS_REASONS: () => TERMINAL_STATUS_REASONS,
18204
+ assertsHostUnreachable: () => assertsHostUnreachable,
18205
+ classifySessionError: () => classifySessionError,
18206
+ classifyStatusReasonKind: () => classifyStatusReasonKind,
18207
+ isInfraSessionError: () => isInfraSessionError,
18208
+ isRecoverableSessionError: () => isRecoverableSessionError,
18209
+ isRescuableStoppedRecord: () => isRescuableStoppedRecord
18210
+ });
18211
+ function assertsHostUnreachable(record) {
18212
+ if (!record.errorMessage) return false;
18213
+ return typeof record.status_reason === "string" && REACHABILITY_CLAIM_REASONS.has(record.status_reason);
18214
+ }
18121
18215
  function classifyStatusReasonKind(reason) {
18122
18216
  if (typeof reason !== "string") return void 0;
18123
18217
  if (INFRA_STATUS_REASONS.has(reason)) return "infra";
@@ -18146,7 +18240,7 @@ function isRescuableStoppedRecord(record, nowMs = Date.now()) {
18146
18240
  if (!Number.isFinite(changedAt)) return false;
18147
18241
  return nowMs - changedAt <= RESCUABLE_STOPPED_WINDOW_MS;
18148
18242
  }
18149
- var INFRA_STATUS_REASONS, TERMINAL_STATUS_REASONS, INFRA_TEXT_SIGNATURES, RESCUABLE_STOPPED_WINDOW_MS;
18243
+ var INFRA_STATUS_REASONS, TERMINAL_STATUS_REASONS, INFRA_TEXT_SIGNATURES, REACHABILITY_CLAIM_REASONS, RESCUABLE_STOPPED_WINDOW_MS;
18150
18244
  var init_session_error_kind = __esm({
18151
18245
  "src/core/session-error-kind.ts"() {
18152
18246
  "use strict";
@@ -18190,6 +18284,9 @@ var init_session_error_kind = __esm({
18190
18284
  "ssh tunnel",
18191
18285
  "broken pipe"
18192
18286
  ];
18287
+ REACHABILITY_CLAIM_REASONS = /* @__PURE__ */ new Set([
18288
+ "remote_unreachable"
18289
+ ]);
18193
18290
  RESCUABLE_STOPPED_WINDOW_MS = 24 * 60 * 60 * 1e3;
18194
18291
  }
18195
18292
  });
@@ -24670,7 +24767,7 @@ __export(credential_resolver_exports, {
24670
24767
  DEFAULT_REGION: () => DEFAULT_REGION,
24671
24768
  hasResolvableCredential: () => hasResolvableCredential,
24672
24769
  readClaudeCredentialExport: () => readClaudeCredentialExport,
24673
- readClaudeSettingsEnv: () => readClaudeSettingsEnv,
24770
+ readClaudeSettingsEnv: () => readClaudeSettingsEnv2,
24674
24771
  resolveCredentialFrom: () => resolveCredentialFrom,
24675
24772
  resolveCredentials: () => resolveCredentials,
24676
24773
  traceCredentialResolution: () => traceCredentialResolution
@@ -24825,7 +24922,7 @@ function traceCredentialResolution(inputs) {
24825
24922
  region: { value: winner.region ?? DEFAULT_REGION, source: regionSource }
24826
24923
  };
24827
24924
  }
24828
- function readClaudeSettingsEnv(settingsFile = CLAUDE_SETTINGS_FILE) {
24925
+ function readClaudeSettingsEnv2(settingsFile = CLAUDE_SETTINGS_FILE) {
24829
24926
  try {
24830
24927
  const raw = fs31.readFileSync(settingsFile, "utf-8");
24831
24928
  const parsed = JSON.parse(raw);
@@ -24865,7 +24962,7 @@ function probeAwsFiles() {
24865
24962
  function resolveCredentials(config) {
24866
24963
  const result = resolveCredentialFrom({
24867
24964
  config,
24868
- claudeEnv: readClaudeSettingsEnv(),
24965
+ claudeEnv: readClaudeSettingsEnv2(),
24869
24966
  processEnv: process.env,
24870
24967
  awsFiles: probeAwsFiles(),
24871
24968
  claudeCredentialExport: readClaudeCredentialExport()
@@ -67647,10 +67744,9 @@ function buildArgs(opts) {
67647
67744
  // Input stays default (text): we write the whole prompt to stdin and close.
67648
67745
  // (stream-json INPUT would force us into a JSON envelope for no gain here.)
67649
67746
  "--tools",
67650
- "",
67747
+ ""
67651
67748
  // disable ALL built-in tools (deny-all)
67652
- "--settings",
67653
- FORCE_SUBSCRIPTION_SETTINGS
67749
+ // No --settings override: the user's settings.json (and so their auth) applies as-is.
67654
67750
  ];
67655
67751
  let systemText = systemToText(opts.system).trim();
67656
67752
  if (opts.tools && opts.tools.length > 0) {
@@ -67679,7 +67775,7 @@ function systemToText(system) {
67679
67775
  }
67680
67776
  function buildSpawnEnv(base = process.env) {
67681
67777
  const env3 = { ...base };
67682
- for (const key2 of AUTH_ENV_TO_STRIP) {
67778
+ for (const key2 of ENV_TO_STRIP) {
67683
67779
  delete env3[key2];
67684
67780
  }
67685
67781
  env3.CLAUDE_CODE_ENTRYPOINT = WALNUT_UTILITY_ENTRYPOINT;
@@ -67741,7 +67837,7 @@ function normalizeCliModel(model) {
67741
67837
  }
67742
67838
  return void 0;
67743
67839
  }
67744
- var AUTH_ENV_TO_STRIP, FORCE_SUBSCRIPTION_SETTINGS, CLI_TURN_TIMEOUT_MS, SESSION_REUSE_TTL_MS, MAX_TRACKED_SESSIONS, ClaudeCliAdapter;
67840
+ var ENV_TO_STRIP, MAX_CONCURRENT_CLI, CLI_TURN_TIMEOUT_MS, SESSION_REUSE_TTL_MS, MAX_TRACKED_SESSIONS, ClaudeCliAdapter;
67745
67841
  var init_adapter_claude_cli = __esm({
67746
67842
  "src/agent/providers/adapter-claude-cli.ts"() {
67747
67843
  "use strict";
@@ -67751,24 +67847,11 @@ var init_adapter_claude_cli = __esm({
67751
67847
  init_logging();
67752
67848
  init_claude_cli_detect();
67753
67849
  init_claude_cli_protocol();
67754
- AUTH_ENV_TO_STRIP = [
67755
- "ANTHROPIC_API_KEY",
67756
- "ANTHROPIC_AUTH_TOKEN",
67757
- "ANTHROPIC_BASE_URL",
67758
- "AWS_BEARER_TOKEN_BEDROCK",
67759
- "AWS_ACCESS_KEY_ID",
67760
- "AWS_SECRET_ACCESS_KEY",
67761
- "AWS_SESSION_TOKEN",
67762
- "AWS_PROFILE",
67763
- "CLAUDE_CODE_USE_BEDROCK",
67764
- "CLAUDE_CODE_USE_VERTEX",
67765
- // Nested-session marker — inline-subagent strips this too so the child doesn't
67766
- // think it's running inside another Claude session.
67767
- "CLAUDECODE"
67768
- ];
67769
- FORCE_SUBSCRIPTION_SETTINGS = JSON.stringify({
67770
- env: { CLAUDE_CODE_USE_BEDROCK: "", CLAUDE_CODE_USE_VERTEX: "" }
67771
- });
67850
+ ENV_TO_STRIP = ["CLAUDECODE"];
67851
+ MAX_CONCURRENT_CLI = (() => {
67852
+ const n6 = Number(process.env.WALNUT_CLAUDE_CLI_CONCURRENCY);
67853
+ return Number.isFinite(n6) && n6 > 0 ? Math.floor(n6) : 3;
67854
+ })();
67772
67855
  CLI_TURN_TIMEOUT_MS = 12e4;
67773
67856
  SESSION_REUSE_TTL_MS = 30 * 60 * 1e3;
67774
67857
  MAX_TRACKED_SESSIONS = 50;
@@ -67776,6 +67859,25 @@ var init_adapter_claude_cli = __esm({
67776
67859
  protocol = "claude-cli";
67777
67860
  /** conversationKey → CLI session for --resume chaining. In-process only. */
67778
67861
  sessions = /* @__PURE__ */ new Map();
67862
+ /** Process gate: MAX_CONCURRENT_CLI turns at once, the rest queue in order. */
67863
+ inFlight = 0;
67864
+ waiters = [];
67865
+ async withSlot(fn) {
67866
+ if (this.inFlight >= MAX_CONCURRENT_CLI) {
67867
+ await new Promise((resolve) => this.waiters.push(resolve));
67868
+ }
67869
+ this.inFlight++;
67870
+ try {
67871
+ return await fn();
67872
+ } finally {
67873
+ this.inFlight--;
67874
+ this.waiters.shift()?.();
67875
+ }
67876
+ }
67877
+ /** Test hook: how many turns are running / waiting right now. */
67878
+ _gateStateForTesting() {
67879
+ return { inFlight: this.inFlight, waiting: this.waiters.length };
67880
+ }
67779
67881
  resetClient() {
67780
67882
  this.sessions.clear();
67781
67883
  }
@@ -67853,7 +67955,7 @@ User: ${overridePrompt}` : "");
67853
67955
  }
67854
67956
  if (!prompt.trim()) prompt = "(continue)";
67855
67957
  const args = [...buildArgs(opts), session2.flag, session2.id];
67856
- return this.execCli(command6, args, env3, prompt, opts.signal, onTextDelta).then((result) => {
67958
+ return this.withSlot(() => this.execCli(command6, args, env3, prompt, opts.signal, onTextDelta)).then((result) => {
67857
67959
  this.sessions.set(key2, {
67858
67960
  sessionId: session2.id,
67859
67961
  seenMessages: opts.messages.length,
@@ -67872,7 +67974,7 @@ User: ${overridePrompt}` : "");
67872
67974
  User: ${overridePrompt}` : "");
67873
67975
  const freshId = randomUUID3();
67874
67976
  const freshArgs = [...buildArgs(opts), "--session-id", freshId];
67875
- return this.execCli(command6, freshArgs, env3, freshPrompt || "(continue)", opts.signal, onTextDelta).then((result) => {
67977
+ return this.withSlot(() => this.execCli(command6, freshArgs, env3, freshPrompt || "(continue)", opts.signal, onTextDelta)).then((result) => {
67876
67978
  this.sessions.set(key2, {
67877
67979
  sessionId: freshId,
67878
67980
  seenMessages: opts.messages.length,
@@ -67956,7 +68058,7 @@ User: ${overridePrompt}` : "");
67956
68058
  }
67957
68059
  const resultText = finalResult?.result || accumulatedText;
67958
68060
  if (resultText && resultText.length < 300 && /^\s*failed to authenticate|^\s*api error: 4\d\d/i.test(resultText)) {
67959
- reject(new Error(`claude-cli subscription auth failed \u2014 run \`claude login\` to re-authenticate. CLI said: ${resultText.slice(0, 200)}`));
68061
+ reject(new Error(`claude-cli could not authenticate \u2014 run \`claude\` once in a terminal to sign in (or check its Bedrock settings). CLI said: ${resultText.slice(0, 200)}`));
67960
68062
  return;
67961
68063
  }
67962
68064
  if (code !== 0 && !accumulatedText && !finalResult) {
@@ -68156,6 +68258,50 @@ var init_registry = __esm({
68156
68258
  }
68157
68259
  });
68158
68260
 
68261
+ // src/agent/providers/default-provider.ts
68262
+ var default_provider_exports = {};
68263
+ __export(default_provider_exports, {
68264
+ CLAUDE_CLI_PROVIDER: () => CLAUDE_CLI_PROVIDER,
68265
+ LEGACY_DEFAULT_PROVIDER: () => LEGACY_DEFAULT_PROVIDER,
68266
+ _resetDefaultProviderCacheForTesting: () => _resetDefaultProviderCacheForTesting,
68267
+ hasExplicitBedrockConfig: () => hasExplicitBedrockConfig,
68268
+ mainProviderIsImplicit: () => mainProviderIsImplicit,
68269
+ resolveMainProviderName: () => resolveMainProviderName
68270
+ });
68271
+ function claudeCliInstalledCached() {
68272
+ const now = Date.now();
68273
+ if (!cliProbe || now - cliProbe.at > CLI_PROBE_TTL_MS) {
68274
+ cliProbe = { at: now, installed: isClaudeCliInstalled() };
68275
+ }
68276
+ return cliProbe.installed;
68277
+ }
68278
+ function _resetDefaultProviderCacheForTesting() {
68279
+ cliProbe = void 0;
68280
+ }
68281
+ function hasExplicitBedrockConfig(config) {
68282
+ const b8 = config.providers?.bedrock;
68283
+ if (b8 && (b8.bearer_token || b8.aws_access_key_id || b8.aws_profile || b8.aws_credential_export)) return true;
68284
+ return !!config.provider?.bedrock_bearer_token;
68285
+ }
68286
+ function resolveMainProviderName(config, claudeInstalled = claudeCliInstalledCached()) {
68287
+ if (config.agent?.main_provider) return config.agent.main_provider;
68288
+ if (claudeInstalled) return CLAUDE_CLI_PROVIDER;
68289
+ return LEGACY_DEFAULT_PROVIDER;
68290
+ }
68291
+ function mainProviderIsImplicit(config) {
68292
+ return !config.agent?.main_provider;
68293
+ }
68294
+ var LEGACY_DEFAULT_PROVIDER, CLAUDE_CLI_PROVIDER, CLI_PROBE_TTL_MS, cliProbe;
68295
+ var init_default_provider = __esm({
68296
+ "src/agent/providers/default-provider.ts"() {
68297
+ "use strict";
68298
+ init_claude_cli_detect();
68299
+ LEGACY_DEFAULT_PROVIDER = "bedrock";
68300
+ CLAUDE_CLI_PROVIDER = "claude_cli";
68301
+ CLI_PROBE_TTL_MS = 6e4;
68302
+ }
68303
+ });
68304
+
68159
68305
  // src/agent/model.ts
68160
68306
  var model_exports = {};
68161
68307
  __export(model_exports, {
@@ -68190,7 +68336,7 @@ function getContextThreshold(model, percent) {
68190
68336
  }
68191
68337
  async function resolveForCall(config) {
68192
68338
  const fullConfig = await getConfig();
68193
- const providerName = config?.provider ?? fullConfig.agent?.main_provider ?? "bedrock";
68339
+ const providerName = config?.provider ?? resolveMainProviderName(fullConfig);
68194
68340
  const hasExplicitProviders = fullConfig.providers && Object.keys(fullConfig.providers).length > 0;
68195
68341
  let providers = hasExplicitProviders ? buildProviderMap(fullConfig.providers, fullConfig) : synthesizeFromLegacy(fullConfig);
68196
68342
  const resolved = resolveProvider(providerName, providers);
@@ -68260,6 +68406,7 @@ var init_model = __esm({
68260
68406
  "use strict";
68261
68407
  init_config_manager();
68262
68408
  init_registry();
68409
+ init_default_provider();
68263
68410
  init_defaults();
68264
68411
  init_model_catalog();
68265
68412
  }
@@ -68462,8 +68609,22 @@ var init_token_truth = __esm({
68462
68609
  });
68463
68610
 
68464
68611
  // src/utils/image-compress.ts
68465
- import sharp from "sharp";
68612
+ async function loadSharp() {
68613
+ sharpLoad ??= import("sharp").then(
68614
+ (m6) => m6.default ?? m6,
68615
+ (err2) => {
68616
+ log.agent.warn("sharp is not installed; images are sent uncompressed", {
68617
+ error: err2 instanceof Error ? err2.message : String(err2),
68618
+ fix: "a machine with glibc 2.28+ installs sharp prebuilt; otherwise install libvips and re-run npm install"
68619
+ });
68620
+ return null;
68621
+ }
68622
+ );
68623
+ return sharpLoad;
68624
+ }
68466
68625
  async function clampImageDimensions(buffer2, mimeType) {
68626
+ const sharp = await loadSharp();
68627
+ if (!sharp) return { buffer: buffer2, mimeType };
68467
68628
  try {
68468
68629
  const meta = await sharp(buffer2).metadata();
68469
68630
  const longest = Math.max(meta.width ?? 0, meta.height ?? 0);
@@ -68485,6 +68646,8 @@ async function compressForApi(inputBuffer, inputMimeType) {
68485
68646
  if (buffer2.toString("base64").length <= MAX_BASE64_BYTES) {
68486
68647
  return { buffer: buffer2, mimeType };
68487
68648
  }
68649
+ const sharp = await loadSharp();
68650
+ if (!sharp) return { buffer: buffer2, mimeType };
68488
68651
  try {
68489
68652
  if (mimeType === "image/gif") {
68490
68653
  const webp = await sharp(buffer2, { animated: true }).webp({ quality: 80 }).toBuffer();
@@ -68522,10 +68685,11 @@ async function compressForApi(inputBuffer, inputMimeType) {
68522
68685
  return { buffer: buffer2, mimeType };
68523
68686
  }
68524
68687
  }
68525
- var MAX_BASE64_BYTES, MAX_IMAGE_DIMENSION;
68688
+ var sharpLoad, MAX_BASE64_BYTES, MAX_IMAGE_DIMENSION;
68526
68689
  var init_image_compress = __esm({
68527
68690
  "src/utils/image-compress.ts"() {
68528
68691
  "use strict";
68692
+ init_logging();
68529
68693
  MAX_BASE64_BYTES = 5e6;
68530
68694
  MAX_IMAGE_DIMENSION = 1568;
68531
68695
  }
@@ -69683,7 +69847,7 @@ __export(cheap_model_exports, {
69683
69847
  });
69684
69848
  function fastModelFor(config) {
69685
69849
  if (config.agent?.fast_model) return config.agent.fast_model;
69686
- const providerName = config.agent?.main_provider ?? "bedrock";
69850
+ const providerName = resolveMainProviderName(config);
69687
69851
  return MODEL_CATALOG[providerName]?.find(
69688
69852
  (model) => model.id.toLowerCase().includes("haiku")
69689
69853
  )?.id;
@@ -69695,6 +69859,7 @@ var init_cheap_model = __esm({
69695
69859
  "src/core/cheap-model.ts"() {
69696
69860
  "use strict";
69697
69861
  init_model_catalog();
69862
+ init_default_provider();
69698
69863
  }
69699
69864
  });
69700
69865
 
@@ -71514,11 +71679,35 @@ async function applySnapshot(sessionId, snapshot2, source) {
71514
71679
  if (projected !== "error") {
71515
71680
  updates.errorMessage = void 0;
71516
71681
  updates.errorKind = void 0;
71517
- clearSuppressedErrorReason(sessionId);
71682
+ const stashedStop = projected === "stopped" ? takeSuppressedErrorReason(sessionId) : null;
71683
+ if (stashedStop?.message) {
71684
+ updates.errorMessage = stashedStop.message;
71685
+ if (stashedStop.kind) updates.errorKind = stashedStop.kind;
71686
+ log.session.info("snapshot projection adopted suppressed stop reason", {
71687
+ sessionId,
71688
+ suppressedReason: stashedStop.reason,
71689
+ kind: stashedStop.kind ?? null,
71690
+ projected,
71691
+ source
71692
+ });
71693
+ } else {
71694
+ clearSuppressedErrorReason(sessionId);
71695
+ }
71518
71696
  } else {
71697
+ let droppedDisproven = false;
71698
+ if (assertsHostUnreachable(record)) {
71699
+ updates.errorMessage = void 0;
71700
+ droppedDisproven = true;
71701
+ log.session.info("snapshot projection dropped disproven unreachability diagnosis", {
71702
+ sessionId,
71703
+ priorReason: record.status_reason ?? null,
71704
+ projected,
71705
+ source
71706
+ });
71707
+ }
71519
71708
  const stashed = takeSuppressedErrorReason(sessionId);
71520
71709
  if (stashed) {
71521
- if (!record.errorMessage && stashed.message) updates.errorMessage = stashed.message;
71710
+ if ((!record.errorMessage || droppedDisproven) && stashed.message) updates.errorMessage = stashed.message;
71522
71711
  if (!record.errorKind && stashed.kind) updates.errorKind = stashed.kind;
71523
71712
  log.session.info("snapshot projection adopted suppressed error reason", {
71524
71713
  sessionId,
@@ -71571,6 +71760,7 @@ var init_session_snapshot_apply = __esm({
71571
71760
  "use strict";
71572
71761
  init_logging();
71573
71762
  init_session_snapshot_gate();
71763
+ init_session_error_kind();
71574
71764
  init_engine_registry();
71575
71765
  init_session_snapshot_gate();
71576
71766
  TERMINAL = /* @__PURE__ */ new Set(["stopped", "error"]);
@@ -74185,9 +74375,15 @@ var init_acp_session = __esm({
74185
74375
  }
74186
74376
  async resolveArtifacts() {
74187
74377
  if (this.cfg.artifacts) return this.cfg.artifacts;
74188
- return resolveAcpArtifacts(this.engineId, {
74378
+ const artifacts = resolveAcpArtifacts(this.engineId, {
74189
74379
  configuredAdapterCmd: await this.resolveConfiguredAdapterCmd()
74190
74380
  });
74381
+ if (!fsModule.existsSync(artifacts.workerCmd[1])) {
74382
+ throw new Error(
74383
+ `Engine '${this.engineId}' runs on the ACP worker, which is not built (${artifacts.workerCmd[1]}). Install Bun (https://bun.sh) and run \`npm run build:daemon\`, or install Walnut from npm, which ships it.`
74384
+ );
74385
+ }
74386
+ return artifacts;
74191
74387
  }
74192
74388
  async establishProviderSession() {
74193
74389
  await this.seedReplayCursor();
@@ -75570,6 +75766,26 @@ var init_transcript_chain = __esm({
75570
75766
  }
75571
75767
  });
75572
75768
 
75769
+ // src/core/sessions/side-thread-warmup.ts
75770
+ function isCacheWarmupText(text) {
75771
+ return text.startsWith(CACHE_WARMUP_TAG);
75772
+ }
75773
+ function markWarmupTurnPending(sessionId) {
75774
+ pendingWarmupTurns.add(sessionId);
75775
+ }
75776
+ function consumeWarmupTurn(sessionId) {
75777
+ return pendingWarmupTurns.delete(sessionId);
75778
+ }
75779
+ var CACHE_WARMUP_TAG, CACHE_WARMUP_MESSAGE, pendingWarmupTurns;
75780
+ var init_side_thread_warmup = __esm({
75781
+ "src/core/sessions/side-thread-warmup.ts"() {
75782
+ "use strict";
75783
+ CACHE_WARMUP_TAG = "<walnut-cache-warmup>";
75784
+ CACHE_WARMUP_MESSAGE = `${CACHE_WARMUP_TAG}This is a cache warm-up from the tool hosting this side thread, not a user message. Reply with exactly one word: Ready. Use no tools and add nothing else.</walnut-cache-warmup>`;
75785
+ pendingWarmupTurns = /* @__PURE__ */ new Set();
75786
+ }
75787
+ });
75788
+
75573
75789
  // src/core/history-disk-cache.ts
75574
75790
  var history_disk_cache_exports = {};
75575
75791
  __export(history_disk_cache_exports, {
@@ -77724,11 +77940,13 @@ __export(session_lifecycle_exports, {
77724
77940
  persistSessionModeChange: () => persistSessionModeChange,
77725
77941
  readProviderSessionHistory: () => readProviderSessionHistory,
77726
77942
  readSessionRichHistory: () => readSessionRichHistory,
77943
+ recheckSession: () => recheckSession,
77727
77944
  respondSessionPermission: () => respondSessionPermission,
77728
77945
  restartSession: () => restartSession,
77729
77946
  retrySession: () => retrySession,
77730
77947
  terminateSession: () => terminateSession
77731
77948
  });
77949
+ import { randomUUID as randomUUID4 } from "crypto";
77732
77950
  async function readProviderSessionHistory(sessionId, record, nativeHost, skipSubagents = true, opts) {
77733
77951
  if (record && engineCaps(record.engine).historySource === "acp-journal") {
77734
77952
  const { readAcpSessionHistoryState: readAcpSessionHistoryState2 } = await Promise.resolve().then(() => (init_acp_session_history(), acp_session_history_exports));
@@ -77839,6 +78057,33 @@ async function changeSessionMode(existingRecord, sessionId, mode, updates = {})
77839
78057
  });
77840
78058
  return updated;
77841
78059
  }
78060
+ function normalizePinQuote(value) {
78061
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
78062
+ throw new SessionControlError("pinned_messages[].quote must be an object", 400);
78063
+ }
78064
+ const quote = value;
78065
+ const exact = quote.exact;
78066
+ if (typeof exact !== "string" || !exact.trim() || exact.length > MAX_PIN_QUOTE_CHARS) {
78067
+ throw new SessionControlError(
78068
+ `pinned_messages[].quote.exact must be a non-empty string (max ${MAX_PIN_QUOTE_CHARS} chars)`,
78069
+ 400
78070
+ );
78071
+ }
78072
+ for (const side of ["prefix", "suffix"]) {
78073
+ const context = quote[side];
78074
+ if (context !== void 0 && context !== null && (typeof context !== "string" || context.length > MAX_PIN_QUOTE_CONTEXT_CHARS)) {
78075
+ throw new SessionControlError(
78076
+ `pinned_messages[].quote.${side} must be a string (max ${MAX_PIN_QUOTE_CONTEXT_CHARS} chars)`,
78077
+ 400
78078
+ );
78079
+ }
78080
+ }
78081
+ return {
78082
+ exact,
78083
+ ...typeof quote.prefix === "string" && quote.prefix ? { prefix: quote.prefix } : {},
78084
+ ...typeof quote.suffix === "string" && quote.suffix ? { suffix: quote.suffix } : {}
78085
+ };
78086
+ }
77842
78087
  function normalizePinnedMessages(value) {
77843
78088
  if (!Array.isArray(value)) {
77844
78089
  throw new SessionControlError("pinned_messages must be an array", 400);
@@ -77847,7 +78092,9 @@ function normalizePinnedMessages(value) {
77847
78092
  throw new SessionControlError(`pinned_messages holds at most ${MAX_PINNED_MESSAGES} entries`, 400);
77848
78093
  }
77849
78094
  const out = [];
77850
- const seen = /* @__PURE__ */ new Set();
78095
+ const seenWholeMessage = /* @__PURE__ */ new Set();
78096
+ const seenQuoteId = /* @__PURE__ */ new Set();
78097
+ const seenQuoteShape = /* @__PURE__ */ new Set();
77851
78098
  for (const raw of value) {
77852
78099
  if (!raw || typeof raw !== "object") {
77853
78100
  throw new SessionControlError("each pinned_messages entry must be an object", 400);
@@ -77857,13 +78104,40 @@ function normalizePinnedMessages(value) {
77857
78104
  if (typeof msgId !== "string" || !msgId.trim() || msgId.length > MAX_PIN_ID_CHARS) {
77858
78105
  throw new SessionControlError("pinned_messages[].msgId must be a non-empty string", 400);
77859
78106
  }
77860
- if (seen.has(msgId)) continue;
77861
- seen.add(msgId);
78107
+ const rawId = entry.id;
78108
+ if (rawId !== void 0 && rawId !== null && (typeof rawId !== "string" || !rawId.trim() || rawId.length > MAX_PIN_ENTRY_ID_CHARS)) {
78109
+ throw new SessionControlError(
78110
+ `pinned_messages[].id must be a non-empty string (max ${MAX_PIN_ENTRY_ID_CHARS} chars)`,
78111
+ 400
78112
+ );
78113
+ }
78114
+ const id = typeof rawId === "string" ? rawId : void 0;
78115
+ const quote = entry.quote === void 0 || entry.quote === null ? void 0 : normalizePinQuote(entry.quote);
78116
+ let pinId = id;
78117
+ if (quote) {
78118
+ if (id && seenQuoteId.has(id)) continue;
78119
+ const shape = [msgId, quote.exact, quote.prefix ?? "", quote.suffix ?? ""].join("\0");
78120
+ if (seenQuoteShape.has(shape)) continue;
78121
+ pinId = id ?? randomUUID4();
78122
+ seenQuoteId.add(pinId);
78123
+ seenQuoteShape.add(shape);
78124
+ } else {
78125
+ if (seenWholeMessage.has(msgId)) continue;
78126
+ seenWholeMessage.add(msgId);
78127
+ }
77862
78128
  const role = entry.role === "user" || entry.role === "assistant" || entry.role === "system" ? entry.role : "assistant";
77863
78129
  const label = typeof entry.label === "string" ? entry.label.trim().slice(0, MAX_PIN_LABEL_CHARS) : "";
77864
78130
  const timestamp = typeof entry.timestamp === "string" ? entry.timestamp : void 0;
77865
78131
  const pinnedAt = typeof entry.pinnedAt === "string" ? entry.pinnedAt : (/* @__PURE__ */ new Date()).toISOString();
77866
- out.push({ msgId, label, role, ...timestamp ? { timestamp } : {}, pinnedAt });
78132
+ out.push({
78133
+ msgId,
78134
+ label,
78135
+ role,
78136
+ ...timestamp ? { timestamp } : {},
78137
+ pinnedAt,
78138
+ ...pinId ? { id: pinId } : {},
78139
+ ...quote ? { quote } : {}
78140
+ });
77867
78141
  }
77868
78142
  return out;
77869
78143
  }
@@ -78061,7 +78335,7 @@ async function retrySession(sessionId) {
78061
78335
  return { status: "reconnected", sessionId };
78062
78336
  }
78063
78337
  if (await resumeConversationExists(record)) {
78064
- const { sendMessageToSession: sendMessageToSession2, getQueue: getQueue2, unparkMessage: unparkMessage2 } = await Promise.resolve().then(() => (init_session_message_queue(), session_message_queue_exports));
78338
+ const { getQueue: getQueue2, unparkMessage: unparkMessage2 } = await Promise.resolve().then(() => (init_session_message_queue(), session_message_queue_exports));
78065
78339
  const pendingMsgs = await getQueue2(sessionId);
78066
78340
  for (const parked of pendingMsgs.filter((m6) => m6.status === "parked")) {
78067
78341
  await unparkMessage2(sessionId, parked.id);
@@ -78071,9 +78345,22 @@ async function retrySession(sessionId) {
78071
78345
  log.session.info("session retry: re-processing pending queue messages", { sessionId, taskId: record.taskId, count: pendingMsgs.length });
78072
78346
  return { status: "resuming", sessionId, restoredMessages: pendingMsgs.length };
78073
78347
  }
78074
- await sendMessageToSession2(sessionId, "continue", { source: "retry", taskId: record.taskId });
78075
- log.session.info("session retry: resuming via --resume (no pending messages)", { sessionId, taskId: record.taskId });
78076
- return { status: "resuming", sessionId };
78348
+ const relabelled = await updateSessionRecord2(sessionId, {
78349
+ process_status: "stopped",
78350
+ errorMessage: void 0,
78351
+ errorKind: void 0,
78352
+ activity: void 0,
78353
+ last_status_change: (/* @__PURE__ */ new Date()).toISOString(),
78354
+ status_reason: "retry_reconnect",
78355
+ status_changed_by: "user"
78356
+ });
78357
+ emitSessionStatusChanged2(relabelled, {}, ["*"]);
78358
+ log.session.info("session retry: reconnected \u2014 conversation preserved, no message sent", {
78359
+ sessionId,
78360
+ taskId: record.taskId,
78361
+ host: record.host ?? "__local__"
78362
+ });
78363
+ return { status: "resumable", sessionId };
78077
78364
  }
78078
78365
  log.session.warn("session retry: conversation never persisted \u2014 starting fresh instead of --resume", {
78079
78366
  sessionId,
@@ -78131,6 +78418,91 @@ async function retrySession(sessionId) {
78131
78418
  });
78132
78419
  return { status: "pending", taskId: task.id, oldSessionId: sessionId };
78133
78420
  }
78421
+ async function withDeadline(p6, ms) {
78422
+ let timer3;
78423
+ try {
78424
+ return await Promise.race([
78425
+ p6,
78426
+ new Promise((resolve) => {
78427
+ timer3 = setTimeout(() => resolve(RECHECK_TIMED_OUT), ms);
78428
+ if (timer3 && typeof timer3 === "object" && "unref" in timer3) timer3.unref();
78429
+ })
78430
+ ]);
78431
+ } finally {
78432
+ if (timer3) clearTimeout(timer3);
78433
+ }
78434
+ }
78435
+ async function recheckSession(sessionId) {
78436
+ const { getSessionByClaudeId: getSessionByClaudeId2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
78437
+ const record = await getSessionByClaudeId2(sessionId);
78438
+ if (!record) throw new SessionControlError("Session not found", 404);
78439
+ const hostKey2 = record.host ?? "__local__";
78440
+ const { classifySessionError: classifySessionError2 } = await Promise.resolve().then(() => (init_session_error_kind(), session_error_kind_exports));
78441
+ const kind = classifySessionError2(record);
78442
+ const base = { sessionId, processStatus: record.process_status, infraClaim: kind === "infra" };
78443
+ const { isDaemonConnected: isDaemonConnected2, getPooledSnapshotConnection: getPooledSnapshotConnection2 } = await Promise.resolve().then(() => (init_daemon_connection(), daemon_connection_exports));
78444
+ const reachable = isDaemonConnected2(hostKey2);
78445
+ if (kind === "terminal") return { ...base, checked: false, reachable, reason: "terminal_error" };
78446
+ if (record.archived) return { ...base, checked: false, reachable, reason: "archived" };
78447
+ const conn = getPooledSnapshotConnection2(record.host);
78448
+ if (!conn) {
78449
+ log.session.info("session recheck: no pooled snapshot connection \u2014 nothing to ask", {
78450
+ sessionId,
78451
+ host: hostKey2,
78452
+ reachable
78453
+ });
78454
+ return { ...base, checked: false, reachable, reason: "no_pooled_connection" };
78455
+ }
78456
+ let raced;
78457
+ try {
78458
+ raced = await withDeadline(
78459
+ conn.send("getState", { sid: sessionId }, RECHECK_RPC_TIMEOUT_MS),
78460
+ RECHECK_RPC_TIMEOUT_MS
78461
+ );
78462
+ } catch (err2) {
78463
+ log.session.info("session recheck: getState failed", {
78464
+ sessionId,
78465
+ host: hostKey2,
78466
+ error: err2 instanceof Error ? err2.message : String(err2)
78467
+ });
78468
+ return { ...base, checked: false, reachable, reason: "rpc_failed" };
78469
+ }
78470
+ if (raced === RECHECK_TIMED_OUT) {
78471
+ log.session.warn("session recheck: getState timed out", {
78472
+ sessionId,
78473
+ host: hostKey2,
78474
+ timeoutMs: RECHECK_RPC_TIMEOUT_MS
78475
+ });
78476
+ return { ...base, checked: false, reachable, reason: "timeout" };
78477
+ }
78478
+ const snapshot2 = raced?.ok ? raced.snapshot : void 0;
78479
+ if (!snapshot2) {
78480
+ return { ...base, checked: false, reachable: true, reason: "no_snapshot" };
78481
+ }
78482
+ const { applySnapshot: applySnapshot2 } = await Promise.resolve().then(() => (init_session_snapshot_apply(), session_snapshot_apply_exports));
78483
+ const outcome = await applySnapshot2(sessionId, snapshot2, "recheck-open");
78484
+ const after = await getSessionByClaudeId2(sessionId);
78485
+ const processStatus = after?.process_status ?? record.process_status;
78486
+ log.session.info("session recheck: reconciled against host", {
78487
+ sessionId,
78488
+ host: hostKey2,
78489
+ previous: record.process_status,
78490
+ processStatus,
78491
+ cliState: snapshot2.cliState,
78492
+ applyOutcome: outcome.outcome
78493
+ });
78494
+ return {
78495
+ sessionId,
78496
+ checked: true,
78497
+ reachable: true,
78498
+ alive: snapshot2.cliState !== "dead",
78499
+ processStatus,
78500
+ // Re-classify against the CURRENT record: a converged row may no longer
78501
+ // carry the claim, and the client uses this to decide whether the sentence
78502
+ // on screen is still true.
78503
+ infraClaim: classifySessionError2(after ?? record) === "infra"
78504
+ };
78505
+ }
78134
78506
  async function respondSessionPermission(sessionId, requestId, allow, denyMessage, optionId, answers) {
78135
78507
  if (typeof requestId !== "string" || !requestId || typeof allow !== "boolean") {
78136
78508
  throw new SessionControlError("requestId (string) and allow (boolean) are required", 400);
@@ -78424,7 +78796,7 @@ async function readSessionRichHistory(sessionId, tail) {
78424
78796
  ...historyWindowed ? { windowed: true } : {}
78425
78797
  };
78426
78798
  }
78427
- var CLAUDE_SESSION_MODES, MAX_PINNED_MESSAGES, MAX_PIN_LABEL_CHARS, MAX_PIN_ID_CHARS, GIT_BASES, MAX_FORK_DEPTH, HISTORY_STARTUP_GRACE_MS;
78799
+ var CLAUDE_SESSION_MODES, MAX_PINNED_MESSAGES, MAX_PIN_LABEL_CHARS, MAX_PIN_ID_CHARS, MAX_PIN_ENTRY_ID_CHARS, MAX_PIN_QUOTE_CHARS, MAX_PIN_QUOTE_CONTEXT_CHARS, RECHECK_RPC_TIMEOUT_MS, RECHECK_TIMED_OUT, GIT_BASES, MAX_FORK_DEPTH, HISTORY_STARTUP_GRACE_MS;
78428
78800
  var init_session_lifecycle = __esm({
78429
78801
  "src/core/sessions/session-lifecycle.ts"() {
78430
78802
  "use strict";
@@ -78438,6 +78810,11 @@ var init_session_lifecycle = __esm({
78438
78810
  MAX_PINNED_MESSAGES = 200;
78439
78811
  MAX_PIN_LABEL_CHARS = 300;
78440
78812
  MAX_PIN_ID_CHARS = 200;
78813
+ MAX_PIN_ENTRY_ID_CHARS = 64;
78814
+ MAX_PIN_QUOTE_CHARS = 2e3;
78815
+ MAX_PIN_QUOTE_CONTEXT_CHARS = 64;
78816
+ RECHECK_RPC_TIMEOUT_MS = 5e3;
78817
+ RECHECK_TIMED_OUT = /* @__PURE__ */ Symbol("recheck-timeout");
78441
78818
  GIT_BASES = /* @__PURE__ */ new Set(["uncommitted", "previous", "remote"]);
78442
78819
  MAX_FORK_DEPTH = 5;
78443
78820
  HISTORY_STARTUP_GRACE_MS = 2 * 60 * 1e3;
@@ -78738,7 +79115,7 @@ __export(compact_inject_exports, {
78738
79115
  extractSessionMeta: () => extractSessionMeta,
78739
79116
  injectCompactBoundary: () => injectCompactBoundary
78740
79117
  });
78741
- import { randomUUID as randomUUID4 } from "crypto";
79118
+ import { randomUUID as randomUUID5 } from "crypto";
78742
79119
  import fsp26 from "fs/promises";
78743
79120
  async function extractSessionMeta(jsonlPath) {
78744
79121
  const content = await fsp26.readFile(jsonlPath, "utf-8");
@@ -78790,8 +79167,8 @@ async function injectCompactBoundary(jsonlPath, summary, meta) {
78790
79167
  log.session.error("Could not find last message UUID in JSONL", { jsonlPath });
78791
79168
  return null;
78792
79169
  }
78793
- const boundaryUuid = randomUUID4();
78794
- const summaryUuid = randomUUID4();
79170
+ const boundaryUuid = randomUUID5();
79171
+ const summaryUuid = randomUUID5();
78795
79172
  const now = /* @__PURE__ */ new Date();
78796
79173
  const boundaryTs = now.toISOString();
78797
79174
  const summaryTs = new Date(now.getTime() + 1e3).toISOString();
@@ -87820,7 +88197,7 @@ function isRevealSecretPath(resolved) {
87820
88197
  ];
87821
88198
  return denied.some((d8) => resolved === d8 || resolved.startsWith(d8 + path70.sep)) || /(^|\/)config\.ya?ml$/.test(resolved);
87822
88199
  }
87823
- var filesRouter, MAX_ENTRIES2, REMOTE_TIMEOUT_MS3, REMOTE_RESOLVE_BUDGET_MS, FilesOpError, MAX_UPWARD_LEVELS2, MAX_DOWNWARD_DEPTH, REFERENCE_SEARCH_BUDGET_MS, SYMBOL_RE2, REVEAL_APP_DENY_EXTS;
88200
+ var filesRouter, MAX_ENTRIES2, REMOTE_TIMEOUT_MS3, REMOTE_RESOLVE_BUDGET_MS, FilesOpError, MAX_UPWARD_LEVELS2, MAX_DOWNWARD_DEPTH, REFERENCE_SEARCH_BUDGET_MS, SYMBOL_RE2, LIST_MANY_MAX_PATHS, LIST_MANY_CONCURRENCY, LIST_MANY_DEADLINE_MS, REVEAL_APP_DENY_EXTS;
87824
88201
  var init_files2 = __esm({
87825
88202
  "src/web/routes/files.ts"() {
87826
88203
  "use strict";
@@ -87953,6 +88330,54 @@ var init_files2 = __esm({
87953
88330
  next(err2);
87954
88331
  }
87955
88332
  });
88333
+ LIST_MANY_MAX_PATHS = 64;
88334
+ LIST_MANY_CONCURRENCY = 8;
88335
+ LIST_MANY_DEADLINE_MS = 8e3;
88336
+ filesRouter.post("/list-many", async (req, res, next) => {
88337
+ try {
88338
+ const body3 = req.body ?? {};
88339
+ const raw = Array.isArray(body3.paths) ? body3.paths : null;
88340
+ if (!raw) {
88341
+ res.status(400).json({ error: "paths (string[]) is required" });
88342
+ return;
88343
+ }
88344
+ const host = typeof body3.host === "string" && body3.host ? body3.host : void 0;
88345
+ const showHidden = body3.showHidden === true || body3.showHidden === "1";
88346
+ const paths = [...new Set(
88347
+ raw.filter((p6) => typeof p6 === "string" && !!p6)
88348
+ )];
88349
+ const truncated = paths.length > LIST_MANY_MAX_PATHS;
88350
+ const wanted = paths.slice(0, LIST_MANY_MAX_PATHS);
88351
+ const deadline4 = Date.now() + LIST_MANY_DEADLINE_MS;
88352
+ const listings = [];
88353
+ let timedOut = false;
88354
+ let next_ = 0;
88355
+ const worker = async () => {
88356
+ for (; ; ) {
88357
+ const i8 = next_++;
88358
+ if (i8 >= wanted.length) return;
88359
+ if (Date.now() >= deadline4) {
88360
+ timedOut = true;
88361
+ return;
88362
+ }
88363
+ const dirPath = wanted[i8];
88364
+ try {
88365
+ listings.push(await listSessionFiles(dirPath, host, showHidden));
88366
+ } catch (err2) {
88367
+ listings.push({ path: dirPath, error: err2 instanceof Error ? err2.message : String(err2) });
88368
+ }
88369
+ }
88370
+ };
88371
+ await Promise.all(Array.from({ length: Math.min(LIST_MANY_CONCURRENCY, wanted.length) }, worker));
88372
+ res.json({ listings, truncated, timedOut });
88373
+ } catch (err2) {
88374
+ if (err2 instanceof FilesOpError) {
88375
+ res.status(err2.statusCode).json({ error: err2.message });
88376
+ return;
88377
+ }
88378
+ next(err2);
88379
+ }
88380
+ });
87956
88381
  REVEAL_APP_DENY_EXTS = /* @__PURE__ */ new Set([
87957
88382
  "command",
87958
88383
  "sh",
@@ -89527,6 +89952,82 @@ var init_task = __esm({
89527
89952
  }
89528
89953
  });
89529
89954
 
89955
+ // src/core/sessions/spawn-prefix.ts
89956
+ var spawn_prefix_exports = {};
89957
+ __export(spawn_prefix_exports, {
89958
+ MAX_SPAWN_PROMPT_BYTES: () => MAX_SPAWN_PROMPT_BYTES,
89959
+ parseSpawnPrefixFromArgs: () => parseSpawnPrefixFromArgs,
89960
+ readParentSpawnPrefix: () => readParentSpawnPrefix,
89961
+ spawnPrefixFromRecord: () => spawnPrefixFromRecord
89962
+ });
89963
+ function flagValue(args, flag) {
89964
+ const i8 = args.indexOf(flag);
89965
+ if (i8 < 0 || i8 + 1 >= args.length) return void 0;
89966
+ const v3 = args[i8 + 1];
89967
+ return v3 !== void 0 && !v3.startsWith("--") ? v3 : void 0;
89968
+ }
89969
+ function parseSpawnPrefixFromArgs(args) {
89970
+ const prompt = flagValue(args, "--append-system-prompt");
89971
+ return {
89972
+ appendSystemPrompt: prompt !== void 0 && Buffer.byteLength(prompt, "utf8") <= MAX_SPAWN_PROMPT_BYTES ? prompt : null,
89973
+ ...flagValue(args, "--model") ? { model: flagValue(args, "--model") } : {},
89974
+ ...EFFORTS.includes(flagValue(args, "--effort") ?? "") ? { effort: flagValue(args, "--effort") } : {},
89975
+ ...flagValue(args, "--permission-mode") ? { permissionMode: flagValue(args, "--permission-mode") } : {},
89976
+ source: "live-process"
89977
+ };
89978
+ }
89979
+ function spawnPrefixFromRecord(record) {
89980
+ const stored = record.appliedAppendSystemPrompt;
89981
+ if (stored === void 0 || stored === null) return { appendSystemPrompt: null, source: "unknown" };
89982
+ const usable = stored.length > 0 && Buffer.byteLength(stored, "utf8") <= MAX_SPAWN_PROMPT_BYTES;
89983
+ return { appendSystemPrompt: usable ? stored : null, source: "record" };
89984
+ }
89985
+ async function defaultLiveArgsReader(hostKey2, sessionId) {
89986
+ const { probeDaemonSessionArgs: probeDaemonSessionArgs2 } = await Promise.resolve().then(() => (init_daemon_connection(), daemon_connection_exports));
89987
+ return probeDaemonSessionArgs2(hostKey2, sessionId);
89988
+ }
89989
+ async function readParentSpawnPrefix(parent, deps = {}) {
89990
+ const readLiveArgs = deps.readLiveArgs ?? defaultLiveArgsReader;
89991
+ const hostKey2 = parent.host || "__local__";
89992
+ let live = null;
89993
+ try {
89994
+ live = await readLiveArgs(hostKey2, parent.claudeSessionId);
89995
+ } catch (err2) {
89996
+ log.session.debug("spawn prefix: live argv read threw", {
89997
+ sessionId: parent.claudeSessionId,
89998
+ error: err2 instanceof Error ? err2.message : String(err2)
89999
+ });
90000
+ }
90001
+ if (live && live.length > 0) {
90002
+ const prefix = parseSpawnPrefixFromArgs(live);
90003
+ const remembered = parent.appliedAppendSystemPrompt ?? void 0;
90004
+ const actual = prefix.appendSystemPrompt ?? "";
90005
+ if (remembered !== actual) {
90006
+ const persist4 = deps.persist ?? (async (sid, prompt) => {
90007
+ const { updateSessionRecord: updateSessionRecord2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
90008
+ return updateSessionRecord2(sid, { appliedAppendSystemPrompt: prompt });
90009
+ });
90010
+ void persist4(parent.claudeSessionId, actual).catch((err2) => {
90011
+ log.session.warn("spawn prefix: backfill of applied prompt failed", {
90012
+ sessionId: parent.claudeSessionId,
90013
+ error: err2 instanceof Error ? err2.message : String(err2)
90014
+ });
90015
+ });
90016
+ }
90017
+ return prefix;
90018
+ }
90019
+ return spawnPrefixFromRecord(parent);
90020
+ }
90021
+ var EFFORTS, MAX_SPAWN_PROMPT_BYTES;
90022
+ var init_spawn_prefix = __esm({
90023
+ "src/core/sessions/spawn-prefix.ts"() {
90024
+ "use strict";
90025
+ init_logging();
90026
+ EFFORTS = ["low", "medium", "high", "xhigh", "max"];
90027
+ MAX_SPAWN_PROMPT_BYTES = 65536;
90028
+ }
90029
+ });
90030
+
89530
90031
  // src/core/sessions/side-thread-fork.ts
89531
90032
  var side_thread_fork_exports = {};
89532
90033
  __export(side_thread_fork_exports, {
@@ -89583,6 +90084,10 @@ async function forkSideThreadSession(parentSid, threadId, opts) {
89583
90084
  const sessionId = crypto18.randomUUID();
89584
90085
  const lane = sideThreadLaneKey(parentSid, threadId);
89585
90086
  const mode = parent.mode !== "default" ? parent.mode : void 0;
90087
+ const { readParentSpawnPrefix: readParentSpawnPrefix2 } = await Promise.resolve().then(() => (init_spawn_prefix(), spawn_prefix_exports));
90088
+ const prefix = await readParentSpawnPrefix2(parent);
90089
+ const cliModel = prefix.model ?? parent.cliModel;
90090
+ const effort = prefix.effort ?? parent.effort;
89586
90091
  await createSessionRecord(sessionId, "", parent.project ?? "", parent.cwd, {
89587
90092
  title,
89588
90093
  ...mode ? { mode } : {},
@@ -89590,8 +90095,10 @@ async function forkSideThreadSession(parentSid, threadId, opts) {
89590
90095
  // The thread inherits the parent's launch bundle verbatim, re-applied from
89591
90096
  // the record on every cold resume.
89592
90097
  ...parent.profile ? { profile: parent.profile } : {},
89593
- ...parent.effort ? { effort: parent.effort } : {},
89594
- ...parent.cliModel ? { cliModel: parent.cliModel } : {},
90098
+ ...effort ? { effort } : {},
90099
+ ...cliModel ? { cliModel } : {},
90100
+ // '' = the parent runs without an append prompt, and so does the thread.
90101
+ appliedAppendSystemPrompt: prefix.appendSystemPrompt ?? "",
89595
90102
  lane,
89596
90103
  forkedFromSessionId: resumeFrom,
89597
90104
  initialProcessStatus: "idle",
@@ -89607,11 +90114,12 @@ async function forkSideThreadSession(parentSid, threadId, opts) {
89607
90114
  ...mode ? { mode } : {},
89608
90115
  ...parent.host ? { host: parent.host } : {},
89609
90116
  ...parent.profile ? { profile: parent.profile } : {},
89610
- ...parent.effort ? { effort: parent.effort } : {},
90117
+ ...effort ? { effort } : {},
89611
90118
  // Must be `cliModel` (the parent's verbatim --model arg, [1m] marker
89612
90119
  // included): the CLI does NOT inherit a model across --fork-session, and
89613
90120
  // the reported `model` has lost the marker.
89614
- ...parent.cliModel ? { model: parent.cliModel } : {},
90121
+ ...cliModel ? { model: cliModel } : {},
90122
+ appendSystemPromptExact: prefix.appendSystemPrompt,
89615
90123
  lane,
89616
90124
  forkedFromSessionId: resumeFrom
89617
90125
  }, ["session-runner"], { source: "side-thread-fork" });
@@ -89620,7 +90128,9 @@ async function forkSideThreadSession(parentSid, threadId, opts) {
89620
90128
  threadId,
89621
90129
  sessionId,
89622
90130
  resumeFrom,
89623
- initOnly: !opts?.message
90131
+ initOnly: !opts?.message,
90132
+ prefixSource: prefix.source,
90133
+ prefixPromptLength: prefix.appendSystemPrompt?.length ?? 0
89624
90134
  });
89625
90135
  return { sessionId, resumeFromSessionId: resumeFrom };
89626
90136
  }
@@ -115492,11 +116002,12 @@ __export(session_controls_exports, {
115492
116002
  applySessionModelChange: () => applySessionModelChange,
115493
116003
  computeModelOptions: () => computeModelOptions,
115494
116004
  controlErrorKind: () => controlErrorKind,
116005
+ createForkSiblingTask: () => createForkSiblingTask,
115495
116006
  forkSessionToTask: () => forkSessionToTask,
115496
116007
  handleSessionControlRelay: () => handleSessionControlRelay,
115497
116008
  modelReadBackMatches: () => modelReadBackMatches
115498
116009
  });
115499
- import { randomUUID as randomUUID5 } from "crypto";
116010
+ import { randomUUID as randomUUID6 } from "crypto";
115500
116011
  function controlErrorKind(status2) {
115501
116012
  if (status2 === 404) return "not_found";
115502
116013
  if (status2 === 405) return "method_not_allowed";
@@ -115669,6 +116180,124 @@ async function computeModelOptions(sessionId) {
115669
116180
  currentEffort: liveEffort ?? record.effectiveEffort ?? record.effort ?? null
115670
116181
  };
115671
116182
  }
116183
+ async function createForkSiblingTask(sourceTaskId, opts) {
116184
+ const {
116185
+ getTask: getTask2,
116186
+ addTask: addTask2,
116187
+ updateTask: updateTask2,
116188
+ groupTasks: groupTasks2,
116189
+ addToGroup: addToGroup2,
116190
+ renameGroup: renameGroup2
116191
+ } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
116192
+ let sourceTask;
116193
+ try {
116194
+ sourceTask = await getTask2(sourceTaskId);
116195
+ } catch {
116196
+ throw new SessionControlError(`Source task "${sourceTaskId}" not found`, 404);
116197
+ }
116198
+ const sourceBaseTitle = sourceTask.title.replace(/^Fork of\s+/i, "").replace(/\s+-\s+fork of\s+.*$/i, "").trim() || sourceTask.title;
116199
+ const explicitTitle = opts.childTitle || (opts.titlePrefix ? `${opts.titlePrefix} - fork of ${sourceBaseTitle}` : void 0);
116200
+ const autoTitle = !explicitTitle;
116201
+ const newTitle = explicitTitle || `Fork of ${sourceBaseTitle}`;
116202
+ let inheritedTier;
116203
+ if (sourceTask.pinned && sourceTask.focus_tier) {
116204
+ const { getCustomTiers: getCustomTiers2 } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
116205
+ const known = ["focus", "backlog", "wait"].includes(sourceTask.focus_tier) || (await getCustomTiers2()).some((t3) => t3.id === sourceTask.focus_tier);
116206
+ if (known) inheritedTier = sourceTask.focus_tier;
116207
+ else {
116208
+ log.session.warn("fork: source tier is not registered, filing the fork in Satellite", {
116209
+ sourceTaskId: sourceTask.id,
116210
+ tier: sourceTask.focus_tier
116211
+ });
116212
+ }
116213
+ }
116214
+ const { task: newFork } = await addTask2({
116215
+ title: newTitle,
116216
+ project: sourceTask.project || "",
116217
+ source: sourceTask.source,
116218
+ ...opts.description ? { description: opts.description } : {},
116219
+ // A fork is a person splitting off live work, so it joins the board like
116220
+ // any other hand-made task (Satellite = pinned, no stored tier).
116221
+ pinned: true,
116222
+ ...inheritedTier ? { focus_tier: inheritedTier } : {},
116223
+ // Forking an Ask Walnut conversation yields another Ask Walnut
116224
+ // conversation: without the flag the fork loses the amber marker AND the
116225
+ // persona drift repair (both key on task.walnut_agent, never on project).
116226
+ ...sourceTask.walnut_agent ? { walnut_agent: true } : {}
116227
+ });
116228
+ let forkGroupId;
116229
+ try {
116230
+ if (sourceTask.group_id) {
116231
+ const r8 = await addToGroup2(sourceTask.group_id, [newFork.id]);
116232
+ forkGroupId = r8.group_id;
116233
+ } else {
116234
+ const r8 = await groupTasks2([sourceTask.id, newFork.id], sourceTask.title);
116235
+ forkGroupId = r8.group_id;
116236
+ }
116237
+ } catch (err2) {
116238
+ log.session.warn("fork: failed to group source + fork", {
116239
+ sourceTaskId: sourceTask.id,
116240
+ forkTaskId: newFork.id,
116241
+ error: err2 instanceof Error ? err2.message : String(err2)
116242
+ });
116243
+ }
116244
+ let task;
116245
+ try {
116246
+ task = await getTask2(newFork.id);
116247
+ } catch {
116248
+ task = newFork;
116249
+ }
116250
+ bus.emit(EventNames.TASK_CREATED, { task }, ["web-ui", "main-agent"], { source: opts.source });
116251
+ if (autoTitle && opts.prompt?.trim()) {
116252
+ const forkId = newFork.id;
116253
+ const sourceTitle = sourceBaseTitle;
116254
+ const placeholderTitle = newTitle;
116255
+ const prompt = opts.prompt;
116256
+ const titleSource = `${opts.source}-title`;
116257
+ void (async () => {
116258
+ try {
116259
+ const { summarizeForkPrompt: summarizeForkPrompt2 } = await Promise.resolve().then(() => (init_fork_title(), fork_title_exports));
116260
+ const label = await summarizeForkPrompt2(prompt);
116261
+ if (!label) return;
116262
+ const current5 = await getTask2(forkId);
116263
+ if (current5.title !== placeholderTitle) {
116264
+ log.session.info("fork title refine skipped \u2014 title changed since fork", { taskId: forkId });
116265
+ return;
116266
+ }
116267
+ const refinedTitle = `${label} - fork of ${sourceTitle}`;
116268
+ const { task: updated } = await updateTask2(forkId, { title: refinedTitle }, { source: titleSource });
116269
+ bus.emit(EventNames.TASK_UPDATED, { task: updated }, ["web-ui", "main-agent"], { source: titleSource });
116270
+ log.session.info("fork title refined", { taskId: forkId, title: refinedTitle });
116271
+ } catch (err2) {
116272
+ log.session.warn("fork title refine failed", {
116273
+ taskId: forkId,
116274
+ error: err2 instanceof Error ? err2.message : String(err2)
116275
+ });
116276
+ }
116277
+ })();
116278
+ }
116279
+ if (forkGroupId && !sourceTask.group_id) {
116280
+ const gid = forkGroupId;
116281
+ const seedTitles = [sourceTask.title, newTitle];
116282
+ const groupSource = opts.source;
116283
+ void (async () => {
116284
+ try {
116285
+ const { summarizeGroupLabel: summarizeGroupLabel2 } = await Promise.resolve().then(() => (init_fork_title(), fork_title_exports));
116286
+ const label = await summarizeGroupLabel2(seedTitles);
116287
+ if (!label) return;
116288
+ await renameGroup2(gid, label);
116289
+ bus.emit(EventNames.TASK_GROUPS_CHANGED, { group_id: gid, label }, ["web-ui", "main-agent"], { source: groupSource });
116290
+ log.session.info("fork group label refined", { groupId: gid, label });
116291
+ } catch (err2) {
116292
+ log.session.warn("fork group label refine failed", {
116293
+ groupId: gid,
116294
+ error: err2 instanceof Error ? err2.message : String(err2)
116295
+ });
116296
+ }
116297
+ })();
116298
+ }
116299
+ return { task, ...forkGroupId ? { groupId: forkGroupId } : {} };
116300
+ }
115672
116301
  async function forkSessionToTask(sourceSessionId, input, source = "web-api") {
115673
116302
  const { task_id, create_child_task, child_title, message, title, model, imageContext } = input;
115674
116303
  if (!task_id && !create_child_task) {
@@ -115678,14 +116307,7 @@ async function forkSessionToTask(sourceSessionId, input, source = "web-api") {
115678
116307
  throw new SessionControlError("task_id and create_child_task are mutually exclusive", 400);
115679
116308
  }
115680
116309
  const { getSessionByClaudeId: getSessionByClaudeId2, getSessionsForTask: getSessionsForTask2, createSessionRecord: createSessionRecord2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
115681
- const {
115682
- getTask: getTask2,
115683
- addTask: addTask2,
115684
- updateTask: updateTask2,
115685
- groupTasks: groupTasks2,
115686
- addToGroup: addToGroup2,
115687
- renameGroup: renameGroup2
115688
- } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
116310
+ const { getTask: getTask2 } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
115689
116311
  const sourceRecord = await getSessionByClaudeId2(sourceSessionId);
115690
116312
  if (!sourceRecord) throw new SessionControlError("Source session not found", 404);
115691
116313
  if (!engineCaps(sourceRecord.engine).fork) {
@@ -115702,108 +116324,13 @@ async function forkSessionToTask(sourceSessionId, input, source = "web-api") {
115702
116324
  if (!sourceRecord.taskId) {
115703
116325
  throw new SessionControlError("Source session has no task \u2014 cannot create fork task", 400);
115704
116326
  }
115705
- let sourceTask;
115706
- try {
115707
- sourceTask = await getTask2(sourceRecord.taskId);
115708
- } catch {
115709
- throw new SessionControlError(`Source task "${sourceRecord.taskId}" not found`, 404);
115710
- }
115711
- const sourceBaseTitle = sourceTask.title.replace(/^Fork of\s+/i, "").replace(/\s+-\s+fork of\s+.*$/i, "").trim() || sourceTask.title;
115712
- const autoTitle = !child_title;
115713
- const newTitle = child_title || `Fork of ${sourceBaseTitle}`;
115714
- let inheritedTier;
115715
- if (sourceTask.pinned && sourceTask.focus_tier) {
115716
- const { getCustomTiers: getCustomTiers2 } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
115717
- const known = ["focus", "backlog", "wait"].includes(sourceTask.focus_tier) || (await getCustomTiers2()).some((t3) => t3.id === sourceTask.focus_tier);
115718
- if (known) inheritedTier = sourceTask.focus_tier;
115719
- else {
115720
- log.session.warn("fork: source tier is not registered, filing the fork in Satellite", {
115721
- sourceTaskId: sourceTask.id,
115722
- tier: sourceTask.focus_tier
115723
- });
115724
- }
115725
- }
115726
- const { task: newFork } = await addTask2({
115727
- title: newTitle,
115728
- project: sourceTask.project || "",
115729
- source: sourceTask.source,
115730
- // A fork is a person splitting off live work, so it joins the board like
115731
- // any other hand-made task (Satellite = pinned, no stored tier).
115732
- pinned: true,
115733
- ...inheritedTier ? { focus_tier: inheritedTier } : {},
115734
- // Forking an Ask Walnut conversation yields another Ask Walnut
115735
- // conversation: without the flag the fork loses the amber marker AND the
115736
- // persona drift repair (both key on task.walnut_agent, never on project).
115737
- ...sourceTask.walnut_agent ? { walnut_agent: true } : {}
115738
- });
115739
- let forkGroupId;
115740
- try {
115741
- if (sourceTask.group_id) {
115742
- const r8 = await addToGroup2(sourceTask.group_id, [newFork.id]);
115743
- forkGroupId = r8.group_id;
115744
- } else {
115745
- const r8 = await groupTasks2([sourceTask.id, newFork.id], sourceTask.title);
115746
- forkGroupId = r8.group_id;
115747
- }
115748
- } catch (err2) {
115749
- log.session.warn("fork: failed to group source + fork", {
115750
- sourceTaskId: sourceTask.id,
115751
- forkTaskId: newFork.id,
115752
- error: err2 instanceof Error ? err2.message : String(err2)
115753
- });
115754
- }
115755
- try {
115756
- task = await getTask2(newFork.id);
115757
- } catch {
115758
- task = newFork;
115759
- }
115760
- bus.emit(EventNames.TASK_CREATED, { task }, ["web-ui", "main-agent"], { source: "fork" });
116327
+ const created = await createForkSiblingTask(sourceRecord.taskId, {
116328
+ childTitle: child_title,
116329
+ prompt: message,
116330
+ source: "fork"
116331
+ });
116332
+ task = created.task;
115761
116333
  childTaskCreated = true;
115762
- if (autoTitle && message?.trim()) {
115763
- const forkId = newFork.id;
115764
- const sourceTitle = sourceBaseTitle;
115765
- const placeholderTitle = newTitle;
115766
- void (async () => {
115767
- try {
115768
- const { summarizeForkPrompt: summarizeForkPrompt2 } = await Promise.resolve().then(() => (init_fork_title(), fork_title_exports));
115769
- const label = await summarizeForkPrompt2(message);
115770
- if (!label) return;
115771
- const current5 = await getTask2(forkId);
115772
- if (current5.title !== placeholderTitle) {
115773
- log.session.info("fork title refine skipped \u2014 title changed since fork", { taskId: forkId });
115774
- return;
115775
- }
115776
- const refinedTitle = `${label} - fork of ${sourceTitle}`;
115777
- const { task: updated } = await updateTask2(forkId, { title: refinedTitle }, { source: "fork-title" });
115778
- bus.emit(EventNames.TASK_UPDATED, { task: updated }, ["web-ui", "main-agent"], { source: "fork-title" });
115779
- log.session.info("fork title refined", { taskId: forkId, title: refinedTitle });
115780
- } catch (err2) {
115781
- log.session.warn("fork title refine failed", {
115782
- taskId: forkId,
115783
- error: err2 instanceof Error ? err2.message : String(err2)
115784
- });
115785
- }
115786
- })();
115787
- }
115788
- if (forkGroupId && !sourceTask.group_id) {
115789
- const gid = forkGroupId;
115790
- const seedTitles = [sourceTask.title, newTitle];
115791
- void (async () => {
115792
- try {
115793
- const { summarizeGroupLabel: summarizeGroupLabel2 } = await Promise.resolve().then(() => (init_fork_title(), fork_title_exports));
115794
- const label = await summarizeGroupLabel2(seedTitles);
115795
- if (!label) return;
115796
- await renameGroup2(gid, label);
115797
- bus.emit(EventNames.TASK_GROUPS_CHANGED, { group_id: gid, label }, ["web-ui", "main-agent"], { source: "fork" });
115798
- log.session.info("fork group label refined", { groupId: gid, label });
115799
- } catch (err2) {
115800
- log.session.warn("fork group label refine failed", {
115801
- groupId: gid,
115802
- error: err2 instanceof Error ? err2.message : String(err2)
115803
- });
115804
- }
115805
- })();
115806
- }
115807
116334
  } else {
115808
116335
  try {
115809
116336
  task = await getTask2(task_id);
@@ -115825,7 +116352,7 @@ async function forkSessionToTask(sourceSessionId, input, source = "web-api") {
115825
116352
  const FORK_FOCUS_PREFIX = "This is a forked session. Focus on the NEW request below \u2014 treat it as your primary task. Do not resume or continue the parent session's previous work unless the user explicitly asks you to.\n\n";
115826
116353
  const forkMessage = `${FORK_FOCUS_PREFIX}${imageContext ?? ""}New request:
115827
116354
  ${userRequest}`;
115828
- const forkSessionId = randomUUID5();
116355
+ const forkSessionId = randomUUID6();
115829
116356
  const forkTitle = title ?? `Fork of ${sourceRecord.title ?? sourceSessionId.slice(0, 16)}`;
115830
116357
  try {
115831
116358
  await createSessionRecord2(forkSessionId, task.id, task.project ?? "", sourceRecord.cwd, {
@@ -115976,6 +116503,11 @@ async function handleSessionControlRelay(action, sessionId, params) {
115976
116503
  result = await retrySession2(sessionId);
115977
116504
  break;
115978
116505
  }
116506
+ case "recheck": {
116507
+ const { recheckSession: recheckSession2 } = await Promise.resolve().then(() => (init_session_lifecycle(), session_lifecycle_exports));
116508
+ result = await recheckSession2(sessionId);
116509
+ break;
116510
+ }
115979
116511
  case "permission": {
115980
116512
  const { respondSessionPermission: respondSessionPermission2 } = await Promise.resolve().then(() => (init_session_lifecycle(), session_lifecycle_exports));
115981
116513
  result = await respondSessionPermission2(sessionId, p6.requestId, p6.allow, p6.message, p6.optionId, p6.answers);
@@ -116543,7 +117075,7 @@ __export(session_rewind_exports, {
116543
117075
  probeRewindViaDaemon: () => probeRewindViaDaemon,
116544
117076
  rewindSessionToMessage: () => rewindSessionToMessage
116545
117077
  });
116546
- import { randomUUID as randomUUID6 } from "crypto";
117078
+ import { randomUUID as randomUUID7 } from "crypto";
116547
117079
  function cutAncestorHistoryAtRewindPoint(ancestorMessages, rewoundAtMessageUuid) {
116548
117080
  if (!rewoundAtMessageUuid) {
116549
117081
  return { messages: ancestorMessages, found: false, dropped: 0 };
@@ -116744,7 +117276,7 @@ async function rewindSessionToMessage(sessionId, input, source = "web-api") {
116744
117276
  error: err2 instanceof Error ? err2.message : String(err2)
116745
117277
  });
116746
117278
  }
116747
- const rewoundId = randomUUID6();
117279
+ const rewoundId = randomUUID7();
116748
117280
  const baseTitle = (record.title ?? sessionId.slice(0, 16)).replace(/\s*\(rewound\)$/i, "");
116749
117281
  const title = `${baseTitle} (rewound)`.slice(0, 200);
116750
117282
  const { createSessionRecord: createSessionRecord2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
@@ -117464,6 +117996,7 @@ function parseSessionMessages(content, opts) {
117464
117996
  }
117465
117997
  }
117466
117998
  let lastPlanContent = null;
117999
+ let dropWarmupReply = false;
117467
118000
  const resultParentIds = [];
117468
118001
  const result = [];
117469
118002
  for (const [mapMsgId, msg] of messageMap) {
@@ -117541,10 +118074,18 @@ ${imgPath}` : imgPath;
117541
118074
  let text = textParts.join("\n").trim();
117542
118075
  if (!text && tools2.length === 0 && !thinking) continue;
117543
118076
  if (msg.role === "user" && text.startsWith("<")) {
118077
+ if (isCacheWarmupText(text)) {
118078
+ dropWarmupReply = true;
118079
+ continue;
118080
+ }
117544
118081
  const transformed = transformInjectedUserText(text);
117545
118082
  if (transformed === null) continue;
117546
118083
  if (transformed !== void 0) text = transformed;
117547
118084
  }
118085
+ if (msg.role === "assistant" && dropWarmupReply) {
118086
+ dropWarmupReply = false;
118087
+ if (tools2.length === 0) continue;
118088
+ }
117548
118089
  if (msg.role === "user") text = stripOutputModeWrappers(text);
117549
118090
  if (msg.role === "assistant" && !text && tools2.length === 0) continue;
117550
118091
  result.push({
@@ -118495,6 +119036,7 @@ var init_session_history = __esm({
118495
119036
  init_session_io();
118496
119037
  init_constants();
118497
119038
  init_remote_image_mirror();
119039
+ init_side_thread_warmup();
118498
119040
  init_session_file_reader();
118499
119041
  LOCAL_HOME = os19.homedir();
118500
119042
  TAIL_SEGMENT_ROLL_BYTES = 4 * 1024 * 1024;
@@ -118787,7 +119329,7 @@ var init_context_window = __esm({
118787
119329
  });
118788
119330
 
118789
119331
  // src/providers/turn-ledger.ts
118790
- import { randomUUID as randomUUID7 } from "crypto";
119332
+ import { randomUUID as randomUUID8 } from "crypto";
118791
119333
  function openTurn(sessionId) {
118792
119334
  const existing = openTurns.get(sessionId);
118793
119335
  if (existing && !existing.settled) {
@@ -118797,7 +119339,7 @@ function openTurn(sessionId) {
118797
119339
  });
118798
119340
  return { turnId: existing.turnId, promise: existing.promise };
118799
119341
  }
118800
- const turnId = randomUUID7();
119342
+ const turnId = randomUUID8();
118801
119343
  let resolve;
118802
119344
  let reject;
118803
119345
  const promise = new Promise((res, rej) => {
@@ -126791,11 +127333,39 @@ var init_claude_code_session = __esm({
126791
127333
  session2.pendingDescription = message.slice(0, 500);
126792
127334
  let appendSystemPrompt;
126793
127335
  const isFork = !!data.forkedFromSessionId;
126794
- if (data.appendSystemPrompt && !isFork) {
126795
- appendSystemPrompt = data.appendSystemPrompt;
126796
- log.session.info("using caller-provided system prompt", { taskId: taskId2, promptLength: data.appendSystemPrompt.length });
126797
- }
126798
- {
127336
+ const { parseSideLaneKey: parseSideLaneKey2 } = await Promise.resolve().then(() => (init_side_thread_fork(), side_thread_fork_exports));
127337
+ const sideIds = parseSideLaneKey2(data.lane);
127338
+ if (sideIds) {
127339
+ let exact = data.appendSystemPromptExact;
127340
+ let source = "live-or-record";
127341
+ if (exact === void 0) {
127342
+ try {
127343
+ const { getSessionByClaudeId: getSessionByClaudeId2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
127344
+ const { spawnPrefixFromRecord: spawnPrefixFromRecord2 } = await Promise.resolve().then(() => (init_spawn_prefix(), spawn_prefix_exports));
127345
+ const parentRecord = await getSessionByClaudeId2(sideIds.parentSid);
127346
+ const prefix = parentRecord ? spawnPrefixFromRecord2(parentRecord) : null;
127347
+ exact = prefix?.appendSystemPrompt ?? null;
127348
+ source = prefix?.source ?? "no-parent-record";
127349
+ } catch (err2) {
127350
+ exact = null;
127351
+ source = "lookup-failed";
127352
+ log.session.warn("side thread: parent prompt lookup failed \u2014 spawning without an append prompt", {
127353
+ parentSid: sideIds.parentSid,
127354
+ error: err2 instanceof Error ? err2.message : String(err2)
127355
+ });
127356
+ }
127357
+ }
127358
+ appendSystemPrompt = exact ?? void 0;
127359
+ log.session.info("side thread: prefix prompt resolved", {
127360
+ parentSid: sideIds.parentSid,
127361
+ source,
127362
+ promptLength: appendSystemPrompt?.length ?? 0
127363
+ });
127364
+ } else {
127365
+ if (data.appendSystemPrompt && !isFork) {
127366
+ appendSystemPrompt = data.appendSystemPrompt;
127367
+ log.session.info("using caller-provided system prompt", { taskId: taskId2, promptLength: data.appendSystemPrompt.length });
127368
+ }
126799
127369
  try {
126800
127370
  const { buildSessionContext: buildSessionContext2 } = await Promise.resolve().then(() => (init_session_context(), session_context_exports));
126801
127371
  const ctx = await buildSessionContext2(taskId2 ?? "", cwd, data.host);
@@ -126813,32 +127383,6 @@ ${ctx.systemPrompt}` : ctx.systemPrompt;
126813
127383
  log.session.warn("failed to build session context", { taskId: taskId2, error: err2 instanceof Error ? err2.message : String(err2) });
126814
127384
  }
126815
127385
  }
126816
- {
126817
- const { parseSideLaneKey: parseSideLaneKey2 } = await Promise.resolve().then(() => (init_side_thread_fork(), side_thread_fork_exports));
126818
- const sideIds = parseSideLaneKey2(data.lane);
126819
- if (sideIds) {
126820
- try {
126821
- const { getSessionByClaudeId: getSessionByClaudeId2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
126822
- const parentRecord = await getSessionByClaudeId2(sideIds.parentSid);
126823
- const inherited = parentRecord?.appliedAppendSystemPrompt;
126824
- if (inherited && Buffer.byteLength(inherited, "utf8") <= 65536) {
126825
- appendSystemPrompt = inherited;
126826
- log.session.info("side thread: inherited parent system prompt verbatim", {
126827
- parentSid: sideIds.parentSid,
126828
- promptLength: appendSystemPrompt.length
126829
- });
126830
- } else {
126831
- log.session.info("side thread: parent has no stored system prompt \u2014 fresh build (cache prefix will diverge)", {
126832
- parentSid: sideIds.parentSid
126833
- });
126834
- }
126835
- } catch (err2) {
126836
- log.session.warn("side thread: parent prompt lookup failed \u2014 fresh build", {
126837
- error: err2 instanceof Error ? err2.message : String(err2)
126838
- });
126839
- }
126840
- }
126841
- }
126842
127386
  if (data.preassignedSessionId && appendSystemPrompt && Buffer.byteLength(appendSystemPrompt, "utf8") <= 65536) {
126843
127387
  const promptToStore = appendSystemPrompt;
126844
127388
  const sidToStore = data.preassignedSessionId;
@@ -128038,7 +128582,7 @@ __export(session_requests_exports, {
128038
128582
  settleNotified: () => settleNotified,
128039
128583
  settleReplied: () => settleReplied
128040
128584
  });
128041
- import { createHash as createHash14, randomUUID as randomUUID8 } from "crypto";
128585
+ import { createHash as createHash14, randomUUID as randomUUID9 } from "crypto";
128042
128586
  import path119 from "path";
128043
128587
  function oneLine2(text, max = 160) {
128044
128588
  const flat = text.replace(/\s+/g, " ").trim();
@@ -128062,7 +128606,7 @@ function clampReplyTimeoutSecs(secs) {
128062
128606
  }
128063
128607
  async function createSessionRequest(input) {
128064
128608
  const request2 = {
128065
- id: `rq-${randomUUID8().replace(/-/g, "").slice(0, 12)}`,
128609
+ id: `rq-${randomUUID9().replace(/-/g, "").slice(0, 12)}`,
128066
128610
  fromSessionId: input.fromSessionId,
128067
128611
  ...input.toSessionId ? { toSessionId: input.toSessionId } : {},
128068
128612
  ...input.toTaskId ? { toTaskId: input.toTaskId } : {},
@@ -137021,7 +137565,7 @@ __export(mobile_launch_exports, {
137021
137565
  performMobileLaunch: () => performMobileLaunch,
137022
137566
  validateMobileLaunchBody: () => validateMobileLaunchBody
137023
137567
  });
137024
- import { randomUUID as randomUUID9 } from "crypto";
137568
+ import { randomUUID as randomUUID10 } from "crypto";
137025
137569
  function launchErrorCode(status2) {
137026
137570
  if (status2 === 404) return "not_found";
137027
137571
  if (status2 === 409) return "conflict";
@@ -137143,7 +137687,7 @@ async function performMobileLaunch(input, source) {
137143
137687
  );
137144
137688
  }
137145
137689
  }
137146
- const preassignedSessionId = engineCaps(input.engine).idProvisioning === "provider-issued" ? void 0 : randomUUID9();
137690
+ const preassignedSessionId = engineCaps(input.engine).idProvisioning === "provider-issued" ? void 0 : randomUUID10();
137147
137691
  const task = await quickStartSession({
137148
137692
  message: input.message,
137149
137693
  cwd: input.cwd,
@@ -137942,6 +138486,7 @@ __export(daemon_connection_exports, {
137942
138486
  isDaemonConnected: () => isDaemonConnected,
137943
138487
  isPooledConnection: () => isPooledConnection,
137944
138488
  probeDaemonSession: () => probeDaemonSession,
138489
+ probeDaemonSessionArgs: () => probeDaemonSessionArgs,
137945
138490
  pushDaemonHooksToAllHosts: () => pushDaemonHooksToAllHosts,
137946
138491
  setOnDaemonHostConnected: () => setOnDaemonHostConnected,
137947
138492
  setOnDaemonStatusChange: () => setOnDaemonStatusChange,
@@ -138118,6 +138663,28 @@ async function probeDaemonSession(hostKey2, sessionId) {
138118
138663
  return null;
138119
138664
  }
138120
138665
  }
138666
+ async function probeDaemonSessionArgs(hostKey2, sessionId, deadlineMs = 2e3) {
138667
+ const conn = getConnectedDaemonConnection(hostKey2);
138668
+ if (!conn) return null;
138669
+ try {
138670
+ const result = await Promise.race([
138671
+ conn.send("status", { sid: sessionId, includeArgs: true }),
138672
+ new Promise((_, reject) => setTimeout(
138673
+ () => reject(new Error(`status probe exceeded ${deadlineMs}ms`)),
138674
+ deadlineMs
138675
+ ).unref?.())
138676
+ ]);
138677
+ if (!result.ok || !result.alive || !Array.isArray(result.args)) return null;
138678
+ return result.args.filter((a8) => typeof a8 === "string");
138679
+ } catch (err2) {
138680
+ log.session.debug("probeDaemonSessionArgs: status probe failed", {
138681
+ hostKey: hostKey2,
138682
+ sessionId,
138683
+ error: err2 instanceof Error ? err2.message : String(err2)
138684
+ });
138685
+ return null;
138686
+ }
138687
+ }
138121
138688
  var execFileAsync11, MOBILE_ENQUEUE_LEDGER_MAX, recentMobileEnqueues, DaemonConnection, onPoolStatusChange, onHostConnected, hostConnectedListeners, connectionPool, connectingPromises, failureCache, FAILURE_CACHE_TTL_MS;
138122
138689
  var init_daemon_connection = __esm({
138123
138690
  "src/providers/daemon-connection.ts"() {
@@ -143463,6 +144030,7 @@ async function createSessionRecord(claudeSessionId, taskId2, project, cwd, extra
143463
144030
  ...extra?.effort ? { effort: extra.effort } : {},
143464
144031
  ...extra?.profile ? { profile: extra.profile } : {},
143465
144032
  ...extra?.lane ? { lane: extra.lane } : {},
144033
+ ...extra?.appliedAppendSystemPrompt !== void 0 ? { appliedAppendSystemPrompt: extra.appliedAppendSystemPrompt } : {},
143466
144034
  ...extra?.engine ? { engine: extra.engine } : {},
143467
144035
  ...extra?.acpRuntimeId ? { acpRuntimeId: extra.acpRuntimeId } : {},
143468
144036
  ...extra?.acpJournalPath ? { acpJournalPath: extra.acpJournalPath } : {},
@@ -143617,7 +144185,7 @@ function applyUpdateToSession(session2, updates, logLabel) {
143617
144185
  };
143618
144186
  if (count2 === 1) log.session.info("legacy status write suppressed", payload);
143619
144187
  else log.session.debug("legacy status write suppressed", payload);
143620
- if (updates.process_status === "error") {
144188
+ if (updates.process_status === "error" || updates.process_status === "stopped") {
143621
144189
  noteSuppressedErrorReason(session2.claudeSessionId, {
143622
144190
  reason: typeof reason === "string" ? reason : "unknown",
143623
144191
  message: typeof updates.errorMessage === "string" ? updates.errorMessage : void 0,
@@ -151566,14 +152134,16 @@ function isLive(r8) {
151566
152134
  function isRevivable(r8) {
151567
152135
  return !!r8.outputFile || r8.consumedOffset !== void 0;
151568
152136
  }
151569
- var STANDBY_TTL_MS, MAX_LIVE_THREADS, IDLE_SWEEP_INTERVAL_MS, THREAD_IDLE_MS, SideThreadManager, sideThreadManager;
152137
+ var STANDBY_TTL_MS, WARMED_STANDBY_TTL_MS, MAX_LIVE_THREADS, IDLE_SWEEP_INTERVAL_MS, THREAD_IDLE_MS, SideThreadManager, sideThreadManager;
151570
152138
  var init_side_thread_manager = __esm({
151571
152139
  "src/core/sessions/side-thread-manager.ts"() {
151572
152140
  "use strict";
151573
152141
  init_logging();
151574
152142
  init_side_thread_fork();
151575
152143
  init_session_controls();
152144
+ init_side_thread_warmup();
151576
152145
  STANDBY_TTL_MS = 12e4;
152146
+ WARMED_STANDBY_TTL_MS = 15 * 6e4;
151577
152147
  MAX_LIVE_THREADS = 3;
151578
152148
  IDLE_SWEEP_INTERVAL_MS = 5 * 6e4;
151579
152149
  THREAD_IDLE_MS = 30 * 6e4;
@@ -151584,6 +152154,8 @@ var init_side_thread_manager = __esm({
151584
152154
  * In-memory only: a server restart loses it, but the boot sweep archives
151585
152155
  * every standby anyway, so a record can never outlive its map entry. */
151586
152156
  standbyParentOffsets = /* @__PURE__ */ new Map();
152157
+ /** parentSid → standby sid that already ran its cache warm-up turn. */
152158
+ warmedStandbys = /* @__PURE__ */ new Map();
151587
152159
  /** One in-flight standby/create op per parent — two concurrent asks must not
151588
152160
  * both consume the same standby (they would collide on its lane rename). */
151589
152161
  inFlight = /* @__PURE__ */ new Map();
@@ -151615,6 +152187,7 @@ var init_side_thread_manager = __esm({
151615
152187
  for (const timer3 of this.standbyTimers.values()) clearTimeout(timer3);
151616
152188
  this.standbyTimers.clear();
151617
152189
  this.standbyParentOffsets.clear();
152190
+ this.warmedStandbys.clear();
151618
152191
  this.inFlight.clear();
151619
152192
  }
151620
152193
  // ── Standby ────────────────────────────────────────────────────────────────
@@ -151630,10 +152203,12 @@ var init_side_thread_manager = __esm({
151630
152203
  const existing = await this.findStandby(parentSid);
151631
152204
  if (existing) {
151632
152205
  if (await this.isStandbyUsable(parentSid, existing)) {
151633
- this.armStandbyTtl(parentSid, existing.claudeSessionId);
152206
+ const warmed = this.warmedStandbys.get(parentSid) === existing.claudeSessionId;
152207
+ this.armStandbyTtl(parentSid, existing.claudeSessionId, warmed ? WARMED_STANDBY_TTL_MS : STANDBY_TTL_MS);
151634
152208
  return existing.claudeSessionId;
151635
152209
  }
151636
152210
  void this.retireSession(existing.claudeSessionId, "side_thread_standby_stale");
152211
+ this.warmedStandbys.delete(parentSid);
151637
152212
  }
151638
152213
  const parentOffset = parent.consumedOffset;
151639
152214
  const { sessionId } = await forkSideThreadSession(parentSid, STANDBY_THREAD_ID);
@@ -151659,7 +152234,7 @@ var init_side_thread_manager = __esm({
151659
152234
  if (!parent) return false;
151660
152235
  return parent.consumedOffset === this.standbyParentOffsets.get(parentSid);
151661
152236
  }
151662
- armStandbyTtl(parentSid, sessionId) {
152237
+ armStandbyTtl(parentSid, sessionId, ttlMs = STANDBY_TTL_MS) {
151663
152238
  const prev = this.standbyTimers.get(parentSid);
151664
152239
  if (prev) clearTimeout(prev);
151665
152240
  const timer3 = setTimeout(() => {
@@ -151669,13 +152244,41 @@ var init_side_thread_manager = __esm({
151669
152244
  const record = await getSessionByClaudeId2(sessionId).catch(() => null);
151670
152245
  if (!record || parseSideLaneKey(record.lane)?.threadId !== STANDBY_THREAD_ID) return;
151671
152246
  this.standbyParentOffsets.delete(parentSid);
152247
+ this.warmedStandbys.delete(parentSid);
151672
152248
  await this.retireSession(sessionId, "side_thread_standby_ttl");
151673
152249
  }).catch(() => {
151674
152250
  });
151675
- }, STANDBY_TTL_MS);
152251
+ }, ttlMs);
151676
152252
  timer3.unref?.();
151677
152253
  this.standbyTimers.set(parentSid, timer3);
151678
152254
  }
152255
+ /**
152256
+ * Run the standby's cache warm-up turn (see side-thread-warmup.ts). Called
152257
+ * when the user starts TYPING a new question: the fork's first API call pays
152258
+ * the full prefix write no matter what, so paying it while they type turns
152259
+ * the actual question into a cached follow-up. Idempotent per standby; a
152260
+ * missing or stale standby is a no-op (the ask path will fork fresh anyway).
152261
+ */
152262
+ async warmStandby(parentSid) {
152263
+ return this.serialize(parentSid, async () => {
152264
+ const standby = await this.findStandby(parentSid);
152265
+ if (!standby) return { warmed: false, reason: "no_standby" };
152266
+ if (!await this.isStandbyUsable(parentSid, standby)) return { warmed: false, reason: "stale" };
152267
+ if (this.warmedStandbys.get(parentSid) === standby.claudeSessionId) {
152268
+ return { warmed: true, reason: "already_warm" };
152269
+ }
152270
+ const { sendMessageToSession: sendMessageToSession2 } = await Promise.resolve().then(() => (init_session_message_queue(), session_message_queue_exports));
152271
+ markWarmupTurnPending(standby.claudeSessionId);
152272
+ await sendMessageToSession2(standby.claudeSessionId, CACHE_WARMUP_MESSAGE, { source: "side-thread-warmup" });
152273
+ this.warmedStandbys.set(parentSid, standby.claudeSessionId);
152274
+ this.armStandbyTtl(parentSid, standby.claudeSessionId, WARMED_STANDBY_TTL_MS);
152275
+ log.session.info("side thread: standby cache warm-up sent", {
152276
+ parentSid,
152277
+ standbySid: standby.claudeSessionId
152278
+ });
152279
+ return { warmed: true };
152280
+ });
152281
+ }
151679
152282
  // ── Threads ────────────────────────────────────────────────────────────────
151680
152283
  /**
151681
152284
  * Open a side thread on `parentSid` and ask `question` in it.
@@ -151686,6 +152289,10 @@ var init_side_thread_manager = __esm({
151686
152289
  * question as its spawn's first turn instead — a send issued right after
151687
152290
  * SESSION_START can lose that race and cold-`--resume` an id the CLI has never
151688
152291
  * seen (see personal-ai-lane.ts LaneSession.created).
152292
+ *
152293
+ * `imageContext` (attachment paths for the CLI to Read) rides the MESSAGE only.
152294
+ * The stored entry keeps the plain question, so the chip label, the injected
152295
+ * transcript and a promoted task never carry the machine preamble.
151689
152296
  */
151690
152297
  async createThread(parentSid, input) {
151691
152298
  const question = input.question?.trim();
@@ -151693,8 +152300,11 @@ var init_side_thread_manager = __esm({
151693
152300
  return this.serialize(parentSid, async () => {
151694
152301
  const threadId = mintSideThreadId();
151695
152302
  const title = input.title?.trim() || void 0;
152303
+ const message = input.imageContext ? `${input.imageContext}
152304
+
152305
+ ${question}` : question;
151696
152306
  const consumed = await this.consumeStandby(parentSid, threadId, title);
151697
- const threadSessionId = consumed ?? (await forkSideThreadSession(parentSid, threadId, { message: question, ...title ? { title } : {} })).sessionId;
152307
+ const threadSessionId = consumed ?? (await forkSideThreadSession(parentSid, threadId, { message, ...title ? { title } : {} })).sessionId;
151698
152308
  let entry;
151699
152309
  try {
151700
152310
  const { addSideThread: addSideThread2 } = await Promise.resolve().then(() => (init_side_questions(), side_questions_exports));
@@ -151706,7 +152316,7 @@ var init_side_thread_manager = __esm({
151706
152316
  });
151707
152317
  if (consumed) {
151708
152318
  const { sendMessageToSession: sendMessageToSession2 } = await Promise.resolve().then(() => (init_session_message_queue(), session_message_queue_exports));
151709
- await sendMessageToSession2(threadSessionId, question, { source: "side-thread" });
152319
+ await sendMessageToSession2(threadSessionId, message, { source: "side-thread" });
151710
152320
  }
151711
152321
  } catch (err2) {
151712
152322
  void this.retireSession(threadSessionId, "side_thread_create_failed");
@@ -151741,6 +152351,7 @@ var init_side_thread_manager = __esm({
151741
152351
  if (!await this.isStandbyUsable(parentSid, standby)) {
151742
152352
  void this.retireSession(standby.claudeSessionId, "side_thread_standby_stale");
151743
152353
  this.standbyParentOffsets.delete(parentSid);
152354
+ this.warmedStandbys.delete(parentSid);
151744
152355
  return null;
151745
152356
  }
151746
152357
  const timer3 = this.standbyTimers.get(parentSid);
@@ -151749,6 +152360,7 @@ var init_side_thread_manager = __esm({
151749
152360
  this.standbyTimers.delete(parentSid);
151750
152361
  }
151751
152362
  this.standbyParentOffsets.delete(parentSid);
152363
+ this.warmedStandbys.delete(parentSid);
151752
152364
  const lane = sideThreadLaneKey(parentSid, threadId);
151753
152365
  const { updateSessionRecord: updateSessionRecord2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
151754
152366
  await updateSessionRecord2(standby.claudeSessionId, {
@@ -151935,16 +152547,30 @@ async function promoteSideThread(parentSid, threadId, opts) {
151935
152547
  const threadRecord = await getSessionByClaudeId2(sessionId);
151936
152548
  if (!threadRecord) throw new SessionControlError("Side thread session not found", 404);
151937
152549
  const parentRecord = await getSessionByClaudeId2(parentSid);
151938
- const parentTaskId = parentRecord?.taskId?.trim() || void 0;
151939
- const title = opts?.title?.trim() || entry.title?.trim() || entry.question;
152550
+ const siblingOfTaskId = parentRecord?.taskId?.trim() || void 0;
152551
+ const threadLabel = opts?.title?.trim() || entry.title?.trim() || void 0;
151940
152552
  const { addTask: addTask2, linkSession: linkSession2, linkSessionSlot: linkSessionSlot2 } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
151941
- const { task } = await addTask2({
151942
- title,
151943
- description: entry.question,
151944
- ...parentTaskId ? { parent_task_id: parentTaskId } : {},
151945
- // A person clicked "promote" — same board default as any hand-made task.
151946
- pinned: true
151947
- });
152553
+ let groupId;
152554
+ let task;
152555
+ if (siblingOfTaskId) {
152556
+ const created = await createForkSiblingTask(siblingOfTaskId, {
152557
+ // A known label short-circuits the background refine; without one the
152558
+ // thread's first question is what gets summarized (same as a fork's message).
152559
+ titlePrefix: threadLabel,
152560
+ prompt: entry.question,
152561
+ description: entry.question,
152562
+ source: "side-thread-promote"
152563
+ });
152564
+ task = created.task;
152565
+ groupId = created.groupId;
152566
+ } else {
152567
+ ({ task } = await addTask2({
152568
+ title: threadLabel || entry.question,
152569
+ description: entry.question,
152570
+ // A person clicked "promote" — same board default as any hand-made task.
152571
+ pinned: true
152572
+ }));
152573
+ }
151948
152574
  const stamped = await markThreadPromoted2(parentSid, threadId, task.id);
151949
152575
  if (!stamped) {
151950
152576
  log.session.warn("side thread promote: entry vanished before stamp \u2014 promote proceeds, 409 guard is unarmed", {
@@ -151958,7 +152584,7 @@ async function promoteSideThread(parentSid, threadId, opts) {
151958
152584
  project: task.project ?? "",
151959
152585
  // Clearing the lane un-hides the session (see the file header).
151960
152586
  lane: void 0,
151961
- title
152587
+ title: task.title
151962
152588
  });
151963
152589
  try {
151964
152590
  const { sessionRunner: sessionRunner2 } = await Promise.resolve().then(() => (init_claude_code_session(), claude_code_session_exports));
@@ -151977,10 +152603,18 @@ async function promoteSideThread(parentSid, threadId, opts) {
151977
152603
  error: err2 instanceof Error ? err2.message : String(err2)
151978
152604
  });
151979
152605
  }
151980
- log.session.info("side thread: promoted to task", { parentSid, threadId, sessionId, taskId: task.id });
152606
+ log.session.info("side thread: promoted to task", {
152607
+ parentSid,
152608
+ threadId,
152609
+ sessionId,
152610
+ taskId: task.id,
152611
+ siblingOfTaskId,
152612
+ groupId
152613
+ });
151981
152614
  return {
151982
152615
  taskId: task.id,
151983
- ...task.parent_task_id ? { parentTaskId: task.parent_task_id } : {},
152616
+ ...siblingOfTaskId ? { siblingOfTaskId } : {},
152617
+ ...groupId ? { groupId } : {},
151984
152618
  sessionId
151985
152619
  };
151986
152620
  }
@@ -151995,7 +152629,7 @@ var init_side_thread_promote = __esm({
151995
152629
 
151996
152630
  // src/web/routes/sessions.ts
151997
152631
  import { Router as Router16 } from "express";
151998
- import { randomUUID as randomUUID10 } from "crypto";
152632
+ import { randomUUID as randomUUID11 } from "crypto";
151999
152633
  import os27 from "os";
152000
152634
  import path142 from "path";
152001
152635
  function condenseStaleReason(msg) {
@@ -152341,7 +152975,7 @@ var init_sessions = __esm({
152341
152975
  }
152342
152976
  const isNativeEngine = !isAcpEngine(engine);
152343
152977
  const clientSessionId = typeof req.body.sessionId === "string" && UUID_RE3.test(req.body.sessionId) ? req.body.sessionId : void 0;
152344
- const preassignedSessionId = isNativeEngine ? clientSessionId ?? randomUUID10() : void 0;
152978
+ const preassignedSessionId = isNativeEngine ? clientSessionId ?? randomUUID11() : void 0;
152345
152979
  const updatedTask = await quickStartSession({
152346
152980
  message: sessionMessage,
152347
152981
  messagePrefix,
@@ -153316,18 +153950,38 @@ var init_sessions = __esm({
153316
153950
  });
153317
153951
  }
153318
153952
  });
153953
+ sessionsRouter.post("/:sessionId/side-threads/standby/warm", async (req, res) => {
153954
+ if (refuseSideThreadsOnReplica(res)) return;
153955
+ const sessionId = String(req.params.sessionId);
153956
+ try {
153957
+ const { sideThreadManager: sideThreadManager2 } = await Promise.resolve().then(() => (init_side_thread_manager(), side_thread_manager_exports));
153958
+ res.json(await sideThreadManager2.warmStandby(sessionId));
153959
+ } catch (err2) {
153960
+ log.web.warn("side thread standby warm-up failed", {
153961
+ sessionId,
153962
+ error: err2 instanceof Error ? err2.message : String(err2)
153963
+ });
153964
+ res.json({ warmed: false, reason: "error" });
153965
+ }
153966
+ });
153319
153967
  sessionsRouter.post("/:sessionId/side-threads", async (req, res, next) => {
153320
153968
  if (refuseSideThreadsOnReplica(res)) return;
153321
153969
  try {
153322
- const { question, title } = req.body ?? {};
153970
+ const { question, title, images } = req.body ?? {};
153323
153971
  if (!question || typeof question !== "string" || !question.trim()) {
153324
153972
  res.status(400).json({ error: "question (non-empty string) is required" });
153325
153973
  return;
153326
153974
  }
153975
+ let imageContext = "";
153976
+ if (images && images.length > 0) {
153977
+ const processed = await processAndSaveImages(images);
153978
+ if (processed) imageContext = buildSessionImageContext(processed.savedImages);
153979
+ }
153327
153980
  const { sideThreadManager: sideThreadManager2 } = await Promise.resolve().then(() => (init_side_thread_manager(), side_thread_manager_exports));
153328
153981
  const thread = await sideThreadManager2.createThread(String(req.params.sessionId), {
153329
153982
  question,
153330
- ...typeof title === "string" && title.trim() ? { title } : {}
153983
+ ...typeof title === "string" && title.trim() ? { title } : {},
153984
+ ...imageContext ? { imageContext } : {}
153331
153985
  });
153332
153986
  res.json({ thread });
153333
153987
  } catch (err2) {
@@ -153346,7 +154000,8 @@ var init_sessions = __esm({
153346
154000
  );
153347
154001
  res.json({
153348
154002
  taskId: result.taskId,
153349
- ...result.parentTaskId ? { parentTaskId: result.parentTaskId } : {},
154003
+ ...result.siblingOfTaskId ? { siblingOfTaskId: result.siblingOfTaskId } : {},
154004
+ ...result.groupId ? { groupId: result.groupId } : {},
153350
154005
  sessionId: result.sessionId
153351
154006
  });
153352
154007
  } catch (err2) {
@@ -153537,6 +154192,19 @@ var init_sessions = __esm({
153537
154192
  next(err2);
153538
154193
  }
153539
154194
  });
154195
+ sessionsRouter.post("/:sessionId/recheck", async (req, res, next) => {
154196
+ try {
154197
+ const sessionId = req.params.sessionId;
154198
+ const result = await recheckSession(sessionId);
154199
+ res.json(result);
154200
+ } catch (err2) {
154201
+ if (err2 instanceof SessionControlError) {
154202
+ res.status(err2.statusCode).json({ error: err2.message });
154203
+ return;
154204
+ }
154205
+ next(err2);
154206
+ }
154207
+ });
153540
154208
  sessionsRouter.post("/:sessionId/restart", async (req, res, next) => {
153541
154209
  const sessionId = req.params.sessionId;
153542
154210
  const startedAt = Date.now();
@@ -154028,7 +154696,7 @@ var micro_claude_exports = {};
154028
154696
  __export(micro_claude_exports, {
154029
154697
  runMicroClaude: () => runMicroClaude
154030
154698
  });
154031
- import { randomUUID as randomUUID11 } from "crypto";
154699
+ import { randomUUID as randomUUID12 } from "crypto";
154032
154700
  async function runMicroClaude(opts) {
154033
154701
  if (opts.warm) {
154034
154702
  const run10 = await runWarmMicroClaude({
@@ -154037,7 +154705,7 @@ async function runMicroClaude(opts) {
154037
154705
  tools: opts.tools ?? [],
154038
154706
  prompt: opts.prompt,
154039
154707
  timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2,
154040
- toolUseId: opts.toolUseId ?? `micro-claude-${randomUUID11()}`,
154708
+ toolUseId: opts.toolUseId ?? `micro-claude-${randomUUID12()}`,
154041
154709
  ...opts.onBlock ? { onBlock: opts.onBlock } : {}
154042
154710
  });
154043
154711
  return { response: run10.response, costUsd: run10.costUsd, durationMs: run10.durationMs };
@@ -154047,7 +154715,7 @@ async function runMicroClaude(opts) {
154047
154715
  systemPrompt: opts.system,
154048
154716
  model: opts.model ?? DEFAULT_MODEL2,
154049
154717
  timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2,
154050
- toolUseId: opts.toolUseId ?? `micro-claude-${randomUUID11()}`,
154718
+ toolUseId: opts.toolUseId ?? `micro-claude-${randomUUID12()}`,
154051
154719
  slim: true,
154052
154720
  thinking: opts.thinking ?? false,
154053
154721
  ...opts.tools && opts.tools.length > 0 ? { tools: opts.tools } : {},
@@ -154077,7 +154745,7 @@ __export(micro_agent_exports, {
154077
154745
  async function resolveTierModel(tier) {
154078
154746
  const { getConfig: getConfig2 } = await Promise.resolve().then(() => (init_config_manager(), config_manager_exports));
154079
154747
  const config = await getConfig2();
154080
- const provider = config.agent?.main_provider ?? "bedrock";
154748
+ const provider = resolveMainProviderName(config);
154081
154749
  const { MODEL_CATALOG: MODEL_CATALOG3 } = await Promise.resolve().then(() => (init_model_catalog(), model_catalog_exports));
154082
154750
  const entry = MODEL_CATALOG3[provider]?.find((m6) => {
154083
154751
  const id = m6.id.toLowerCase();
@@ -154146,6 +154814,7 @@ var DEFAULT_TIER, DEFAULT_MAX_TOKENS2, DEFAULT_MAX_ROUNDS, DEFAULT_TIMEOUT_MS3;
154146
154814
  var init_micro_agent = __esm({
154147
154815
  "src/agent/micro-agent.ts"() {
154148
154816
  "use strict";
154817
+ init_default_provider();
154149
154818
  DEFAULT_TIER = "sonnet";
154150
154819
  DEFAULT_MAX_TOKENS2 = 2e3;
154151
154820
  DEFAULT_MAX_ROUNDS = 3;
@@ -154161,7 +154830,7 @@ __export(task_search_agent_exports, {
154161
154830
  prewarmAgentSearchChild: () => prewarmAgentSearchChild,
154162
154831
  runTaskSearchAgent: () => runTaskSearchAgent
154163
154832
  });
154164
- import { randomUUID as randomUUID12 } from "crypto";
154833
+ import { randomUUID as randomUUID13 } from "crypto";
154165
154834
  function useCliEngine() {
154166
154835
  return process.env.WALNUT_AGENT_SEARCH_ENGINE !== "inprocess";
154167
154836
  }
@@ -154241,7 +154910,7 @@ async function claudeCliEngine(userPrompt, options) {
154241
154910
  model: options.model,
154242
154911
  timeoutMs: options.timeoutMs,
154243
154912
  tools: ["Bash"],
154244
- toolUseId: `task-search-${randomUUID12()}`,
154913
+ toolUseId: `task-search-${randomUUID13()}`,
154245
154914
  warm: true,
154246
154915
  onBlock
154247
154916
  });
@@ -155260,9 +155929,13 @@ var init_credential_verify = __esm({
155260
155929
  var config_exports = {};
155261
155930
  __export(config_exports, {
155262
155931
  buildProvidersPayload: () => buildProvidersPayload,
155263
- configRouter: () => configRouter
155932
+ configRouter: () => configRouter,
155933
+ setStaticRootReporter: () => setStaticRootReporter
155264
155934
  });
155265
155935
  import { Router as Router20 } from "express";
155936
+ function setStaticRootReporter(fn) {
155937
+ staticRootReporter = fn;
155938
+ }
155266
155939
  async function fetchOllamaModels(baseUrl) {
155267
155940
  if (Date.now() - _ollamaCache.ts < OLLAMA_CACHE_TTL) return _ollamaCache.models;
155268
155941
  const url = (baseUrl ?? DEFAULT_BASE_URLS["ollama"] ?? "http://localhost:11434") + "/api/tags";
@@ -155315,10 +155988,11 @@ async function buildProvidersPayload() {
155315
155988
  providers[name] = {
155316
155989
  api: prov.api,
155317
155990
  base_url: prov.base_url,
155318
- status: caps.subscriptionReady ? "ready" : "no_key",
155991
+ status: caps.installed ? "ready" : "no_key",
155319
155992
  auto_detected: !explicitNames.has(name),
155320
155993
  models: getModelsForProvider(name, prov.models),
155321
- credential_source: caps.subscriptionReady ? "subscription" : caps.installed ? "cli_no_subscription" : "cli_not_installed"
155994
+ credential_source: caps.installed ? `cli_${caps.auth.mode}` : "cli_not_installed",
155995
+ credential_detail: caps.installed ? caps.auth.label : void 0
155322
155996
  };
155323
155997
  continue;
155324
155998
  }
@@ -155377,10 +156051,11 @@ async function buildProvidersPayload() {
155377
156051
  const caps = detectClaudeCli2();
155378
156052
  providers[name] = {
155379
156053
  api: template.api,
155380
- status: caps.subscriptionReady ? "ready" : "no_key",
156054
+ status: caps.installed ? "ready" : "no_key",
155381
156055
  auto_detected: false,
155382
156056
  models: getModelsForProvider(name),
155383
- credential_source: caps.subscriptionReady ? "subscription" : caps.installed ? "cli_no_subscription" : "cli_not_installed"
156057
+ credential_source: caps.installed ? `cli_${caps.auth.mode}` : "cli_not_installed",
156058
+ credential_detail: caps.installed ? caps.auth.label : void 0
155384
156059
  };
155385
156060
  continue;
155386
156061
  }
@@ -155395,7 +156070,7 @@ async function buildProvidersPayload() {
155395
156070
  }
155396
156071
  return { providers };
155397
156072
  }
155398
- var configRouter, _ollamaCache, OLLAMA_CACHE_TTL;
156073
+ var configRouter, staticRootReporter, _ollamaCache, OLLAMA_CACHE_TTL;
155399
156074
  var init_config11 = __esm({
155400
156075
  "src/web/routes/config.ts"() {
155401
156076
  "use strict";
@@ -155410,6 +156085,7 @@ var init_config11 = __esm({
155410
156085
  init_defaults();
155411
156086
  init_config_redact();
155412
156087
  configRouter = Router20();
156088
+ staticRootReporter = null;
155413
156089
  configRouter.get("/", async (_req, res, next) => {
155414
156090
  try {
155415
156091
  const config = await getConfig();
@@ -155436,7 +156112,8 @@ var init_config11 = __esm({
155436
156112
  remoteIdUniquenessGaps = getExtIndexUniquenessGaps2();
155437
156113
  } catch {
155438
156114
  }
155439
- res.json({ config: CLOUD_MODE ? redactConfig(config) : config, envTokenHint, installDir: CLOUD_MODE ? null : WALNUT_INSTALL_DIR, notesDir: CLOUD_MODE ? null : NOTES_DIR, processNice, memory, canRevealLocalFiles, remoteIdUniquenessGaps, cloud: CLOUD_MODE });
156115
+ const webAssets = staticRootReporter?.() ?? null;
156116
+ res.json({ config: CLOUD_MODE ? redactConfig(config) : config, envTokenHint, installDir: CLOUD_MODE ? null : WALNUT_INSTALL_DIR, notesDir: CLOUD_MODE ? null : NOTES_DIR, processNice, memory, canRevealLocalFiles, remoteIdUniquenessGaps, cloud: CLOUD_MODE, webAssets });
155440
156117
  } catch (err2) {
155441
156118
  next(err2);
155442
156119
  }
@@ -155534,7 +156211,7 @@ var init_config11 = __esm({
155534
156211
  const {
155535
156212
  traceCredentialResolution: traceCredentialResolution2,
155536
156213
  resolveCredentials: resolveCredentials2,
155537
- readClaudeSettingsEnv: readClaudeSettingsEnv2,
156214
+ readClaudeSettingsEnv: readClaudeSettingsEnv3,
155538
156215
  readClaudeCredentialExport: readClaudeCredentialExport2
155539
156216
  } = await Promise.resolve().then(() => (init_credential_resolver(), credential_resolver_exports));
155540
156217
  const fs114 = await import("fs");
@@ -155550,7 +156227,7 @@ var init_config11 = __esm({
155550
156227
  };
155551
156228
  const trace = traceCredentialResolution2({
155552
156229
  config,
155553
- claudeEnv: readClaudeSettingsEnv2(),
156230
+ claudeEnv: readClaudeSettingsEnv3(),
155554
156231
  processEnv: process.env,
155555
156232
  awsFiles: {
155556
156233
  credentials: exists3(path185.join(home, ".aws", "credentials")),
@@ -159226,6 +159903,9 @@ var init_session_chat = __esm({
159226
159903
 
159227
159904
  // src/web/routes/browser-logs.ts
159228
159905
  import { Router as Router31 } from "express";
159906
+ function entrySubsystem(entry) {
159907
+ return entry.subsystem && ENTRY_SUBSYSTEMS.has(entry.subsystem) ? entry.subsystem : "browser";
159908
+ }
159229
159909
  function checkRate(client, entryCount) {
159230
159910
  const now = Date.now();
159231
159911
  let state9 = clientRates.get(client);
@@ -159267,10 +159947,11 @@ function writeEntries(entries) {
159267
159947
  if (entry.args) meta.args = truncate(entry.args, 1e3);
159268
159948
  if (entry.url) meta.url = entry.url;
159269
159949
  if (entry.count && entry.count > 1) meta.count = entry.count;
159950
+ if (entry.client === "mac-app") meta.client = entry.client;
159270
159951
  writeLogEntry({
159271
159952
  time: entry.time,
159272
159953
  level: mapLevel(entry.level),
159273
- subsystem: "browser",
159954
+ subsystem: entrySubsystem(entry),
159274
159955
  message: truncate(entry.message, 2e3),
159275
159956
  browserLevel: entry.level,
159276
159957
  // preserve original level (e.g. 'log' vs 'info')
@@ -159293,13 +159974,14 @@ function registerBrowserLogsRpc() {
159293
159974
  writeEntries(entries);
159294
159975
  });
159295
159976
  }
159296
- var RATE_WINDOW_MS, RATE_LIMIT, clientRates, REST_MAX_ENTRIES, browserLogsRouter;
159977
+ var ENTRY_SUBSYSTEMS, RATE_WINDOW_MS, RATE_LIMIT, clientRates, REST_MAX_ENTRIES, browserLogsRouter;
159297
159978
  var init_browser_logs = __esm({
159298
159979
  "src/web/routes/browser-logs.ts"() {
159299
159980
  "use strict";
159300
159981
  init_handler();
159301
159982
  init_logging();
159302
159983
  init_logger();
159984
+ ENTRY_SUBSYSTEMS = /* @__PURE__ */ new Set(["browser", "desktop"]);
159303
159985
  RATE_WINDOW_MS = 1e4;
159304
159986
  RATE_LIMIT = 500;
159305
159987
  clientRates = /* @__PURE__ */ new WeakMap();
@@ -163643,6 +164325,7 @@ var init_session_health_monitor = __esm({
163643
164325
  log.session.info("health monitor: drained orphan dead-pool", { orphanCount: orphanIds.length, written: written.length });
163644
164326
  }
163645
164327
  if (sessions3.length === 0) {
164328
+ if (!ctx.overBudget()) await this.checkSnapshotPull(sessions3, ctx, allSessions);
163646
164329
  const total = Date.now() - checkT0;
163647
164330
  if (total > 500) log.session.warn("health monitor: check() slow (no active sessions)", { totalMs: total, orphanMs: tOrphan - checkT0, recoverMs: tRecover - tOrphan });
163648
164331
  return;
@@ -163672,7 +164355,7 @@ var init_session_health_monitor = __esm({
163672
164355
  await this.reconcilePendingBackgroundTasks(sessions3);
163673
164356
  endPhase("pendingBgTasks");
163674
164357
  if (ctx.overBudget()) return;
163675
- await this.checkSnapshotPull(sessions3, ctx);
164358
+ await this.checkSnapshotPull(sessions3, ctx, allSessions);
163676
164359
  endPhase("snapshotPull");
163677
164360
  if (ctx.overBudget()) return;
163678
164361
  const reconciledIds = await this.reconcileStuckRunningSessions(sessions3, taskMap);
@@ -164166,15 +164849,39 @@ var init_session_health_monitor = __esm({
164166
164849
  bus.emit(EventNames.SESSION_WILL_REAP, payload, ["*"], { source: "health-monitor" });
164167
164850
  }
164168
164851
  /**
164169
- * C2 snapshot pull channel (contract §5): every 30s tick, for records in
164170
- * {running, idle} on a native (non-codex, non-embedded/sdk) engine whose
164171
- * host has a POOLED CONNECTED DaemonConnection advertising 'snapshot-v1',
164172
- * PULL getState and feed the snapshot to applySnapshot. NEVER dials a new
164173
- * connection. Sequential, capped at 10 sids/tick, per-sid 25s spacing.
164852
+ * C2 snapshot pull channel (contract §5): every 30s tick, PULL getState from
164853
+ * the host's POOLED CONNECTED DaemonConnection advertising 'snapshot-v1' and
164854
+ * feed the answer to applySnapshot. NEVER dials a new connection, always
164855
+ * sequential. TWO candidate classes with SEPARATE budgets and cadences:
164856
+ *
164857
+ * - LIVE — {running, idle}: 10 sids/tick, 25s per-sid spacing. The steady
164858
+ * state channel.
164859
+ * - RE-EXAMINATION — {error, stopped} whose cause is NOT positively terminal
164860
+ * (classifySessionError) and whose last_status_change is inside
164861
+ * RESCUABLE_STOPPED_WINDOW_MS: 5 sids/tick, 5 min per-sid spacing. Same
164862
+ * engine/provider/awaiting_spawn/archived exclusions as the live class.
164863
+ *
164864
+ * WHY RE-EXAMINATION EXISTS — do not "optimize" the error class back out. A
164865
+ * wrongly-'error' record was a two-sided deadlock: this lane is the ONE writer
164866
+ * the enforce-mode gate sanctions for a snapshot-covered session, and while it
164867
+ * looked at live records only it never re-derived status for an 'error' one.
164868
+ * recoverInfraFailedSessions DOES look, but the fix it writes
164869
+ * (('health-monitor','auto_recovered_dead')) is itself category-①, so the gate
164870
+ * drops the whole patch — the lane allowed to write refused to look, and the
164871
+ * lane that looked was not allowed to write. Records froze in 'error' with
164872
+ * "Connection lost — unable to reach remote host" for hours after their host
164873
+ * came back (2026-09-03, a remote dev host whose tunnel flapped: prose still on
164874
+ * the record 2h after the reconnect), burning a 30s log-churn loop the whole
164875
+ * time. Terminality is decided STRUCTURALLY, never by matching errorMessage
164876
+ * prose — see the header of session-error-kind.ts for why that ban exists.
164877
+ *
164878
+ * Re-examination candidates come from a WIDER pool than the live ones: the
164879
+ * tick's own working set drops every 'error' row (isTerminalSession), so the
164880
+ * caller passes the unfiltered health-scan set for this class only.
164174
164881
  *
164175
- * TWO fairness/budget properties this loop must keep (C8/C9/C12/C29):
164882
+ * THREE fairness/budget properties both classes must keep (C8/C9/C12/C29):
164176
164883
  *
164177
- * - It honors the TickContext budget INSIDE the loop (same discipline as
164884
+ * - The TickContext budget is honored INSIDE the loop (same discipline as
164178
164885
  * checkHungSessions). Each iteration is a real daemon RPC with a
164179
164886
  * probe-timeout ceiling, so 10 slow hosts could burn 10 × the probe
164180
164887
  * timeout sequentially BEFORE the authoritative reconcile phases that run
@@ -164183,70 +164890,101 @@ var init_session_health_monitor = __esm({
164183
164890
  * next tick (see the rotation below).
164184
164891
  *
164185
164892
  * - Candidates are ordered by lastPullAt ASCENDING (never-pulled first),
164186
- * which turns the 10/tick cap into a round-robin. In list order the SAME
164187
- * first ten sids were pulled every tick and — because the 25s spacing is
164188
- * shorter than the 30s cadence — sids 11+ were never pulled at all: the
164189
- * pull channel silently did not exist for them.
164893
+ * which turns each cap into a round-robin. In list order the SAME first ten
164894
+ * sids were pulled every tick and — because the 25s spacing is shorter than
164895
+ * the 30s cadence — sids 11+ were never pulled at all: the pull channel
164896
+ * silently did not exist for them. Each class rotates on its OWN timestamps.
164897
+ *
164898
+ * - The classes cannot spend each other's budget. Re-examination is
164899
+ * speculative ("is this record still true?") and a settled-death cohort
164900
+ * dwarfs the live set (the live DB measured 100 stopped candidates against
164901
+ * 4 error rows), so it must cost a handful of cheap RPCs per tick and
164902
+ * nothing more — the same reasoning as MAX_STOPPED_PROBES_PER_TICK below.
164190
164903
  */
164191
164904
  snapshotPullAt = /* @__PURE__ */ new Map();
164192
- async checkSnapshotPull(sessions3, ctx) {
164905
+ /** Separate from snapshotPullAt on purpose: the two classes rotate
164906
+ * independently, so a live pull can never reset the slow re-examination
164907
+ * cadence (or vice versa) when a record moves between them. */
164908
+ snapshotReexamPullAt = /* @__PURE__ */ new Map();
164909
+ async checkSnapshotPull(sessions3, ctx, reexamPool = sessions3) {
164193
164910
  const PULL_MIN_GAP_MS = 25e3;
164194
164911
  const MAX_PULLS_PER_TICK = 10;
164912
+ const REEXAM_MIN_GAP_MS = 5 * 6e4;
164913
+ const MAX_REEXAM_PULLS_PER_TICK = 5;
164195
164914
  try {
164196
164915
  const { getSnapshotStatusMode: getSnapshotStatusMode2 } = await Promise.resolve().then(() => (init_session_snapshot_gate(), session_snapshot_gate_exports));
164197
164916
  if (getSnapshotStatusMode2() === "off") return;
164198
164917
  const { getPooledSnapshotConnection: getPooledSnapshotConnection2 } = await Promise.resolve().then(() => (init_daemon_connection(), daemon_connection_exports));
164199
164918
  const { applySnapshot: applySnapshot2 } = await Promise.resolve().then(() => (init_session_snapshot_apply(), session_snapshot_apply_exports));
164200
164919
  const now = Date.now();
164201
- const candidates = sessions3.filter((s4) => {
164202
- if (s4.process_status !== "running" && s4.process_status !== "idle") return false;
164920
+ const pullable = (s4) => {
164921
+ if (s4.archived) return false;
164203
164922
  if (!engineCaps(s4.engine).snapshotPull) return false;
164204
164923
  if (s4.provider === "embedded" || s4.provider === "sdk") return false;
164205
- if (s4.status_reason === "awaiting_spawn") return false;
164206
- const lastPull = this.snapshotPullAt.get(s4.claudeSessionId) ?? 0;
164207
- return now - lastPull >= PULL_MIN_GAP_MS;
164208
- }).sort((a8, b8) => (this.snapshotPullAt.get(a8.claudeSessionId) ?? 0) - (this.snapshotPullAt.get(b8.claudeSessionId) ?? 0));
164209
- let pulled = 0;
164210
- let capped = false;
164211
- for (const s4 of candidates) {
164212
- if (ctx?.overBudget()) {
164213
- log.session.warn("health monitor: checkSnapshotPull abandoned mid-loop (over budget)", {
164214
- pulled,
164215
- candidateCount: candidates.length
164216
- });
164217
- break;
164218
- }
164219
- const conn = getPooledSnapshotConnection2(s4.host);
164220
- if (!conn) continue;
164221
- if (pulled >= MAX_PULLS_PER_TICK) {
164222
- capped = true;
164223
- break;
164224
- }
164225
- pulled++;
164226
- this.snapshotPullAt.set(s4.claudeSessionId, now);
164227
- try {
164228
- const resp = await probeWithTimeout(
164229
- conn.send("getState", { sid: s4.claudeSessionId }),
164230
- null,
164231
- "snapshot-pull-getState",
164232
- s4.claudeSessionId
164233
- );
164234
- const snapshot2 = resp?.ok ? resp.snapshot : void 0;
164235
- if (snapshot2) await applySnapshot2(s4.claudeSessionId, snapshot2, "pull-30s");
164236
- } catch (err2) {
164237
- log.session.debug("health monitor: snapshot pull failed", {
164238
- sessionId: s4.claudeSessionId,
164239
- host: s4.host ?? "__local__",
164240
- error: err2 instanceof Error ? err2.message : String(err2)
164241
- });
164924
+ return s4.status_reason !== "awaiting_spawn";
164925
+ };
164926
+ const byPullAge = (at) => (a8, b8) => (at.get(a8.claudeSessionId) ?? 0) - (at.get(b8.claudeSessionId) ?? 0);
164927
+ const live = sessions3.filter((s4) => {
164928
+ if (s4.process_status !== "running" && s4.process_status !== "idle") return false;
164929
+ if (!pullable(s4)) return false;
164930
+ return now - (this.snapshotPullAt.get(s4.claudeSessionId) ?? 0) >= PULL_MIN_GAP_MS;
164931
+ }).sort(byPullAge(this.snapshotPullAt));
164932
+ const reexam = reexamPool.filter((s4) => {
164933
+ if (s4.process_status !== "error" && s4.process_status !== "stopped") return false;
164934
+ if (!pullable(s4)) return false;
164935
+ if (classifySessionError(s4) === "terminal") return false;
164936
+ const changedAt = s4.last_status_change ? Date.parse(s4.last_status_change) : NaN;
164937
+ if (!Number.isFinite(changedAt)) return false;
164938
+ if (now - changedAt > RESCUABLE_STOPPED_WINDOW_MS) return false;
164939
+ return now - (this.snapshotReexamPullAt.get(s4.claudeSessionId) ?? 0) >= REEXAM_MIN_GAP_MS;
164940
+ }).sort(byPullAge(this.snapshotReexamPullAt));
164941
+ let abandoned = false;
164942
+ const runClass = async (candidates, cap2, at, candidateClass) => {
164943
+ let pulled = 0;
164944
+ for (const s4 of candidates) {
164945
+ if (ctx?.overBudget()) {
164946
+ abandoned = true;
164947
+ log.session.warn("health monitor: checkSnapshotPull abandoned mid-loop (over budget)", {
164948
+ pulled,
164949
+ candidateCount: candidates.length,
164950
+ candidateClass
164951
+ });
164952
+ break;
164953
+ }
164954
+ const conn = getPooledSnapshotConnection2(s4.host);
164955
+ if (!conn) continue;
164956
+ if (pulled >= cap2) {
164957
+ log.session.info("health monitor: snapshot pull capped this tick", {
164958
+ cap: cap2,
164959
+ sessionCount: sessions3.length,
164960
+ candidateCount: candidates.length,
164961
+ candidateClass
164962
+ });
164963
+ break;
164964
+ }
164965
+ pulled++;
164966
+ at.set(s4.claudeSessionId, now);
164967
+ try {
164968
+ const resp = await probeWithTimeout(
164969
+ conn.send("getState", { sid: s4.claudeSessionId }),
164970
+ null,
164971
+ "snapshot-pull-getState",
164972
+ s4.claudeSessionId
164973
+ );
164974
+ const snapshot2 = resp?.ok ? resp.snapshot : void 0;
164975
+ if (snapshot2) await applySnapshot2(s4.claudeSessionId, snapshot2, "pull-30s");
164976
+ } catch (err2) {
164977
+ log.session.debug("health monitor: snapshot pull failed", {
164978
+ sessionId: s4.claudeSessionId,
164979
+ host: s4.host ?? "__local__",
164980
+ error: err2 instanceof Error ? err2.message : String(err2)
164981
+ });
164982
+ }
164242
164983
  }
164243
- }
164244
- if (capped) {
164245
- log.session.info("health monitor: snapshot pull capped this tick", {
164246
- cap: MAX_PULLS_PER_TICK,
164247
- sessionCount: sessions3.length,
164248
- candidateCount: candidates.length
164249
- });
164984
+ };
164985
+ await runClass(live, MAX_PULLS_PER_TICK, this.snapshotPullAt, "live");
164986
+ if (!abandoned) {
164987
+ await runClass(reexam, MAX_REEXAM_PULLS_PER_TICK, this.snapshotReexamPullAt, "reexam");
164250
164988
  }
164251
164989
  if (this.snapshotPullAt.size > 200) {
164252
164990
  const liveIds = new Set(sessions3.map((s4) => s4.claudeSessionId));
@@ -164254,6 +164992,12 @@ var init_session_health_monitor = __esm({
164254
164992
  if (!liveIds.has(sid)) this.snapshotPullAt.delete(sid);
164255
164993
  }
164256
164994
  }
164995
+ if (this.snapshotReexamPullAt.size > 200) {
164996
+ const poolIds = new Set(reexamPool.map((s4) => s4.claudeSessionId));
164997
+ for (const sid of this.snapshotReexamPullAt.keys()) {
164998
+ if (!poolIds.has(sid)) this.snapshotReexamPullAt.delete(sid);
164999
+ }
165000
+ }
164257
165001
  } catch (err2) {
164258
165002
  log.session.warn("health monitor: checkSnapshotPull failed", {
164259
165003
  error: err2 instanceof Error ? err2.message : String(err2)
@@ -164416,6 +165160,31 @@ var init_session_health_monitor = __esm({
164416
165160
  });
164417
165161
  }
164418
165162
  }
165163
+ /**
165164
+ * Can a scheduled auto-recovery actually FIRE for this record?
165165
+ *
165166
+ * fire() (session-auto-recover) hard-requires the linked task to still be
165167
+ * IN_PROGRESS — AGENT_COMPLETE means the work was already handed back and
165168
+ * resuming would talk over the human. Arming a recovery that cannot pass that
165169
+ * check is not free: the 30s tick re-armed it forever and each arming logged
165170
+ * an abort 20s later (measured running for 2+ hours on one session), while
165171
+ * `if (armed) continue` also skipped the phase sync that IS appropriate there.
165172
+ *
165173
+ * A lookup that cannot answer (no task id, deleted task, DB error) returns
165174
+ * true: only a KNOWN non-IN_PROGRESS phase blocks arming, so a transient read
165175
+ * failure can never disable recovery. schedule()'s own guards still apply.
165176
+ */
165177
+ async autoRecoverCanFire(s4) {
165178
+ if (!s4.taskId) return true;
165179
+ try {
165180
+ const { getTask: getTask2 } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
165181
+ const task = await getTask2(s4.taskId);
165182
+ if (!task) return true;
165183
+ return task.phase === "IN_PROGRESS";
165184
+ } catch {
165185
+ return true;
165186
+ }
165187
+ }
164419
165188
  /**
164420
165189
  * Auto-recover remote sessions stuck in 'error' with "Connection lost" message.
164421
165190
  *
@@ -164502,28 +165271,31 @@ var init_session_health_monitor = __esm({
164502
165271
  } else if (wasStopped) {
164503
165272
  continue;
164504
165273
  } else {
164505
- const updated = await updateSessionRecord2(s4.claudeSessionId, {
164506
- process_status: "stopped",
164507
- errorMessage: void 0,
164508
- errorKind: "infra",
164509
- activity: void 0,
164510
- last_status_change: now,
164511
- status_reason: "auto_recovered_dead",
164512
- status_changed_by: "health-monitor"
164513
- });
164514
- emitSessionStatusChanged(
164515
- updated,
164516
- {},
164517
- ["*"],
164518
- { source: "health-monitor", urgency: "urgent" }
164519
- );
164520
- log.session.info("health monitor: auto-recovered connection-lost session (process dead)", {
164521
- sessionId: s4.claudeSessionId,
164522
- host: s4.host,
164523
- alive: false
164524
- });
165274
+ const { isSnapshotCovered: isSnapshotCovered2 } = await Promise.resolve().then(() => (init_session_snapshot_gate(), session_snapshot_gate_exports));
165275
+ if (!isSnapshotCovered2(s4.claudeSessionId)) {
165276
+ const updated = await updateSessionRecord2(s4.claudeSessionId, {
165277
+ process_status: "stopped",
165278
+ errorMessage: void 0,
165279
+ errorKind: "infra",
165280
+ activity: void 0,
165281
+ last_status_change: now,
165282
+ status_reason: "auto_recovered_dead",
165283
+ status_changed_by: "health-monitor"
165284
+ });
165285
+ emitSessionStatusChanged(
165286
+ updated,
165287
+ {},
165288
+ ["*"],
165289
+ { source: "health-monitor", urgency: "urgent" }
165290
+ );
165291
+ log.session.info("health monitor: auto-recovered connection-lost session (process dead)", {
165292
+ sessionId: s4.claudeSessionId,
165293
+ host: s4.host,
165294
+ alive: false
165295
+ });
165296
+ }
164525
165297
  const { scheduleSessionAutoRecover: scheduleSessionAutoRecover2 } = await Promise.resolve().then(() => (init_session_auto_recover(), session_auto_recover_exports));
164526
- const armed = scheduleSessionAutoRecover2(s4, s4.status_reason ?? "daemon_reported_exit");
165298
+ const armed = await this.autoRecoverCanFire(s4) && scheduleSessionAutoRecover2(s4, s4.status_reason ?? "daemon_reported_exit");
164527
165299
  if (armed) continue;
164528
165300
  if (s4.taskId) {
164529
165301
  try {
@@ -172673,6 +173445,20 @@ var init_session_lifecycle_v1 = __esm({
172673
173445
  next(err2);
172674
173446
  }
172675
173447
  });
173448
+ sessionLifecycleV1Router.post("/sessions/:id/recheck", async (req, res, next) => {
173449
+ try {
173450
+ const sessionId = validSid2(req, res);
173451
+ if (!sessionId) return;
173452
+ if (CLOUD_MODE) {
173453
+ await relayControlAction(res, "recheck", sessionId, void 0, 200);
173454
+ return;
173455
+ }
173456
+ const { recheckSession: recheckSession2 } = await Promise.resolve().then(() => (init_session_lifecycle(), session_lifecycle_exports));
173457
+ await runLocal(res, next, 200, () => recheckSession2(sessionId));
173458
+ } catch (err2) {
173459
+ next(err2);
173460
+ }
173461
+ });
172676
173462
  sessionLifecycleV1Router.post("/sessions/:id/permission", async (req, res, next) => {
172677
173463
  try {
172678
173464
  const sessionId = validSid2(req, res);
@@ -172771,7 +173557,7 @@ __export(task_start_exports, {
172771
173557
  SessionExistsError: () => SessionExistsError,
172772
173558
  startSessionForTask: () => startSessionForTask
172773
173559
  });
172774
- import { randomUUID as randomUUID13 } from "crypto";
173560
+ import { randomUUID as randomUUID14 } from "crypto";
172775
173561
  async function startSessionForTask(params) {
172776
173562
  const { taskIdPrefix, source } = params;
172777
173563
  let task;
@@ -172822,7 +173608,7 @@ async function startSessionForTask(params) {
172822
173608
  }
172823
173609
  let message = params.message?.trim() || `Working on task: ${task.title}`;
172824
173610
  const engine = normalizeEngine(params.engine);
172825
- const preassignedSessionId = engineCaps(engine).idProvisioning === "provider-issued" ? void 0 : randomUUID13();
173611
+ const preassignedSessionId = engineCaps(engine).idProvisioning === "provider-issued" ? void 0 : randomUUID14();
172826
173612
  let requestId;
172827
173613
  if (params.expectReply) {
172828
173614
  const { resolveCaller: resolveCaller2 } = await Promise.resolve().then(() => (init_session_send_core(), session_send_core_exports));
@@ -184150,7 +184936,7 @@ async function generateLedgerDesc(task, timeoutMs = 15e3) {
184150
184936
  try {
184151
184937
  const { getConfig: getConfig2 } = await Promise.resolve().then(() => (init_config_manager(), config_manager_exports));
184152
184938
  const config = await getConfig2();
184153
- const providerName = config.agent?.main_provider ?? "bedrock";
184939
+ const providerName = resolveMainProviderName(config);
184154
184940
  const model = cheapModelFor(providerName);
184155
184941
  const detail = (task.description ?? "").trim().slice(0, 800);
184156
184942
  const project = (task.project ?? "").trim();
@@ -184209,6 +184995,7 @@ var init_task_ledger_desc = __esm({
184209
184995
  "use strict";
184210
184996
  init_model();
184211
184997
  init_model_catalog();
184998
+ init_default_provider();
184212
184999
  init_logging();
184213
185000
  init_task_manager();
184214
185001
  MAX_DESC_LEN = 120;
@@ -184232,6 +185019,10 @@ function diffStreamedVsPersisted(streamedTextMsgIds, persistedMsgIds) {
184232
185019
  }
184233
185020
  function armStreamConvergenceCheck(sessionId, streamedTextMsgIds) {
184234
185021
  if (streamedTextMsgIds.length === 0) return;
185022
+ if (consumeWarmupTurn(sessionId)) {
185023
+ log.obs.debug("stream-convergence: skipped hidden warm-up turn", { sessionId });
185024
+ return;
185025
+ }
184235
185026
  const timer3 = setTimeout(() => {
184236
185027
  void runCheck(sessionId, streamedTextMsgIds).catch((err2) => {
184237
185028
  log.obs.warn("stream-convergence check failed", {
@@ -184297,6 +185088,7 @@ var init_stream_convergence = __esm({
184297
185088
  "src/core/observability/stream-convergence.ts"() {
184298
185089
  "use strict";
184299
185090
  init_logging();
185091
+ init_side_thread_warmup();
184300
185092
  CHECK_DELAY_MS = 15e3;
184301
185093
  INCIDENT_DEDUPE_MS = 30 * 6e4;
184302
185094
  lastIncidentAt = /* @__PURE__ */ new Map();
@@ -184547,12 +185339,17 @@ function getSystemHealth() {
184547
185339
  }
184548
185340
  async function refreshSystemHealth() {
184549
185341
  systemHealth.claudeCliAvailable = checkClaudeCliAvailable();
184550
- const cred = await resolveCredentialHealth();
185342
+ applyCredentialHealth(await resolveCredentialHealth());
185343
+ broadcastEvent("system:health", systemHealth);
185344
+ return systemHealth;
185345
+ }
185346
+ function applyCredentialHealth(cred) {
184551
185347
  systemHealth.hasReadyProvider = cred.hasReadyProvider;
184552
185348
  systemHealth.credentialSource = cred.source;
184553
185349
  systemHealth.credentialDetail = cred.detail;
184554
- broadcastEvent("system:health", systemHealth);
184555
- return systemHealth;
185350
+ systemHealth.mainProvider = cred.mainProvider;
185351
+ systemHealth.mainProviderImplicit = cred.mainProviderImplicit;
185352
+ systemHealth.claudeCliAuth = cred.claudeCliAuth;
184556
185353
  }
184557
185354
  function checkClaudeCliAvailable() {
184558
185355
  return isClaudeCliInstalled();
@@ -184562,31 +185359,50 @@ async function resolveCredentialHealth() {
184562
185359
  const { getConfig: getConfig2 } = await Promise.resolve().then(() => (init_config_manager(), config_manager_exports));
184563
185360
  const { resolveCredentials: resolveCredentials2 } = await Promise.resolve().then(() => (init_credential_resolver(), credential_resolver_exports));
184564
185361
  const { buildProviderMap: buildProviderMap2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
185362
+ const { resolveMainProviderName: resolveMainProviderName2, mainProviderIsImplicit: mainProviderIsImplicit2 } = await Promise.resolve().then(() => (init_default_provider(), default_provider_exports));
185363
+ const { detectClaudeCli: detectClaudeCli2 } = await Promise.resolve().then(() => (init_claude_cli_detect(), claude_cli_detect_exports));
184565
185364
  const config = await getConfig2();
185365
+ const cli = detectClaudeCli2();
185366
+ const mainProvider = resolveMainProviderName2(config, cli.installed);
185367
+ const base = {
185368
+ mainProvider,
185369
+ mainProviderImplicit: mainProviderIsImplicit2(config),
185370
+ claudeCliAuth: cli.installed ? cli.auth.label : void 0
185371
+ };
185372
+ if (mainProvider === "claude_cli") {
185373
+ return cli.installed ? { ...base, hasReadyProvider: true, source: "config", detail: `claude-cli (${cli.auth.label})` } : { ...base, hasReadyProvider: false, source: "none" };
185374
+ }
184566
185375
  const cred = resolveCredentials2(config);
184567
185376
  if (cred.source !== "none") {
184568
- return { hasReadyProvider: true, source: cred.source, detail: cred.detail };
185377
+ return { ...base, hasReadyProvider: true, source: cred.source, detail: cred.detail };
184569
185378
  }
184570
185379
  const providers = buildProviderMap2(config.providers, config);
184571
185380
  for (const [, prov] of Object.entries(providers)) {
184572
185381
  if (prov.api === "bedrock" || prov.api === "ollama") continue;
184573
185382
  if (prov.api === "claude-cli") {
184574
- const { detectClaudeCli: detectClaudeCli2 } = await Promise.resolve().then(() => (init_claude_cli_detect(), claude_cli_detect_exports));
184575
- if (detectClaudeCli2().subscriptionReady) {
184576
- return { hasReadyProvider: true, source: "config", detail: "claude-cli (subscription)" };
184577
- }
185383
+ if (cli.installed) return { ...base, hasReadyProvider: true, source: "config", detail: `claude-cli (${cli.auth.label})` };
184578
185384
  continue;
184579
185385
  }
184580
185386
  const implemented = prov.api === "anthropic-messages" || prov.api === "openai-chat" || prov.api === "google-generative-ai";
184581
185387
  if (implemented && (prov.api_key || prov.bearer_token)) {
184582
- return { hasReadyProvider: true, source: "config", detail: prov.api };
185388
+ return { ...base, hasReadyProvider: true, source: "config", detail: prov.api };
184583
185389
  }
184584
185390
  }
184585
- return { hasReadyProvider: false, source: "none" };
185391
+ return { ...base, hasReadyProvider: false, source: "none" };
184586
185392
  } catch {
184587
185393
  return { hasReadyProvider: false, source: "none" };
184588
185394
  }
184589
185395
  }
185396
+ async function isWalnutAnsweringOn(port) {
185397
+ try {
185398
+ const res = await fetch(`http://127.0.0.1:${port}/api/system/health`, { signal: AbortSignal.timeout(1500) });
185399
+ if (!res.ok) return false;
185400
+ const body3 = await res.json();
185401
+ return !!body3 && typeof body3 === "object" && "hasReadyProvider" in body3;
185402
+ } catch {
185403
+ return false;
185404
+ }
185405
+ }
184590
185406
  async function startServer(options = {}) {
184591
185407
  if (httpServer) throw new Error("Server already running. Call stopServer() first.");
184592
185408
  bootCompleted = false;
@@ -184611,11 +185427,11 @@ async function startServer(options = {}) {
184611
185427
  }
184612
185428
  {
184613
185429
  const cred = await resolveCredentialHealth();
184614
- systemHealth.hasReadyProvider = cred.hasReadyProvider;
184615
- systemHealth.credentialSource = cred.source;
184616
- systemHealth.credentialDetail = cred.detail;
185430
+ applyCredentialHealth(cred);
184617
185431
  if (!systemHealth.hasReadyProvider) {
184618
185432
  log.web.warn("No AI provider configured \u2014 configure one in Settings");
185433
+ } else if (cred.mainProvider === "claude_cli") {
185434
+ log.web.info("AI provider: Claude Code", { auth: cred.claudeCliAuth, implicit: cred.mainProviderImplicit });
184619
185435
  } else if (cred.source && cred.source !== "config") {
184620
185436
  log.web.info("Auto-detected Bedrock credential", { source: cred.source, detail: cred.detail });
184621
185437
  }
@@ -185113,6 +185929,7 @@ async function startServer(options = {}) {
185113
185929
  });
185114
185930
  if (!dev) {
185115
185931
  const staticDir = (() => {
185932
+ if (process.env.WALNUT_WEB_STATIC_DIR) return process.env.WALNUT_WEB_STATIC_DIR;
185116
185933
  let dir2 = path184.dirname(fileURLToPath13(import.meta.url));
185117
185934
  for (let i8 = 0; i8 < 5; i8++) {
185118
185935
  const candidate = path184.join(dir2, "web", "static", "index.html");
@@ -185129,11 +185946,65 @@ async function startServer(options = {}) {
185129
185946
  }
185130
185947
  return path184.join(process.cwd(), "dist", "web", "static");
185131
185948
  })();
185949
+ const mirrorDir = process.env.WALNUT_WEB_STATIC_MIRROR || path184.join(WALNUT_HOME, "cache", "web-static");
185950
+ let mirrorReady = false;
185951
+ try {
185952
+ const want = fs113.readFileSync(path184.join(staticDir, "index.html"), "utf-8");
185953
+ let have = null;
185954
+ try {
185955
+ have = fs113.readFileSync(path184.join(mirrorDir, "index.html"), "utf-8");
185956
+ } catch {
185957
+ }
185958
+ if (have !== want) {
185959
+ fs113.rmSync(mirrorDir, { recursive: true, force: true });
185960
+ fs113.mkdirSync(path184.dirname(mirrorDir), { recursive: true });
185961
+ fs113.cpSync(staticDir, mirrorDir, { recursive: true });
185962
+ }
185963
+ } catch (err2) {
185964
+ log.web.warn("could not refresh the web-asset mirror", {
185965
+ mirrorDir,
185966
+ error: err2 instanceof Error ? err2.message : String(err2)
185967
+ });
185968
+ }
185969
+ try {
185970
+ mirrorReady = fs113.statSync(path184.join(mirrorDir, "index.html")).isFile();
185971
+ } catch {
185972
+ mirrorReady = false;
185973
+ }
185132
185974
  app.use(express3.static(staticDir));
185975
+ if (mirrorReady) app.use(express3.static(mirrorDir));
185976
+ const checkStaticRoot = () => {
185977
+ try {
185978
+ return fs113.statSync(path184.join(staticDir, "index.html")).isFile();
185979
+ } catch {
185980
+ return false;
185981
+ }
185982
+ };
185983
+ let staticRootOk = checkStaticRoot();
185984
+ if (!staticRootOk) {
185985
+ log.web.error("web assets are NOT servable at startup", { staticDir, mirrorReady });
185986
+ }
185987
+ const staticRootTimer = setInterval(() => {
185988
+ const ok = checkStaticRoot();
185989
+ if (ok === staticRootOk) return;
185990
+ staticRootOk = ok;
185991
+ if (ok) log.web.info("web assets are servable again", { staticDir });
185992
+ else log.web.error("web assets VANISHED from under the running server", { staticDir, mirrorReady });
185993
+ }, 6e4);
185994
+ staticRootTimer.unref();
185995
+ setStaticRootReporter(() => ({ staticDir, ok: staticRootOk, mirrorDir, mirrorReady }));
185133
185996
  app.use((req, res, next) => {
185134
185997
  if (req.method !== "GET" || req.path.startsWith("/api/")) return next();
185135
185998
  if (isStaticAssetPath(req.path)) return next();
185136
- res.sendFile("index.html", { root: staticDir });
185999
+ res.sendFile("index.html", { root: staticDir }, (err2) => {
186000
+ if (!err2) return;
186001
+ if (mirrorReady) {
186002
+ res.sendFile("index.html", { root: mirrorDir }, () => {
186003
+ });
186004
+ return;
186005
+ }
186006
+ next(err2);
186007
+ });
185137
186008
  });
185138
186009
  }
185139
186010
  app.use("/api", notFoundHandler);
@@ -185153,9 +186024,21 @@ async function startServer(options = {}) {
185153
186024
  }
185154
186025
  httpServer = createServer(app);
185155
186026
  attachWss(httpServer);
185156
- await new Promise((resolve, reject) => {
185157
- httpServer.listen(port, () => resolve());
185158
- httpServer.once("error", reject);
186027
+ try {
186028
+ await new Promise((resolve, reject) => {
186029
+ httpServer.once("error", reject);
186030
+ httpServer.listen(port, () => resolve());
186031
+ });
186032
+ } catch (err2) {
186033
+ if (err2?.code !== "EADDRINUSE") throw err2;
186034
+ const already = await isWalnutAnsweringOn(port);
186035
+ throw new Error(
186036
+ already ? `Walnut is already running at http://localhost:${port} (EADDRINUSE). Open that address; there is nothing to start.` : `Port ${port} is in use by another program (EADDRINUSE). Start Walnut on a free one instead, for example:
186037
+ open-walnut web --port ${port + 1}`
186038
+ );
186039
+ }
186040
+ httpServer.on("error", (err2) => {
186041
+ log.web.error("http server error after listen", { error: err2 instanceof Error ? err2.message : String(err2) });
185159
186042
  });
185160
186043
  const label = dev ? "dev" : "production";
185161
186044
  log.web.info(`server listening on http://localhost:${port}`, { mode: label, port });
@@ -185165,6 +186048,7 @@ async function startServer(options = {}) {
185165
186048
  const { updateInstanceLockPort: updateInstanceLockPort2 } = await Promise.resolve().then(() => (init_instance_lock(), instance_lock_exports));
185166
186049
  updateInstanceLockPort2(bound.port);
185167
186050
  setPluginApiBase(`http://127.0.0.1:${bound.port}`);
186051
+ process.env.WALNUT_SERVER_URL = `http://localhost:${bound.port}`;
185168
186052
  }
185169
186053
  }
185170
186054
  if (CLOUD_MODE) {
@@ -185827,10 +186711,9 @@ async function startServer(options = {}) {
185827
186711
  const prev = systemHealth.hasReadyProvider;
185828
186712
  const prevSource = systemHealth.credentialSource;
185829
186713
  const cred = await resolveCredentialHealth();
185830
- systemHealth.hasReadyProvider = cred.hasReadyProvider;
185831
- systemHealth.credentialSource = cred.source;
185832
- systemHealth.credentialDetail = cred.detail;
185833
- if (cred.hasReadyProvider !== prev || cred.source !== prevSource) {
186714
+ const prevProvider = systemHealth.mainProvider;
186715
+ applyCredentialHealth(cred);
186716
+ if (cred.hasReadyProvider !== prev || cred.source !== prevSource || cred.mainProvider !== prevProvider) {
185834
186717
  broadcastEvent("system:health", systemHealth);
185835
186718
  }
185836
186719
  } catch {