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
package/dist/cli.js CHANGED
@@ -7366,6 +7366,23 @@ function findRepoRoot() {
7366
7366
  }
7367
7367
  return null;
7368
7368
  }
7369
+ function bunAvailable() {
7370
+ const home = process.env.HOME ?? "";
7371
+ const candidates = [
7372
+ ...(process.env.PATH ?? "").split(path14.delimiter).filter(Boolean).map((d8) => path14.join(d8, "bun")),
7373
+ path14.join(home, ".bun", "bin", "bun"),
7374
+ "/opt/homebrew/bin/bun",
7375
+ "/usr/local/bin/bun"
7376
+ ];
7377
+ return candidates.some((p6) => {
7378
+ try {
7379
+ fs12.accessSync(p6, fs12.constants.X_OK);
7380
+ return true;
7381
+ } catch {
7382
+ return false;
7383
+ }
7384
+ });
7385
+ }
7369
7386
  function readBuiltVersions() {
7370
7387
  const out = /* @__PURE__ */ new Map();
7371
7388
  try {
@@ -7389,6 +7406,10 @@ function verifyDaemonBinaryVersion() {
7389
7406
  return true;
7390
7407
  }
7391
7408
  const built = readBuiltVersions();
7409
+ if (built.size === 0 && !bunAvailable()) {
7410
+ log.session.info("no daemon binaries and no Bun on this machine; sessions use the source daemon", { expected });
7411
+ return true;
7412
+ }
7392
7413
  if (built.size === 0) {
7393
7414
  return handleMismatch({
7394
7415
  reason: "no daemon binaries built",
@@ -7447,6 +7468,12 @@ function handleMismatch(ctx) {
7447
7468
  return false;
7448
7469
  }
7449
7470
  const builtAfter = readBuiltVersions();
7471
+ if (builtAfter.size === 0) {
7472
+ log.session.warn("daemon rebuild produced no binaries (no Bun?); continuing with the source daemon", { expected: ctx.expected });
7473
+ console.error(` Daemon binaries were not built (Bun is not installed). Sessions use the source daemon. Continuing startup.
7474
+ `);
7475
+ return true;
7476
+ }
7450
7477
  const stillOff = [...builtAfter].filter(([, v3]) => v3 !== ctx.expected);
7451
7478
  if (stillOff.length > 0) {
7452
7479
  log.session.error(
@@ -8788,7 +8815,26 @@ function reapSession(sid, code, reason) {
8788
8815
  let jsonlAgeMs = null;
8789
8816
  try { jsonlAgeMs = Date.now() - fs.statSync(session.jsonlPath).mtimeMs; } catch {}
8790
8817
  const cleanExit = isTurnCompleteExit(session.jsonlPath);
8791
- if (code !== 0 && cleanExit) {
8818
+ // A DELIBERATE stop is not a failure, and the JSONL tail cannot tell us it
8819
+ // happened. Both writers (our idle scanner, and any stop RPC via cmdStop)
8820
+ // stamp intentionalStopAt before signalling; the death then lands here through
8821
+ // whichever path notices it (usually orphan-poll ESRCH \u2192 code -1). Verified
8822
+ // production case: the last line was a control_response (the reply to a
8823
+ // Walnut-issued control request \u2014 a routine shape), so isTurnCompleteExit said
8824
+ // false, exitCode stayed -1, and the projection painted a red "Error" over a
8825
+ // healthy, fully resumable session that was stopped on purpose. Checked FIRST
8826
+ // and unconditionally: the flag carries information the tail does not contain,
8827
+ // which is why isTurnCompleteExit is left alone.
8828
+ // Keep in sync with daemon-core.ts reapSession.
8829
+ const intentionalStop = session.intentionalStopAt != null;
8830
+ if (intentionalStop && code !== 0) {
8831
+ logMsg('info', 'reapSession: intentional stop, normalizing exit code', {
8832
+ sid, pid: session.pid, originalCode: code, originalReason: reason,
8833
+ jsonlAgeMs, cleanExit, intentionalStopAt: session.intentionalStopAt,
8834
+ });
8835
+ code = 0;
8836
+ reason = reason + '+intentional-stop';
8837
+ } else if (code !== 0 && cleanExit) {
8792
8838
  logMsg('info', 'reapSession: turn-complete detected, normalizing exit code', {
8793
8839
  sid, pid: session.pid, originalCode: code, originalReason: reason, jsonlAgeMs,
8794
8840
  });
@@ -12441,6 +12487,15 @@ function cmdStop(ws, id, cmd) {
12441
12487
  const pid = session.pid;
12442
12488
  logMsg('info', 'cmdStop: stopping session (process group kill)', { sid, pid });
12443
12489
 
12490
+ // Nobody sends stop by accident, so record the intent BEFORE the first signal:
12491
+ // the death lands asynchronously (orphan poll \u2192 ESRCH \u2192 code -1) and by then
12492
+ // nothing remembers this was asked for. This path is how the SERVER's idle
12493
+ // reaper kills (mgr.kill() \u2192 stop RPC), not the daemon's own scan, so it needs
12494
+ // its own stamp: a LOCAL session auto-stopped after 2h idle surfaced as
12495
+ // "Session ended unexpectedly and no cause was recorded" (2026-09-03).
12496
+ // Keep in sync with daemon-standalone.ts cmdStop.
12497
+ session.intentionalStopAt = Date.now();
12498
+
12444
12499
  // An explicit stop is a human decision \u2014 never undone by a pending auto-retry
12445
12500
  // respawning the CLI minutes later. Cancel the timer AND clear the streak so
12446
12501
  // an in-flight result line can't re-arm one either.
@@ -12477,7 +12532,7 @@ function cmdStop(ws, id, cmd) {
12477
12532
 
12478
12533
  // \u2500\u2500 Status \u2500\u2500
12479
12534
  function cmdStatus(ws, id, cmd) {
12480
- const { sid } = cmd;
12535
+ const { sid, includeArgs } = cmd;
12481
12536
  if (!sid) return sendError(ws, id, 'status: missing sid');
12482
12537
 
12483
12538
  const session = sessions.get(sid);
@@ -12508,6 +12563,11 @@ function cmdStatus(ws, id, cmd) {
12508
12563
  exitCode: session.exitCode,
12509
12564
  exitReason: session.exitReason,
12510
12565
  pendingCtrl: session.pendingCtrl,
12566
+ // Opt-in: the argv the live process was launched with (a fork copies it to
12567
+ // reproduce the parent's prompt-cache prefix). Off by default because the
12568
+ // health monitor polls status for every session and the argv carries the
12569
+ // multi-KB --append-system-prompt.
12570
+ ...(includeArgs && session.args && session.args.length > 0 ? { args: session.args } : {}),
12511
12571
  });
12512
12572
  }
12513
12573
 
@@ -14553,6 +14613,14 @@ function scanIdleSessions() {
14553
14613
  }
14554
14614
  const idleMinutes = Math.round(idleMs / 60000);
14555
14615
  logMsg('warn', 'idle scan: killing idle session (no subscribers, no output)', { sid, pid, idleMinutes, protectedBy: prot.source, thresholdMs: prot.killMs, detail: prot.detail });
14616
+ // Record the intent BEFORE signalling: the reap arrives asynchronously
14617
+ // (the orphan poll sees ESRCH and reports 'orphan-poll-dead', code -1), so
14618
+ // by then nothing remembers that WE asked for this. Without the stamp
14619
+ // reapSession can only consult the JSONL tail, which says "not a clean turn
14620
+ // end" for any session whose last line isn't a type:result \u2014 and the
14621
+ // projection then shows a red Error for our own routine reclamation.
14622
+ // Keep in sync with daemon-standalone.ts scanIdleSessions.
14623
+ session.intentionalStopAt = now;
14556
14624
  killSessionProcessGroup(pid, sid);
14557
14625
  }
14558
14626
  }
@@ -15236,7 +15304,20 @@ function createDaemonCore(deps) {
15236
15304
  jsonlAgeMs = clock() - fs121.statSync(session2.jsonlPath).mtimeMs;
15237
15305
  } catch {
15238
15306
  }
15239
- if (cleanExit && code !== 0) {
15307
+ const intentionalStop = session2.intentionalStopAt != null;
15308
+ if (intentionalStop && code !== 0) {
15309
+ logger2("info", "reapSession: intentional stop, normalizing exit code", {
15310
+ sid,
15311
+ pid: session2.pid,
15312
+ originalCode: code,
15313
+ originalReason: reason,
15314
+ jsonlAgeMs,
15315
+ cleanExit,
15316
+ intentionalStopAt: session2.intentionalStopAt
15317
+ });
15318
+ code = 0;
15319
+ reason = reason + "+intentional-stop";
15320
+ } else if (cleanExit && code !== 0) {
15240
15321
  logger2("info", "reapSession: turn-complete detected, normalizing exit code", {
15241
15322
  sid,
15242
15323
  pid: session2.pid,
@@ -16026,6 +16107,7 @@ var init_daemon_core = __esm({
16026
16107
  var claude_cli_detect_exports = {};
16027
16108
  __export(claude_cli_detect_exports, {
16028
16109
  _CLAUDE_CREDENTIALS_HOME: () => _CLAUDE_CREDENTIALS_HOME,
16110
+ describeClaudeCliAuth: () => describeClaudeCliAuth,
16029
16111
  detectClaudeCli: () => detectClaudeCli,
16030
16112
  hasClaudeSubscriptionAuth: () => hasClaudeSubscriptionAuth,
16031
16113
  isClaudeCliInstalled: () => isClaudeCliInstalled,
@@ -16086,9 +16168,32 @@ function detectClaudeCli() {
16086
16168
  return {
16087
16169
  installed: installed2,
16088
16170
  subscriptionAuth,
16089
- subscriptionReady: installed2 && subscriptionAuth
16171
+ subscriptionReady: installed2 && subscriptionAuth,
16172
+ ready: installed2,
16173
+ auth: installed2 ? describeClaudeCliAuth() : { mode: "unknown", label: "not installed" }
16090
16174
  };
16091
16175
  }
16176
+ function describeClaudeCliAuth(env3 = process.env, settingsEnv = readClaudeSettingsEnv()) {
16177
+ const merged = { ...env3, ...settingsEnv };
16178
+ const truthy = (v3) => !!v3 && v3 !== "0" && v3.toLowerCase() !== "false";
16179
+ if (truthy(merged.CLAUDE_CODE_USE_BEDROCK)) {
16180
+ const region = merged.AWS_REGION || merged.AWS_DEFAULT_REGION;
16181
+ return { mode: "bedrock", label: region ? `Bedrock (${region})` : "Bedrock" };
16182
+ }
16183
+ if (truthy(merged.CLAUDE_CODE_USE_VERTEX)) return { mode: "vertex", label: "Vertex AI" };
16184
+ if (merged.ANTHROPIC_API_KEY || merged.ANTHROPIC_AUTH_TOKEN) return { mode: "api-key", label: "an Anthropic API key" };
16185
+ if (hasClaudeSubscriptionAuth()) return { mode: "subscription", label: "your Claude subscription" };
16186
+ return { mode: "unknown", label: "its own login" };
16187
+ }
16188
+ function readClaudeSettingsEnv() {
16189
+ try {
16190
+ const raw = fs16.readFileSync(path17.join(os4.homedir(), ".claude", "settings.json"), "utf8");
16191
+ const env3 = JSON.parse(raw).env ?? {};
16192
+ return Object.fromEntries(Object.entries(env3).filter(([, v3]) => typeof v3 === "string"));
16193
+ } catch {
16194
+ return {};
16195
+ }
16196
+ }
16092
16197
  var KEYCHAIN_OAUTH_SERVICE, _CLAUDE_CREDENTIALS_HOME;
16093
16198
  var init_claude_cli_detect = __esm({
16094
16199
  "src/core/claude-cli-detect.ts"() {
@@ -16707,6 +16812,22 @@ var init_local_daemon = __esm({
16707
16812
  });
16708
16813
 
16709
16814
  // src/core/session-error-kind.ts
16815
+ var session_error_kind_exports = {};
16816
+ __export(session_error_kind_exports, {
16817
+ INFRA_STATUS_REASONS: () => INFRA_STATUS_REASONS,
16818
+ RESCUABLE_STOPPED_WINDOW_MS: () => RESCUABLE_STOPPED_WINDOW_MS,
16819
+ TERMINAL_STATUS_REASONS: () => TERMINAL_STATUS_REASONS,
16820
+ assertsHostUnreachable: () => assertsHostUnreachable,
16821
+ classifySessionError: () => classifySessionError,
16822
+ classifyStatusReasonKind: () => classifyStatusReasonKind,
16823
+ isInfraSessionError: () => isInfraSessionError,
16824
+ isRecoverableSessionError: () => isRecoverableSessionError,
16825
+ isRescuableStoppedRecord: () => isRescuableStoppedRecord
16826
+ });
16827
+ function assertsHostUnreachable(record) {
16828
+ if (!record.errorMessage) return false;
16829
+ return typeof record.status_reason === "string" && REACHABILITY_CLAIM_REASONS.has(record.status_reason);
16830
+ }
16710
16831
  function classifyStatusReasonKind(reason) {
16711
16832
  if (typeof reason !== "string") return void 0;
16712
16833
  if (INFRA_STATUS_REASONS.has(reason)) return "infra";
@@ -16735,7 +16856,7 @@ function isRescuableStoppedRecord(record, nowMs = Date.now()) {
16735
16856
  if (!Number.isFinite(changedAt)) return false;
16736
16857
  return nowMs - changedAt <= RESCUABLE_STOPPED_WINDOW_MS;
16737
16858
  }
16738
- var INFRA_STATUS_REASONS, TERMINAL_STATUS_REASONS, INFRA_TEXT_SIGNATURES, RESCUABLE_STOPPED_WINDOW_MS;
16859
+ var INFRA_STATUS_REASONS, TERMINAL_STATUS_REASONS, INFRA_TEXT_SIGNATURES, REACHABILITY_CLAIM_REASONS, RESCUABLE_STOPPED_WINDOW_MS;
16739
16860
  var init_session_error_kind = __esm({
16740
16861
  "src/core/session-error-kind.ts"() {
16741
16862
  "use strict";
@@ -16779,6 +16900,9 @@ var init_session_error_kind = __esm({
16779
16900
  "ssh tunnel",
16780
16901
  "broken pipe"
16781
16902
  ];
16903
+ REACHABILITY_CLAIM_REASONS = /* @__PURE__ */ new Set([
16904
+ "remote_unreachable"
16905
+ ]);
16782
16906
  RESCUABLE_STOPPED_WINDOW_MS = 24 * 60 * 60 * 1e3;
16783
16907
  }
16784
16908
  });
@@ -23439,7 +23563,7 @@ __export(credential_resolver_exports, {
23439
23563
  DEFAULT_REGION: () => DEFAULT_REGION,
23440
23564
  hasResolvableCredential: () => hasResolvableCredential,
23441
23565
  readClaudeCredentialExport: () => readClaudeCredentialExport,
23442
- readClaudeSettingsEnv: () => readClaudeSettingsEnv,
23566
+ readClaudeSettingsEnv: () => readClaudeSettingsEnv2,
23443
23567
  resolveCredentialFrom: () => resolveCredentialFrom,
23444
23568
  resolveCredentials: () => resolveCredentials,
23445
23569
  traceCredentialResolution: () => traceCredentialResolution
@@ -23594,7 +23718,7 @@ function traceCredentialResolution(inputs) {
23594
23718
  region: { value: winner.region ?? DEFAULT_REGION, source: regionSource }
23595
23719
  };
23596
23720
  }
23597
- function readClaudeSettingsEnv(settingsFile = CLAUDE_SETTINGS_FILE) {
23721
+ function readClaudeSettingsEnv2(settingsFile = CLAUDE_SETTINGS_FILE) {
23598
23722
  try {
23599
23723
  const raw = fs30.readFileSync(settingsFile, "utf-8");
23600
23724
  const parsed = JSON.parse(raw);
@@ -23634,7 +23758,7 @@ function probeAwsFiles() {
23634
23758
  function resolveCredentials(config) {
23635
23759
  const result = resolveCredentialFrom({
23636
23760
  config,
23637
- claudeEnv: readClaudeSettingsEnv(),
23761
+ claudeEnv: readClaudeSettingsEnv2(),
23638
23762
  processEnv: process.env,
23639
23763
  awsFiles: probeAwsFiles(),
23640
23764
  claudeCredentialExport: readClaudeCredentialExport()
@@ -66416,10 +66540,9 @@ function buildArgs(opts) {
66416
66540
  // Input stays default (text): we write the whole prompt to stdin and close.
66417
66541
  // (stream-json INPUT would force us into a JSON envelope for no gain here.)
66418
66542
  "--tools",
66419
- "",
66543
+ ""
66420
66544
  // disable ALL built-in tools (deny-all)
66421
- "--settings",
66422
- FORCE_SUBSCRIPTION_SETTINGS
66545
+ // No --settings override: the user's settings.json (and so their auth) applies as-is.
66423
66546
  ];
66424
66547
  let systemText = systemToText(opts.system).trim();
66425
66548
  if (opts.tools && opts.tools.length > 0) {
@@ -66448,7 +66571,7 @@ function systemToText(system) {
66448
66571
  }
66449
66572
  function buildSpawnEnv(base = process.env) {
66450
66573
  const env3 = { ...base };
66451
- for (const key2 of AUTH_ENV_TO_STRIP) {
66574
+ for (const key2 of ENV_TO_STRIP) {
66452
66575
  delete env3[key2];
66453
66576
  }
66454
66577
  env3.CLAUDE_CODE_ENTRYPOINT = WALNUT_UTILITY_ENTRYPOINT;
@@ -66510,7 +66633,7 @@ function normalizeCliModel(model) {
66510
66633
  }
66511
66634
  return void 0;
66512
66635
  }
66513
- var AUTH_ENV_TO_STRIP, FORCE_SUBSCRIPTION_SETTINGS, CLI_TURN_TIMEOUT_MS, SESSION_REUSE_TTL_MS, MAX_TRACKED_SESSIONS, ClaudeCliAdapter;
66636
+ var ENV_TO_STRIP, MAX_CONCURRENT_CLI, CLI_TURN_TIMEOUT_MS, SESSION_REUSE_TTL_MS, MAX_TRACKED_SESSIONS, ClaudeCliAdapter;
66514
66637
  var init_adapter_claude_cli = __esm({
66515
66638
  "src/agent/providers/adapter-claude-cli.ts"() {
66516
66639
  "use strict";
@@ -66520,24 +66643,11 @@ var init_adapter_claude_cli = __esm({
66520
66643
  init_logging();
66521
66644
  init_claude_cli_detect();
66522
66645
  init_claude_cli_protocol();
66523
- AUTH_ENV_TO_STRIP = [
66524
- "ANTHROPIC_API_KEY",
66525
- "ANTHROPIC_AUTH_TOKEN",
66526
- "ANTHROPIC_BASE_URL",
66527
- "AWS_BEARER_TOKEN_BEDROCK",
66528
- "AWS_ACCESS_KEY_ID",
66529
- "AWS_SECRET_ACCESS_KEY",
66530
- "AWS_SESSION_TOKEN",
66531
- "AWS_PROFILE",
66532
- "CLAUDE_CODE_USE_BEDROCK",
66533
- "CLAUDE_CODE_USE_VERTEX",
66534
- // Nested-session marker — inline-subagent strips this too so the child doesn't
66535
- // think it's running inside another Claude session.
66536
- "CLAUDECODE"
66537
- ];
66538
- FORCE_SUBSCRIPTION_SETTINGS = JSON.stringify({
66539
- env: { CLAUDE_CODE_USE_BEDROCK: "", CLAUDE_CODE_USE_VERTEX: "" }
66540
- });
66646
+ ENV_TO_STRIP = ["CLAUDECODE"];
66647
+ MAX_CONCURRENT_CLI = (() => {
66648
+ const n6 = Number(process.env.WALNUT_CLAUDE_CLI_CONCURRENCY);
66649
+ return Number.isFinite(n6) && n6 > 0 ? Math.floor(n6) : 3;
66650
+ })();
66541
66651
  CLI_TURN_TIMEOUT_MS = 12e4;
66542
66652
  SESSION_REUSE_TTL_MS = 30 * 60 * 1e3;
66543
66653
  MAX_TRACKED_SESSIONS = 50;
@@ -66545,6 +66655,25 @@ var init_adapter_claude_cli = __esm({
66545
66655
  protocol = "claude-cli";
66546
66656
  /** conversationKey → CLI session for --resume chaining. In-process only. */
66547
66657
  sessions = /* @__PURE__ */ new Map();
66658
+ /** Process gate: MAX_CONCURRENT_CLI turns at once, the rest queue in order. */
66659
+ inFlight = 0;
66660
+ waiters = [];
66661
+ async withSlot(fn) {
66662
+ if (this.inFlight >= MAX_CONCURRENT_CLI) {
66663
+ await new Promise((resolve) => this.waiters.push(resolve));
66664
+ }
66665
+ this.inFlight++;
66666
+ try {
66667
+ return await fn();
66668
+ } finally {
66669
+ this.inFlight--;
66670
+ this.waiters.shift()?.();
66671
+ }
66672
+ }
66673
+ /** Test hook: how many turns are running / waiting right now. */
66674
+ _gateStateForTesting() {
66675
+ return { inFlight: this.inFlight, waiting: this.waiters.length };
66676
+ }
66548
66677
  resetClient() {
66549
66678
  this.sessions.clear();
66550
66679
  }
@@ -66622,7 +66751,7 @@ User: ${overridePrompt}` : "");
66622
66751
  }
66623
66752
  if (!prompt.trim()) prompt = "(continue)";
66624
66753
  const args = [...buildArgs(opts), session2.flag, session2.id];
66625
- return this.execCli(command6, args, env3, prompt, opts.signal, onTextDelta).then((result) => {
66754
+ return this.withSlot(() => this.execCli(command6, args, env3, prompt, opts.signal, onTextDelta)).then((result) => {
66626
66755
  this.sessions.set(key2, {
66627
66756
  sessionId: session2.id,
66628
66757
  seenMessages: opts.messages.length,
@@ -66641,7 +66770,7 @@ User: ${overridePrompt}` : "");
66641
66770
  User: ${overridePrompt}` : "");
66642
66771
  const freshId = randomUUID3();
66643
66772
  const freshArgs = [...buildArgs(opts), "--session-id", freshId];
66644
- return this.execCli(command6, freshArgs, env3, freshPrompt || "(continue)", opts.signal, onTextDelta).then((result) => {
66773
+ return this.withSlot(() => this.execCli(command6, freshArgs, env3, freshPrompt || "(continue)", opts.signal, onTextDelta)).then((result) => {
66645
66774
  this.sessions.set(key2, {
66646
66775
  sessionId: freshId,
66647
66776
  seenMessages: opts.messages.length,
@@ -66725,7 +66854,7 @@ User: ${overridePrompt}` : "");
66725
66854
  }
66726
66855
  const resultText = finalResult?.result || accumulatedText;
66727
66856
  if (resultText && resultText.length < 300 && /^\s*failed to authenticate|^\s*api error: 4\d\d/i.test(resultText)) {
66728
- reject(new Error(`claude-cli subscription auth failed \u2014 run \`claude login\` to re-authenticate. CLI said: ${resultText.slice(0, 200)}`));
66857
+ 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)}`));
66729
66858
  return;
66730
66859
  }
66731
66860
  if (code !== 0 && !accumulatedText && !finalResult) {
@@ -66925,6 +67054,50 @@ var init_registry2 = __esm({
66925
67054
  }
66926
67055
  });
66927
67056
 
67057
+ // src/agent/providers/default-provider.ts
67058
+ var default_provider_exports = {};
67059
+ __export(default_provider_exports, {
67060
+ CLAUDE_CLI_PROVIDER: () => CLAUDE_CLI_PROVIDER,
67061
+ LEGACY_DEFAULT_PROVIDER: () => LEGACY_DEFAULT_PROVIDER,
67062
+ _resetDefaultProviderCacheForTesting: () => _resetDefaultProviderCacheForTesting,
67063
+ hasExplicitBedrockConfig: () => hasExplicitBedrockConfig,
67064
+ mainProviderIsImplicit: () => mainProviderIsImplicit,
67065
+ resolveMainProviderName: () => resolveMainProviderName
67066
+ });
67067
+ function claudeCliInstalledCached() {
67068
+ const now = Date.now();
67069
+ if (!cliProbe || now - cliProbe.at > CLI_PROBE_TTL_MS) {
67070
+ cliProbe = { at: now, installed: isClaudeCliInstalled() };
67071
+ }
67072
+ return cliProbe.installed;
67073
+ }
67074
+ function _resetDefaultProviderCacheForTesting() {
67075
+ cliProbe = void 0;
67076
+ }
67077
+ function hasExplicitBedrockConfig(config) {
67078
+ const b8 = config.providers?.bedrock;
67079
+ if (b8 && (b8.bearer_token || b8.aws_access_key_id || b8.aws_profile || b8.aws_credential_export)) return true;
67080
+ return !!config.provider?.bedrock_bearer_token;
67081
+ }
67082
+ function resolveMainProviderName(config, claudeInstalled = claudeCliInstalledCached()) {
67083
+ if (config.agent?.main_provider) return config.agent.main_provider;
67084
+ if (claudeInstalled) return CLAUDE_CLI_PROVIDER;
67085
+ return LEGACY_DEFAULT_PROVIDER;
67086
+ }
67087
+ function mainProviderIsImplicit(config) {
67088
+ return !config.agent?.main_provider;
67089
+ }
67090
+ var LEGACY_DEFAULT_PROVIDER, CLAUDE_CLI_PROVIDER, CLI_PROBE_TTL_MS, cliProbe;
67091
+ var init_default_provider = __esm({
67092
+ "src/agent/providers/default-provider.ts"() {
67093
+ "use strict";
67094
+ init_claude_cli_detect();
67095
+ LEGACY_DEFAULT_PROVIDER = "bedrock";
67096
+ CLAUDE_CLI_PROVIDER = "claude_cli";
67097
+ CLI_PROBE_TTL_MS = 6e4;
67098
+ }
67099
+ });
67100
+
66928
67101
  // src/agent/model.ts
66929
67102
  var model_exports = {};
66930
67103
  __export(model_exports, {
@@ -66959,7 +67132,7 @@ function getContextThreshold(model, percent) {
66959
67132
  }
66960
67133
  async function resolveForCall(config) {
66961
67134
  const fullConfig = await getConfig();
66962
- const providerName = config?.provider ?? fullConfig.agent?.main_provider ?? "bedrock";
67135
+ const providerName = config?.provider ?? resolveMainProviderName(fullConfig);
66963
67136
  const hasExplicitProviders = fullConfig.providers && Object.keys(fullConfig.providers).length > 0;
66964
67137
  let providers = hasExplicitProviders ? buildProviderMap(fullConfig.providers, fullConfig) : synthesizeFromLegacy(fullConfig);
66965
67138
  const resolved = resolveProvider(providerName, providers);
@@ -67029,6 +67202,7 @@ var init_model = __esm({
67029
67202
  "use strict";
67030
67203
  init_config_manager();
67031
67204
  init_registry2();
67205
+ init_default_provider();
67032
67206
  init_defaults();
67033
67207
  init_model_catalog();
67034
67208
  }
@@ -67231,8 +67405,22 @@ var init_token_truth = __esm({
67231
67405
  });
67232
67406
 
67233
67407
  // src/utils/image-compress.ts
67234
- import sharp from "sharp";
67408
+ async function loadSharp() {
67409
+ sharpLoad ??= import("sharp").then(
67410
+ (m6) => m6.default ?? m6,
67411
+ (err2) => {
67412
+ log.agent.warn("sharp is not installed; images are sent uncompressed", {
67413
+ error: err2 instanceof Error ? err2.message : String(err2),
67414
+ fix: "a machine with glibc 2.28+ installs sharp prebuilt; otherwise install libvips and re-run npm install"
67415
+ });
67416
+ return null;
67417
+ }
67418
+ );
67419
+ return sharpLoad;
67420
+ }
67235
67421
  async function clampImageDimensions(buffer2, mimeType) {
67422
+ const sharp = await loadSharp();
67423
+ if (!sharp) return { buffer: buffer2, mimeType };
67236
67424
  try {
67237
67425
  const meta = await sharp(buffer2).metadata();
67238
67426
  const longest = Math.max(meta.width ?? 0, meta.height ?? 0);
@@ -67254,6 +67442,8 @@ async function compressForApi(inputBuffer, inputMimeType) {
67254
67442
  if (buffer2.toString("base64").length <= MAX_BASE64_BYTES) {
67255
67443
  return { buffer: buffer2, mimeType };
67256
67444
  }
67445
+ const sharp = await loadSharp();
67446
+ if (!sharp) return { buffer: buffer2, mimeType };
67257
67447
  try {
67258
67448
  if (mimeType === "image/gif") {
67259
67449
  const webp = await sharp(buffer2, { animated: true }).webp({ quality: 80 }).toBuffer();
@@ -67291,10 +67481,11 @@ async function compressForApi(inputBuffer, inputMimeType) {
67291
67481
  return { buffer: buffer2, mimeType };
67292
67482
  }
67293
67483
  }
67294
- var MAX_BASE64_BYTES, MAX_IMAGE_DIMENSION;
67484
+ var sharpLoad, MAX_BASE64_BYTES, MAX_IMAGE_DIMENSION;
67295
67485
  var init_image_compress = __esm({
67296
67486
  "src/utils/image-compress.ts"() {
67297
67487
  "use strict";
67488
+ init_logging();
67298
67489
  MAX_BASE64_BYTES = 5e6;
67299
67490
  MAX_IMAGE_DIMENSION = 1568;
67300
67491
  }
@@ -68452,7 +68643,7 @@ __export(cheap_model_exports, {
68452
68643
  });
68453
68644
  function fastModelFor(config) {
68454
68645
  if (config.agent?.fast_model) return config.agent.fast_model;
68455
- const providerName = config.agent?.main_provider ?? "bedrock";
68646
+ const providerName = resolveMainProviderName(config);
68456
68647
  return MODEL_CATALOG[providerName]?.find(
68457
68648
  (model) => model.id.toLowerCase().includes("haiku")
68458
68649
  )?.id;
@@ -68464,6 +68655,7 @@ var init_cheap_model = __esm({
68464
68655
  "src/core/cheap-model.ts"() {
68465
68656
  "use strict";
68466
68657
  init_model_catalog();
68658
+ init_default_provider();
68467
68659
  }
68468
68660
  });
68469
68661
 
@@ -70283,11 +70475,35 @@ async function applySnapshot(sessionId, snapshot2, source) {
70283
70475
  if (projected !== "error") {
70284
70476
  updates.errorMessage = void 0;
70285
70477
  updates.errorKind = void 0;
70286
- clearSuppressedErrorReason(sessionId);
70478
+ const stashedStop = projected === "stopped" ? takeSuppressedErrorReason(sessionId) : null;
70479
+ if (stashedStop?.message) {
70480
+ updates.errorMessage = stashedStop.message;
70481
+ if (stashedStop.kind) updates.errorKind = stashedStop.kind;
70482
+ log.session.info("snapshot projection adopted suppressed stop reason", {
70483
+ sessionId,
70484
+ suppressedReason: stashedStop.reason,
70485
+ kind: stashedStop.kind ?? null,
70486
+ projected,
70487
+ source
70488
+ });
70489
+ } else {
70490
+ clearSuppressedErrorReason(sessionId);
70491
+ }
70287
70492
  } else {
70493
+ let droppedDisproven = false;
70494
+ if (assertsHostUnreachable(record)) {
70495
+ updates.errorMessage = void 0;
70496
+ droppedDisproven = true;
70497
+ log.session.info("snapshot projection dropped disproven unreachability diagnosis", {
70498
+ sessionId,
70499
+ priorReason: record.status_reason ?? null,
70500
+ projected,
70501
+ source
70502
+ });
70503
+ }
70288
70504
  const stashed = takeSuppressedErrorReason(sessionId);
70289
70505
  if (stashed) {
70290
- if (!record.errorMessage && stashed.message) updates.errorMessage = stashed.message;
70506
+ if ((!record.errorMessage || droppedDisproven) && stashed.message) updates.errorMessage = stashed.message;
70291
70507
  if (!record.errorKind && stashed.kind) updates.errorKind = stashed.kind;
70292
70508
  log.session.info("snapshot projection adopted suppressed error reason", {
70293
70509
  sessionId,
@@ -70340,6 +70556,7 @@ var init_session_snapshot_apply = __esm({
70340
70556
  "use strict";
70341
70557
  init_logging();
70342
70558
  init_session_snapshot_gate();
70559
+ init_session_error_kind();
70343
70560
  init_engine_registry();
70344
70561
  init_session_snapshot_gate();
70345
70562
  TERMINAL = /* @__PURE__ */ new Set(["stopped", "error"]);
@@ -72954,9 +73171,15 @@ var init_acp_session = __esm({
72954
73171
  }
72955
73172
  async resolveArtifacts() {
72956
73173
  if (this.cfg.artifacts) return this.cfg.artifacts;
72957
- return resolveAcpArtifacts(this.engineId, {
73174
+ const artifacts = resolveAcpArtifacts(this.engineId, {
72958
73175
  configuredAdapterCmd: await this.resolveConfiguredAdapterCmd()
72959
73176
  });
73177
+ if (!fsModule.existsSync(artifacts.workerCmd[1])) {
73178
+ throw new Error(
73179
+ `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.`
73180
+ );
73181
+ }
73182
+ return artifacts;
72960
73183
  }
72961
73184
  async establishProviderSession() {
72962
73185
  await this.seedReplayCursor();
@@ -74339,6 +74562,26 @@ var init_transcript_chain = __esm({
74339
74562
  }
74340
74563
  });
74341
74564
 
74565
+ // src/core/sessions/side-thread-warmup.ts
74566
+ function isCacheWarmupText(text) {
74567
+ return text.startsWith(CACHE_WARMUP_TAG);
74568
+ }
74569
+ function markWarmupTurnPending(sessionId) {
74570
+ pendingWarmupTurns.add(sessionId);
74571
+ }
74572
+ function consumeWarmupTurn(sessionId) {
74573
+ return pendingWarmupTurns.delete(sessionId);
74574
+ }
74575
+ var CACHE_WARMUP_TAG, CACHE_WARMUP_MESSAGE, pendingWarmupTurns;
74576
+ var init_side_thread_warmup = __esm({
74577
+ "src/core/sessions/side-thread-warmup.ts"() {
74578
+ "use strict";
74579
+ CACHE_WARMUP_TAG = "<walnut-cache-warmup>";
74580
+ 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>`;
74581
+ pendingWarmupTurns = /* @__PURE__ */ new Set();
74582
+ }
74583
+ });
74584
+
74342
74585
  // src/core/history-disk-cache.ts
74343
74586
  var history_disk_cache_exports = {};
74344
74587
  __export(history_disk_cache_exports, {
@@ -76493,11 +76736,13 @@ __export(session_lifecycle_exports, {
76493
76736
  persistSessionModeChange: () => persistSessionModeChange,
76494
76737
  readProviderSessionHistory: () => readProviderSessionHistory,
76495
76738
  readSessionRichHistory: () => readSessionRichHistory,
76739
+ recheckSession: () => recheckSession,
76496
76740
  respondSessionPermission: () => respondSessionPermission,
76497
76741
  restartSession: () => restartSession,
76498
76742
  retrySession: () => retrySession,
76499
76743
  terminateSession: () => terminateSession
76500
76744
  });
76745
+ import { randomUUID as randomUUID4 } from "crypto";
76501
76746
  async function readProviderSessionHistory(sessionId, record, nativeHost, skipSubagents = true, opts) {
76502
76747
  if (record && engineCaps(record.engine).historySource === "acp-journal") {
76503
76748
  const { readAcpSessionHistoryState: readAcpSessionHistoryState2 } = await Promise.resolve().then(() => (init_acp_session_history(), acp_session_history_exports));
@@ -76608,6 +76853,33 @@ async function changeSessionMode(existingRecord, sessionId, mode, updates = {})
76608
76853
  });
76609
76854
  return updated;
76610
76855
  }
76856
+ function normalizePinQuote(value) {
76857
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
76858
+ throw new SessionControlError("pinned_messages[].quote must be an object", 400);
76859
+ }
76860
+ const quote = value;
76861
+ const exact = quote.exact;
76862
+ if (typeof exact !== "string" || !exact.trim() || exact.length > MAX_PIN_QUOTE_CHARS) {
76863
+ throw new SessionControlError(
76864
+ `pinned_messages[].quote.exact must be a non-empty string (max ${MAX_PIN_QUOTE_CHARS} chars)`,
76865
+ 400
76866
+ );
76867
+ }
76868
+ for (const side of ["prefix", "suffix"]) {
76869
+ const context = quote[side];
76870
+ if (context !== void 0 && context !== null && (typeof context !== "string" || context.length > MAX_PIN_QUOTE_CONTEXT_CHARS)) {
76871
+ throw new SessionControlError(
76872
+ `pinned_messages[].quote.${side} must be a string (max ${MAX_PIN_QUOTE_CONTEXT_CHARS} chars)`,
76873
+ 400
76874
+ );
76875
+ }
76876
+ }
76877
+ return {
76878
+ exact,
76879
+ ...typeof quote.prefix === "string" && quote.prefix ? { prefix: quote.prefix } : {},
76880
+ ...typeof quote.suffix === "string" && quote.suffix ? { suffix: quote.suffix } : {}
76881
+ };
76882
+ }
76611
76883
  function normalizePinnedMessages(value) {
76612
76884
  if (!Array.isArray(value)) {
76613
76885
  throw new SessionControlError("pinned_messages must be an array", 400);
@@ -76616,7 +76888,9 @@ function normalizePinnedMessages(value) {
76616
76888
  throw new SessionControlError(`pinned_messages holds at most ${MAX_PINNED_MESSAGES} entries`, 400);
76617
76889
  }
76618
76890
  const out = [];
76619
- const seen = /* @__PURE__ */ new Set();
76891
+ const seenWholeMessage = /* @__PURE__ */ new Set();
76892
+ const seenQuoteId = /* @__PURE__ */ new Set();
76893
+ const seenQuoteShape = /* @__PURE__ */ new Set();
76620
76894
  for (const raw of value) {
76621
76895
  if (!raw || typeof raw !== "object") {
76622
76896
  throw new SessionControlError("each pinned_messages entry must be an object", 400);
@@ -76626,13 +76900,40 @@ function normalizePinnedMessages(value) {
76626
76900
  if (typeof msgId !== "string" || !msgId.trim() || msgId.length > MAX_PIN_ID_CHARS) {
76627
76901
  throw new SessionControlError("pinned_messages[].msgId must be a non-empty string", 400);
76628
76902
  }
76629
- if (seen.has(msgId)) continue;
76630
- seen.add(msgId);
76903
+ const rawId = entry.id;
76904
+ if (rawId !== void 0 && rawId !== null && (typeof rawId !== "string" || !rawId.trim() || rawId.length > MAX_PIN_ENTRY_ID_CHARS)) {
76905
+ throw new SessionControlError(
76906
+ `pinned_messages[].id must be a non-empty string (max ${MAX_PIN_ENTRY_ID_CHARS} chars)`,
76907
+ 400
76908
+ );
76909
+ }
76910
+ const id = typeof rawId === "string" ? rawId : void 0;
76911
+ const quote = entry.quote === void 0 || entry.quote === null ? void 0 : normalizePinQuote(entry.quote);
76912
+ let pinId = id;
76913
+ if (quote) {
76914
+ if (id && seenQuoteId.has(id)) continue;
76915
+ const shape = [msgId, quote.exact, quote.prefix ?? "", quote.suffix ?? ""].join("\0");
76916
+ if (seenQuoteShape.has(shape)) continue;
76917
+ pinId = id ?? randomUUID4();
76918
+ seenQuoteId.add(pinId);
76919
+ seenQuoteShape.add(shape);
76920
+ } else {
76921
+ if (seenWholeMessage.has(msgId)) continue;
76922
+ seenWholeMessage.add(msgId);
76923
+ }
76631
76924
  const role = entry.role === "user" || entry.role === "assistant" || entry.role === "system" ? entry.role : "assistant";
76632
76925
  const label = typeof entry.label === "string" ? entry.label.trim().slice(0, MAX_PIN_LABEL_CHARS) : "";
76633
76926
  const timestamp = typeof entry.timestamp === "string" ? entry.timestamp : void 0;
76634
76927
  const pinnedAt = typeof entry.pinnedAt === "string" ? entry.pinnedAt : (/* @__PURE__ */ new Date()).toISOString();
76635
- out.push({ msgId, label, role, ...timestamp ? { timestamp } : {}, pinnedAt });
76928
+ out.push({
76929
+ msgId,
76930
+ label,
76931
+ role,
76932
+ ...timestamp ? { timestamp } : {},
76933
+ pinnedAt,
76934
+ ...pinId ? { id: pinId } : {},
76935
+ ...quote ? { quote } : {}
76936
+ });
76636
76937
  }
76637
76938
  return out;
76638
76939
  }
@@ -76830,7 +77131,7 @@ async function retrySession(sessionId) {
76830
77131
  return { status: "reconnected", sessionId };
76831
77132
  }
76832
77133
  if (await resumeConversationExists(record)) {
76833
- const { sendMessageToSession: sendMessageToSession2, getQueue: getQueue2, unparkMessage: unparkMessage2 } = await Promise.resolve().then(() => (init_session_message_queue(), session_message_queue_exports));
77134
+ const { getQueue: getQueue2, unparkMessage: unparkMessage2 } = await Promise.resolve().then(() => (init_session_message_queue(), session_message_queue_exports));
76834
77135
  const pendingMsgs = await getQueue2(sessionId);
76835
77136
  for (const parked of pendingMsgs.filter((m6) => m6.status === "parked")) {
76836
77137
  await unparkMessage2(sessionId, parked.id);
@@ -76840,9 +77141,22 @@ async function retrySession(sessionId) {
76840
77141
  log.session.info("session retry: re-processing pending queue messages", { sessionId, taskId: record.taskId, count: pendingMsgs.length });
76841
77142
  return { status: "resuming", sessionId, restoredMessages: pendingMsgs.length };
76842
77143
  }
76843
- await sendMessageToSession2(sessionId, "continue", { source: "retry", taskId: record.taskId });
76844
- log.session.info("session retry: resuming via --resume (no pending messages)", { sessionId, taskId: record.taskId });
76845
- return { status: "resuming", sessionId };
77144
+ const relabelled = await updateSessionRecord2(sessionId, {
77145
+ process_status: "stopped",
77146
+ errorMessage: void 0,
77147
+ errorKind: void 0,
77148
+ activity: void 0,
77149
+ last_status_change: (/* @__PURE__ */ new Date()).toISOString(),
77150
+ status_reason: "retry_reconnect",
77151
+ status_changed_by: "user"
77152
+ });
77153
+ emitSessionStatusChanged2(relabelled, {}, ["*"]);
77154
+ log.session.info("session retry: reconnected \u2014 conversation preserved, no message sent", {
77155
+ sessionId,
77156
+ taskId: record.taskId,
77157
+ host: record.host ?? "__local__"
77158
+ });
77159
+ return { status: "resumable", sessionId };
76846
77160
  }
76847
77161
  log.session.warn("session retry: conversation never persisted \u2014 starting fresh instead of --resume", {
76848
77162
  sessionId,
@@ -76900,6 +77214,91 @@ async function retrySession(sessionId) {
76900
77214
  });
76901
77215
  return { status: "pending", taskId: task.id, oldSessionId: sessionId };
76902
77216
  }
77217
+ async function withDeadline(p6, ms) {
77218
+ let timer3;
77219
+ try {
77220
+ return await Promise.race([
77221
+ p6,
77222
+ new Promise((resolve) => {
77223
+ timer3 = setTimeout(() => resolve(RECHECK_TIMED_OUT), ms);
77224
+ if (timer3 && typeof timer3 === "object" && "unref" in timer3) timer3.unref();
77225
+ })
77226
+ ]);
77227
+ } finally {
77228
+ if (timer3) clearTimeout(timer3);
77229
+ }
77230
+ }
77231
+ async function recheckSession(sessionId) {
77232
+ const { getSessionByClaudeId: getSessionByClaudeId2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
77233
+ const record = await getSessionByClaudeId2(sessionId);
77234
+ if (!record) throw new SessionControlError("Session not found", 404);
77235
+ const hostKey2 = record.host ?? "__local__";
77236
+ const { classifySessionError: classifySessionError2 } = await Promise.resolve().then(() => (init_session_error_kind(), session_error_kind_exports));
77237
+ const kind = classifySessionError2(record);
77238
+ const base = { sessionId, processStatus: record.process_status, infraClaim: kind === "infra" };
77239
+ const { isDaemonConnected: isDaemonConnected2, getPooledSnapshotConnection: getPooledSnapshotConnection2 } = await Promise.resolve().then(() => (init_daemon_connection(), daemon_connection_exports));
77240
+ const reachable = isDaemonConnected2(hostKey2);
77241
+ if (kind === "terminal") return { ...base, checked: false, reachable, reason: "terminal_error" };
77242
+ if (record.archived) return { ...base, checked: false, reachable, reason: "archived" };
77243
+ const conn = getPooledSnapshotConnection2(record.host);
77244
+ if (!conn) {
77245
+ log.session.info("session recheck: no pooled snapshot connection \u2014 nothing to ask", {
77246
+ sessionId,
77247
+ host: hostKey2,
77248
+ reachable
77249
+ });
77250
+ return { ...base, checked: false, reachable, reason: "no_pooled_connection" };
77251
+ }
77252
+ let raced;
77253
+ try {
77254
+ raced = await withDeadline(
77255
+ conn.send("getState", { sid: sessionId }, RECHECK_RPC_TIMEOUT_MS),
77256
+ RECHECK_RPC_TIMEOUT_MS
77257
+ );
77258
+ } catch (err2) {
77259
+ log.session.info("session recheck: getState failed", {
77260
+ sessionId,
77261
+ host: hostKey2,
77262
+ error: err2 instanceof Error ? err2.message : String(err2)
77263
+ });
77264
+ return { ...base, checked: false, reachable, reason: "rpc_failed" };
77265
+ }
77266
+ if (raced === RECHECK_TIMED_OUT) {
77267
+ log.session.warn("session recheck: getState timed out", {
77268
+ sessionId,
77269
+ host: hostKey2,
77270
+ timeoutMs: RECHECK_RPC_TIMEOUT_MS
77271
+ });
77272
+ return { ...base, checked: false, reachable, reason: "timeout" };
77273
+ }
77274
+ const snapshot2 = raced?.ok ? raced.snapshot : void 0;
77275
+ if (!snapshot2) {
77276
+ return { ...base, checked: false, reachable: true, reason: "no_snapshot" };
77277
+ }
77278
+ const { applySnapshot: applySnapshot2 } = await Promise.resolve().then(() => (init_session_snapshot_apply(), session_snapshot_apply_exports));
77279
+ const outcome = await applySnapshot2(sessionId, snapshot2, "recheck-open");
77280
+ const after = await getSessionByClaudeId2(sessionId);
77281
+ const processStatus = after?.process_status ?? record.process_status;
77282
+ log.session.info("session recheck: reconciled against host", {
77283
+ sessionId,
77284
+ host: hostKey2,
77285
+ previous: record.process_status,
77286
+ processStatus,
77287
+ cliState: snapshot2.cliState,
77288
+ applyOutcome: outcome.outcome
77289
+ });
77290
+ return {
77291
+ sessionId,
77292
+ checked: true,
77293
+ reachable: true,
77294
+ alive: snapshot2.cliState !== "dead",
77295
+ processStatus,
77296
+ // Re-classify against the CURRENT record: a converged row may no longer
77297
+ // carry the claim, and the client uses this to decide whether the sentence
77298
+ // on screen is still true.
77299
+ infraClaim: classifySessionError2(after ?? record) === "infra"
77300
+ };
77301
+ }
76903
77302
  async function respondSessionPermission(sessionId, requestId, allow, denyMessage, optionId, answers) {
76904
77303
  if (typeof requestId !== "string" || !requestId || typeof allow !== "boolean") {
76905
77304
  throw new SessionControlError("requestId (string) and allow (boolean) are required", 400);
@@ -77193,7 +77592,7 @@ async function readSessionRichHistory(sessionId, tail) {
77193
77592
  ...historyWindowed ? { windowed: true } : {}
77194
77593
  };
77195
77594
  }
77196
- var CLAUDE_SESSION_MODES, MAX_PINNED_MESSAGES, MAX_PIN_LABEL_CHARS, MAX_PIN_ID_CHARS, GIT_BASES, MAX_FORK_DEPTH, HISTORY_STARTUP_GRACE_MS;
77595
+ 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;
77197
77596
  var init_session_lifecycle = __esm({
77198
77597
  "src/core/sessions/session-lifecycle.ts"() {
77199
77598
  "use strict";
@@ -77207,6 +77606,11 @@ var init_session_lifecycle = __esm({
77207
77606
  MAX_PINNED_MESSAGES = 200;
77208
77607
  MAX_PIN_LABEL_CHARS = 300;
77209
77608
  MAX_PIN_ID_CHARS = 200;
77609
+ MAX_PIN_ENTRY_ID_CHARS = 64;
77610
+ MAX_PIN_QUOTE_CHARS = 2e3;
77611
+ MAX_PIN_QUOTE_CONTEXT_CHARS = 64;
77612
+ RECHECK_RPC_TIMEOUT_MS = 5e3;
77613
+ RECHECK_TIMED_OUT = /* @__PURE__ */ Symbol("recheck-timeout");
77210
77614
  GIT_BASES = /* @__PURE__ */ new Set(["uncommitted", "previous", "remote"]);
77211
77615
  MAX_FORK_DEPTH = 5;
77212
77616
  HISTORY_STARTUP_GRACE_MS = 2 * 60 * 1e3;
@@ -77507,7 +77911,7 @@ __export(compact_inject_exports, {
77507
77911
  extractSessionMeta: () => extractSessionMeta,
77508
77912
  injectCompactBoundary: () => injectCompactBoundary
77509
77913
  });
77510
- import { randomUUID as randomUUID4 } from "crypto";
77914
+ import { randomUUID as randomUUID5 } from "crypto";
77511
77915
  import fsp26 from "fs/promises";
77512
77916
  async function extractSessionMeta(jsonlPath) {
77513
77917
  const content = await fsp26.readFile(jsonlPath, "utf-8");
@@ -77559,8 +77963,8 @@ async function injectCompactBoundary(jsonlPath, summary, meta) {
77559
77963
  log.session.error("Could not find last message UUID in JSONL", { jsonlPath });
77560
77964
  return null;
77561
77965
  }
77562
- const boundaryUuid = randomUUID4();
77563
- const summaryUuid = randomUUID4();
77966
+ const boundaryUuid = randomUUID5();
77967
+ const summaryUuid = randomUUID5();
77564
77968
  const now = /* @__PURE__ */ new Date();
77565
77969
  const boundaryTs = now.toISOString();
77566
77970
  const summaryTs = new Date(now.getTime() + 1e3).toISOString();
@@ -86589,7 +86993,7 @@ function isRevealSecretPath(resolved) {
86589
86993
  ];
86590
86994
  return denied.some((d8) => resolved === d8 || resolved.startsWith(d8 + path69.sep)) || /(^|\/)config\.ya?ml$/.test(resolved);
86591
86995
  }
86592
- 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;
86996
+ 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;
86593
86997
  var init_files2 = __esm({
86594
86998
  "src/web/routes/files.ts"() {
86595
86999
  "use strict";
@@ -86722,6 +87126,54 @@ var init_files2 = __esm({
86722
87126
  next(err2);
86723
87127
  }
86724
87128
  });
87129
+ LIST_MANY_MAX_PATHS = 64;
87130
+ LIST_MANY_CONCURRENCY = 8;
87131
+ LIST_MANY_DEADLINE_MS = 8e3;
87132
+ filesRouter.post("/list-many", async (req, res, next) => {
87133
+ try {
87134
+ const body3 = req.body ?? {};
87135
+ const raw = Array.isArray(body3.paths) ? body3.paths : null;
87136
+ if (!raw) {
87137
+ res.status(400).json({ error: "paths (string[]) is required" });
87138
+ return;
87139
+ }
87140
+ const host = typeof body3.host === "string" && body3.host ? body3.host : void 0;
87141
+ const showHidden = body3.showHidden === true || body3.showHidden === "1";
87142
+ const paths = [...new Set(
87143
+ raw.filter((p6) => typeof p6 === "string" && !!p6)
87144
+ )];
87145
+ const truncated = paths.length > LIST_MANY_MAX_PATHS;
87146
+ const wanted = paths.slice(0, LIST_MANY_MAX_PATHS);
87147
+ const deadline4 = Date.now() + LIST_MANY_DEADLINE_MS;
87148
+ const listings = [];
87149
+ let timedOut = false;
87150
+ let next_ = 0;
87151
+ const worker = async () => {
87152
+ for (; ; ) {
87153
+ const i8 = next_++;
87154
+ if (i8 >= wanted.length) return;
87155
+ if (Date.now() >= deadline4) {
87156
+ timedOut = true;
87157
+ return;
87158
+ }
87159
+ const dirPath = wanted[i8];
87160
+ try {
87161
+ listings.push(await listSessionFiles(dirPath, host, showHidden));
87162
+ } catch (err2) {
87163
+ listings.push({ path: dirPath, error: err2 instanceof Error ? err2.message : String(err2) });
87164
+ }
87165
+ }
87166
+ };
87167
+ await Promise.all(Array.from({ length: Math.min(LIST_MANY_CONCURRENCY, wanted.length) }, worker));
87168
+ res.json({ listings, truncated, timedOut });
87169
+ } catch (err2) {
87170
+ if (err2 instanceof FilesOpError) {
87171
+ res.status(err2.statusCode).json({ error: err2.message });
87172
+ return;
87173
+ }
87174
+ next(err2);
87175
+ }
87176
+ });
86725
87177
  REVEAL_APP_DENY_EXTS = /* @__PURE__ */ new Set([
86726
87178
  "command",
86727
87179
  "sh",
@@ -88296,6 +88748,82 @@ var init_task = __esm({
88296
88748
  }
88297
88749
  });
88298
88750
 
88751
+ // src/core/sessions/spawn-prefix.ts
88752
+ var spawn_prefix_exports = {};
88753
+ __export(spawn_prefix_exports, {
88754
+ MAX_SPAWN_PROMPT_BYTES: () => MAX_SPAWN_PROMPT_BYTES,
88755
+ parseSpawnPrefixFromArgs: () => parseSpawnPrefixFromArgs,
88756
+ readParentSpawnPrefix: () => readParentSpawnPrefix,
88757
+ spawnPrefixFromRecord: () => spawnPrefixFromRecord
88758
+ });
88759
+ function flagValue(args, flag) {
88760
+ const i8 = args.indexOf(flag);
88761
+ if (i8 < 0 || i8 + 1 >= args.length) return void 0;
88762
+ const v3 = args[i8 + 1];
88763
+ return v3 !== void 0 && !v3.startsWith("--") ? v3 : void 0;
88764
+ }
88765
+ function parseSpawnPrefixFromArgs(args) {
88766
+ const prompt = flagValue(args, "--append-system-prompt");
88767
+ return {
88768
+ appendSystemPrompt: prompt !== void 0 && Buffer.byteLength(prompt, "utf8") <= MAX_SPAWN_PROMPT_BYTES ? prompt : null,
88769
+ ...flagValue(args, "--model") ? { model: flagValue(args, "--model") } : {},
88770
+ ...EFFORTS.includes(flagValue(args, "--effort") ?? "") ? { effort: flagValue(args, "--effort") } : {},
88771
+ ...flagValue(args, "--permission-mode") ? { permissionMode: flagValue(args, "--permission-mode") } : {},
88772
+ source: "live-process"
88773
+ };
88774
+ }
88775
+ function spawnPrefixFromRecord(record) {
88776
+ const stored = record.appliedAppendSystemPrompt;
88777
+ if (stored === void 0 || stored === null) return { appendSystemPrompt: null, source: "unknown" };
88778
+ const usable = stored.length > 0 && Buffer.byteLength(stored, "utf8") <= MAX_SPAWN_PROMPT_BYTES;
88779
+ return { appendSystemPrompt: usable ? stored : null, source: "record" };
88780
+ }
88781
+ async function defaultLiveArgsReader(hostKey2, sessionId) {
88782
+ const { probeDaemonSessionArgs: probeDaemonSessionArgs2 } = await Promise.resolve().then(() => (init_daemon_connection(), daemon_connection_exports));
88783
+ return probeDaemonSessionArgs2(hostKey2, sessionId);
88784
+ }
88785
+ async function readParentSpawnPrefix(parent, deps = {}) {
88786
+ const readLiveArgs = deps.readLiveArgs ?? defaultLiveArgsReader;
88787
+ const hostKey2 = parent.host || "__local__";
88788
+ let live = null;
88789
+ try {
88790
+ live = await readLiveArgs(hostKey2, parent.claudeSessionId);
88791
+ } catch (err2) {
88792
+ log.session.debug("spawn prefix: live argv read threw", {
88793
+ sessionId: parent.claudeSessionId,
88794
+ error: err2 instanceof Error ? err2.message : String(err2)
88795
+ });
88796
+ }
88797
+ if (live && live.length > 0) {
88798
+ const prefix = parseSpawnPrefixFromArgs(live);
88799
+ const remembered = parent.appliedAppendSystemPrompt ?? void 0;
88800
+ const actual = prefix.appendSystemPrompt ?? "";
88801
+ if (remembered !== actual) {
88802
+ const persist4 = deps.persist ?? (async (sid, prompt) => {
88803
+ const { updateSessionRecord: updateSessionRecord2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
88804
+ return updateSessionRecord2(sid, { appliedAppendSystemPrompt: prompt });
88805
+ });
88806
+ void persist4(parent.claudeSessionId, actual).catch((err2) => {
88807
+ log.session.warn("spawn prefix: backfill of applied prompt failed", {
88808
+ sessionId: parent.claudeSessionId,
88809
+ error: err2 instanceof Error ? err2.message : String(err2)
88810
+ });
88811
+ });
88812
+ }
88813
+ return prefix;
88814
+ }
88815
+ return spawnPrefixFromRecord(parent);
88816
+ }
88817
+ var EFFORTS, MAX_SPAWN_PROMPT_BYTES;
88818
+ var init_spawn_prefix = __esm({
88819
+ "src/core/sessions/spawn-prefix.ts"() {
88820
+ "use strict";
88821
+ init_logging();
88822
+ EFFORTS = ["low", "medium", "high", "xhigh", "max"];
88823
+ MAX_SPAWN_PROMPT_BYTES = 65536;
88824
+ }
88825
+ });
88826
+
88299
88827
  // src/core/sessions/side-thread-fork.ts
88300
88828
  var side_thread_fork_exports = {};
88301
88829
  __export(side_thread_fork_exports, {
@@ -88352,6 +88880,10 @@ async function forkSideThreadSession(parentSid, threadId, opts) {
88352
88880
  const sessionId = crypto16.randomUUID();
88353
88881
  const lane = sideThreadLaneKey(parentSid, threadId);
88354
88882
  const mode = parent.mode !== "default" ? parent.mode : void 0;
88883
+ const { readParentSpawnPrefix: readParentSpawnPrefix2 } = await Promise.resolve().then(() => (init_spawn_prefix(), spawn_prefix_exports));
88884
+ const prefix = await readParentSpawnPrefix2(parent);
88885
+ const cliModel = prefix.model ?? parent.cliModel;
88886
+ const effort = prefix.effort ?? parent.effort;
88355
88887
  await createSessionRecord(sessionId, "", parent.project ?? "", parent.cwd, {
88356
88888
  title,
88357
88889
  ...mode ? { mode } : {},
@@ -88359,8 +88891,10 @@ async function forkSideThreadSession(parentSid, threadId, opts) {
88359
88891
  // The thread inherits the parent's launch bundle verbatim, re-applied from
88360
88892
  // the record on every cold resume.
88361
88893
  ...parent.profile ? { profile: parent.profile } : {},
88362
- ...parent.effort ? { effort: parent.effort } : {},
88363
- ...parent.cliModel ? { cliModel: parent.cliModel } : {},
88894
+ ...effort ? { effort } : {},
88895
+ ...cliModel ? { cliModel } : {},
88896
+ // '' = the parent runs without an append prompt, and so does the thread.
88897
+ appliedAppendSystemPrompt: prefix.appendSystemPrompt ?? "",
88364
88898
  lane,
88365
88899
  forkedFromSessionId: resumeFrom,
88366
88900
  initialProcessStatus: "idle",
@@ -88376,11 +88910,12 @@ async function forkSideThreadSession(parentSid, threadId, opts) {
88376
88910
  ...mode ? { mode } : {},
88377
88911
  ...parent.host ? { host: parent.host } : {},
88378
88912
  ...parent.profile ? { profile: parent.profile } : {},
88379
- ...parent.effort ? { effort: parent.effort } : {},
88913
+ ...effort ? { effort } : {},
88380
88914
  // Must be `cliModel` (the parent's verbatim --model arg, [1m] marker
88381
88915
  // included): the CLI does NOT inherit a model across --fork-session, and
88382
88916
  // the reported `model` has lost the marker.
88383
- ...parent.cliModel ? { model: parent.cliModel } : {},
88917
+ ...cliModel ? { model: cliModel } : {},
88918
+ appendSystemPromptExact: prefix.appendSystemPrompt,
88384
88919
  lane,
88385
88920
  forkedFromSessionId: resumeFrom
88386
88921
  }, ["session-runner"], { source: "side-thread-fork" });
@@ -88389,7 +88924,9 @@ async function forkSideThreadSession(parentSid, threadId, opts) {
88389
88924
  threadId,
88390
88925
  sessionId,
88391
88926
  resumeFrom,
88392
- initOnly: !opts?.message
88927
+ initOnly: !opts?.message,
88928
+ prefixSource: prefix.source,
88929
+ prefixPromptLength: prefix.appendSystemPrompt?.length ?? 0
88393
88930
  });
88394
88931
  return { sessionId, resumeFromSessionId: resumeFrom };
88395
88932
  }
@@ -100645,14 +101182,16 @@ function isLive(r8) {
100645
101182
  function isRevivable(r8) {
100646
101183
  return !!r8.outputFile || r8.consumedOffset !== void 0;
100647
101184
  }
100648
- var STANDBY_TTL_MS, MAX_LIVE_THREADS, IDLE_SWEEP_INTERVAL_MS, THREAD_IDLE_MS, SideThreadManager, sideThreadManager;
101185
+ var STANDBY_TTL_MS, WARMED_STANDBY_TTL_MS, MAX_LIVE_THREADS, IDLE_SWEEP_INTERVAL_MS, THREAD_IDLE_MS, SideThreadManager, sideThreadManager;
100649
101186
  var init_side_thread_manager = __esm({
100650
101187
  "src/core/sessions/side-thread-manager.ts"() {
100651
101188
  "use strict";
100652
101189
  init_logging();
100653
101190
  init_side_thread_fork();
100654
101191
  init_session_controls();
101192
+ init_side_thread_warmup();
100655
101193
  STANDBY_TTL_MS = 12e4;
101194
+ WARMED_STANDBY_TTL_MS = 15 * 6e4;
100656
101195
  MAX_LIVE_THREADS = 3;
100657
101196
  IDLE_SWEEP_INTERVAL_MS = 5 * 6e4;
100658
101197
  THREAD_IDLE_MS = 30 * 6e4;
@@ -100663,6 +101202,8 @@ var init_side_thread_manager = __esm({
100663
101202
  * In-memory only: a server restart loses it, but the boot sweep archives
100664
101203
  * every standby anyway, so a record can never outlive its map entry. */
100665
101204
  standbyParentOffsets = /* @__PURE__ */ new Map();
101205
+ /** parentSid → standby sid that already ran its cache warm-up turn. */
101206
+ warmedStandbys = /* @__PURE__ */ new Map();
100666
101207
  /** One in-flight standby/create op per parent — two concurrent asks must not
100667
101208
  * both consume the same standby (they would collide on its lane rename). */
100668
101209
  inFlight = /* @__PURE__ */ new Map();
@@ -100694,6 +101235,7 @@ var init_side_thread_manager = __esm({
100694
101235
  for (const timer3 of this.standbyTimers.values()) clearTimeout(timer3);
100695
101236
  this.standbyTimers.clear();
100696
101237
  this.standbyParentOffsets.clear();
101238
+ this.warmedStandbys.clear();
100697
101239
  this.inFlight.clear();
100698
101240
  }
100699
101241
  // ── Standby ────────────────────────────────────────────────────────────────
@@ -100709,10 +101251,12 @@ var init_side_thread_manager = __esm({
100709
101251
  const existing = await this.findStandby(parentSid);
100710
101252
  if (existing) {
100711
101253
  if (await this.isStandbyUsable(parentSid, existing)) {
100712
- this.armStandbyTtl(parentSid, existing.claudeSessionId);
101254
+ const warmed = this.warmedStandbys.get(parentSid) === existing.claudeSessionId;
101255
+ this.armStandbyTtl(parentSid, existing.claudeSessionId, warmed ? WARMED_STANDBY_TTL_MS : STANDBY_TTL_MS);
100713
101256
  return existing.claudeSessionId;
100714
101257
  }
100715
101258
  void this.retireSession(existing.claudeSessionId, "side_thread_standby_stale");
101259
+ this.warmedStandbys.delete(parentSid);
100716
101260
  }
100717
101261
  const parentOffset = parent.consumedOffset;
100718
101262
  const { sessionId } = await forkSideThreadSession(parentSid, STANDBY_THREAD_ID);
@@ -100738,7 +101282,7 @@ var init_side_thread_manager = __esm({
100738
101282
  if (!parent) return false;
100739
101283
  return parent.consumedOffset === this.standbyParentOffsets.get(parentSid);
100740
101284
  }
100741
- armStandbyTtl(parentSid, sessionId) {
101285
+ armStandbyTtl(parentSid, sessionId, ttlMs = STANDBY_TTL_MS) {
100742
101286
  const prev = this.standbyTimers.get(parentSid);
100743
101287
  if (prev) clearTimeout(prev);
100744
101288
  const timer3 = setTimeout(() => {
@@ -100748,13 +101292,41 @@ var init_side_thread_manager = __esm({
100748
101292
  const record = await getSessionByClaudeId2(sessionId).catch(() => null);
100749
101293
  if (!record || parseSideLaneKey(record.lane)?.threadId !== STANDBY_THREAD_ID) return;
100750
101294
  this.standbyParentOffsets.delete(parentSid);
101295
+ this.warmedStandbys.delete(parentSid);
100751
101296
  await this.retireSession(sessionId, "side_thread_standby_ttl");
100752
101297
  }).catch(() => {
100753
101298
  });
100754
- }, STANDBY_TTL_MS);
101299
+ }, ttlMs);
100755
101300
  timer3.unref?.();
100756
101301
  this.standbyTimers.set(parentSid, timer3);
100757
101302
  }
101303
+ /**
101304
+ * Run the standby's cache warm-up turn (see side-thread-warmup.ts). Called
101305
+ * when the user starts TYPING a new question: the fork's first API call pays
101306
+ * the full prefix write no matter what, so paying it while they type turns
101307
+ * the actual question into a cached follow-up. Idempotent per standby; a
101308
+ * missing or stale standby is a no-op (the ask path will fork fresh anyway).
101309
+ */
101310
+ async warmStandby(parentSid) {
101311
+ return this.serialize(parentSid, async () => {
101312
+ const standby = await this.findStandby(parentSid);
101313
+ if (!standby) return { warmed: false, reason: "no_standby" };
101314
+ if (!await this.isStandbyUsable(parentSid, standby)) return { warmed: false, reason: "stale" };
101315
+ if (this.warmedStandbys.get(parentSid) === standby.claudeSessionId) {
101316
+ return { warmed: true, reason: "already_warm" };
101317
+ }
101318
+ const { sendMessageToSession: sendMessageToSession2 } = await Promise.resolve().then(() => (init_session_message_queue(), session_message_queue_exports));
101319
+ markWarmupTurnPending(standby.claudeSessionId);
101320
+ await sendMessageToSession2(standby.claudeSessionId, CACHE_WARMUP_MESSAGE, { source: "side-thread-warmup" });
101321
+ this.warmedStandbys.set(parentSid, standby.claudeSessionId);
101322
+ this.armStandbyTtl(parentSid, standby.claudeSessionId, WARMED_STANDBY_TTL_MS);
101323
+ log.session.info("side thread: standby cache warm-up sent", {
101324
+ parentSid,
101325
+ standbySid: standby.claudeSessionId
101326
+ });
101327
+ return { warmed: true };
101328
+ });
101329
+ }
100758
101330
  // ── Threads ────────────────────────────────────────────────────────────────
100759
101331
  /**
100760
101332
  * Open a side thread on `parentSid` and ask `question` in it.
@@ -100765,6 +101337,10 @@ var init_side_thread_manager = __esm({
100765
101337
  * question as its spawn's first turn instead — a send issued right after
100766
101338
  * SESSION_START can lose that race and cold-`--resume` an id the CLI has never
100767
101339
  * seen (see personal-ai-lane.ts LaneSession.created).
101340
+ *
101341
+ * `imageContext` (attachment paths for the CLI to Read) rides the MESSAGE only.
101342
+ * The stored entry keeps the plain question, so the chip label, the injected
101343
+ * transcript and a promoted task never carry the machine preamble.
100768
101344
  */
100769
101345
  async createThread(parentSid, input) {
100770
101346
  const question = input.question?.trim();
@@ -100772,8 +101348,11 @@ var init_side_thread_manager = __esm({
100772
101348
  return this.serialize(parentSid, async () => {
100773
101349
  const threadId = mintSideThreadId();
100774
101350
  const title = input.title?.trim() || void 0;
101351
+ const message = input.imageContext ? `${input.imageContext}
101352
+
101353
+ ${question}` : question;
100775
101354
  const consumed = await this.consumeStandby(parentSid, threadId, title);
100776
- const threadSessionId = consumed ?? (await forkSideThreadSession(parentSid, threadId, { message: question, ...title ? { title } : {} })).sessionId;
101355
+ const threadSessionId = consumed ?? (await forkSideThreadSession(parentSid, threadId, { message, ...title ? { title } : {} })).sessionId;
100777
101356
  let entry;
100778
101357
  try {
100779
101358
  const { addSideThread: addSideThread2 } = await Promise.resolve().then(() => (init_side_questions(), side_questions_exports));
@@ -100785,7 +101364,7 @@ var init_side_thread_manager = __esm({
100785
101364
  });
100786
101365
  if (consumed) {
100787
101366
  const { sendMessageToSession: sendMessageToSession2 } = await Promise.resolve().then(() => (init_session_message_queue(), session_message_queue_exports));
100788
- await sendMessageToSession2(threadSessionId, question, { source: "side-thread" });
101367
+ await sendMessageToSession2(threadSessionId, message, { source: "side-thread" });
100789
101368
  }
100790
101369
  } catch (err2) {
100791
101370
  void this.retireSession(threadSessionId, "side_thread_create_failed");
@@ -100820,6 +101399,7 @@ var init_side_thread_manager = __esm({
100820
101399
  if (!await this.isStandbyUsable(parentSid, standby)) {
100821
101400
  void this.retireSession(standby.claudeSessionId, "side_thread_standby_stale");
100822
101401
  this.standbyParentOffsets.delete(parentSid);
101402
+ this.warmedStandbys.delete(parentSid);
100823
101403
  return null;
100824
101404
  }
100825
101405
  const timer3 = this.standbyTimers.get(parentSid);
@@ -100828,6 +101408,7 @@ var init_side_thread_manager = __esm({
100828
101408
  this.standbyTimers.delete(parentSid);
100829
101409
  }
100830
101410
  this.standbyParentOffsets.delete(parentSid);
101411
+ this.warmedStandbys.delete(parentSid);
100831
101412
  const lane = sideThreadLaneKey(parentSid, threadId);
100832
101413
  const { updateSessionRecord: updateSessionRecord2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
100833
101414
  await updateSessionRecord2(standby.claudeSessionId, {
@@ -101014,16 +101595,30 @@ async function promoteSideThread(parentSid, threadId, opts) {
101014
101595
  const threadRecord = await getSessionByClaudeId2(sessionId);
101015
101596
  if (!threadRecord) throw new SessionControlError("Side thread session not found", 404);
101016
101597
  const parentRecord = await getSessionByClaudeId2(parentSid);
101017
- const parentTaskId = parentRecord?.taskId?.trim() || void 0;
101018
- const title = opts?.title?.trim() || entry.title?.trim() || entry.question;
101598
+ const siblingOfTaskId = parentRecord?.taskId?.trim() || void 0;
101599
+ const threadLabel = opts?.title?.trim() || entry.title?.trim() || void 0;
101019
101600
  const { addTask: addTask2, linkSession: linkSession2, linkSessionSlot: linkSessionSlot2 } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
101020
- const { task } = await addTask2({
101021
- title,
101022
- description: entry.question,
101023
- ...parentTaskId ? { parent_task_id: parentTaskId } : {},
101024
- // A person clicked "promote" — same board default as any hand-made task.
101025
- pinned: true
101026
- });
101601
+ let groupId;
101602
+ let task;
101603
+ if (siblingOfTaskId) {
101604
+ const created = await createForkSiblingTask(siblingOfTaskId, {
101605
+ // A known label short-circuits the background refine; without one the
101606
+ // thread's first question is what gets summarized (same as a fork's message).
101607
+ titlePrefix: threadLabel,
101608
+ prompt: entry.question,
101609
+ description: entry.question,
101610
+ source: "side-thread-promote"
101611
+ });
101612
+ task = created.task;
101613
+ groupId = created.groupId;
101614
+ } else {
101615
+ ({ task } = await addTask2({
101616
+ title: threadLabel || entry.question,
101617
+ description: entry.question,
101618
+ // A person clicked "promote" — same board default as any hand-made task.
101619
+ pinned: true
101620
+ }));
101621
+ }
101027
101622
  const stamped = await markThreadPromoted2(parentSid, threadId, task.id);
101028
101623
  if (!stamped) {
101029
101624
  log.session.warn("side thread promote: entry vanished before stamp \u2014 promote proceeds, 409 guard is unarmed", {
@@ -101037,7 +101632,7 @@ async function promoteSideThread(parentSid, threadId, opts) {
101037
101632
  project: task.project ?? "",
101038
101633
  // Clearing the lane un-hides the session (see the file header).
101039
101634
  lane: void 0,
101040
- title
101635
+ title: task.title
101041
101636
  });
101042
101637
  try {
101043
101638
  const { sessionRunner: sessionRunner2 } = await Promise.resolve().then(() => (init_claude_code_session(), claude_code_session_exports));
@@ -101056,10 +101651,18 @@ async function promoteSideThread(parentSid, threadId, opts) {
101056
101651
  error: err2 instanceof Error ? err2.message : String(err2)
101057
101652
  });
101058
101653
  }
101059
- log.session.info("side thread: promoted to task", { parentSid, threadId, sessionId, taskId: task.id });
101654
+ log.session.info("side thread: promoted to task", {
101655
+ parentSid,
101656
+ threadId,
101657
+ sessionId,
101658
+ taskId: task.id,
101659
+ siblingOfTaskId,
101660
+ groupId
101661
+ });
101060
101662
  return {
101061
101663
  taskId: task.id,
101062
- ...task.parent_task_id ? { parentTaskId: task.parent_task_id } : {},
101664
+ ...siblingOfTaskId ? { siblingOfTaskId } : {},
101665
+ ...groupId ? { groupId } : {},
101063
101666
  sessionId
101064
101667
  };
101065
101668
  }
@@ -101074,7 +101677,7 @@ var init_side_thread_promote = __esm({
101074
101677
 
101075
101678
  // src/web/routes/sessions.ts
101076
101679
  import { Router as Router10 } from "express";
101077
- import { randomUUID as randomUUID5 } from "crypto";
101680
+ import { randomUUID as randomUUID6 } from "crypto";
101078
101681
  import os19 from "os";
101079
101682
  import path91 from "path";
101080
101683
  function condenseStaleReason(msg) {
@@ -101420,7 +102023,7 @@ var init_sessions2 = __esm({
101420
102023
  }
101421
102024
  const isNativeEngine = !isAcpEngine(engine);
101422
102025
  const clientSessionId = typeof req.body.sessionId === "string" && UUID_RE2.test(req.body.sessionId) ? req.body.sessionId : void 0;
101423
- const preassignedSessionId = isNativeEngine ? clientSessionId ?? randomUUID5() : void 0;
102026
+ const preassignedSessionId = isNativeEngine ? clientSessionId ?? randomUUID6() : void 0;
101424
102027
  const updatedTask = await quickStartSession({
101425
102028
  message: sessionMessage,
101426
102029
  messagePrefix,
@@ -102395,18 +102998,38 @@ var init_sessions2 = __esm({
102395
102998
  });
102396
102999
  }
102397
103000
  });
103001
+ sessionsRouter.post("/:sessionId/side-threads/standby/warm", async (req, res) => {
103002
+ if (refuseSideThreadsOnReplica(res)) return;
103003
+ const sessionId = String(req.params.sessionId);
103004
+ try {
103005
+ const { sideThreadManager: sideThreadManager2 } = await Promise.resolve().then(() => (init_side_thread_manager(), side_thread_manager_exports));
103006
+ res.json(await sideThreadManager2.warmStandby(sessionId));
103007
+ } catch (err2) {
103008
+ log.web.warn("side thread standby warm-up failed", {
103009
+ sessionId,
103010
+ error: err2 instanceof Error ? err2.message : String(err2)
103011
+ });
103012
+ res.json({ warmed: false, reason: "error" });
103013
+ }
103014
+ });
102398
103015
  sessionsRouter.post("/:sessionId/side-threads", async (req, res, next) => {
102399
103016
  if (refuseSideThreadsOnReplica(res)) return;
102400
103017
  try {
102401
- const { question, title } = req.body ?? {};
103018
+ const { question, title, images } = req.body ?? {};
102402
103019
  if (!question || typeof question !== "string" || !question.trim()) {
102403
103020
  res.status(400).json({ error: "question (non-empty string) is required" });
102404
103021
  return;
102405
103022
  }
103023
+ let imageContext = "";
103024
+ if (images && images.length > 0) {
103025
+ const processed = await processAndSaveImages(images);
103026
+ if (processed) imageContext = buildSessionImageContext(processed.savedImages);
103027
+ }
102406
103028
  const { sideThreadManager: sideThreadManager2 } = await Promise.resolve().then(() => (init_side_thread_manager(), side_thread_manager_exports));
102407
103029
  const thread = await sideThreadManager2.createThread(String(req.params.sessionId), {
102408
103030
  question,
102409
- ...typeof title === "string" && title.trim() ? { title } : {}
103031
+ ...typeof title === "string" && title.trim() ? { title } : {},
103032
+ ...imageContext ? { imageContext } : {}
102410
103033
  });
102411
103034
  res.json({ thread });
102412
103035
  } catch (err2) {
@@ -102425,7 +103048,8 @@ var init_sessions2 = __esm({
102425
103048
  );
102426
103049
  res.json({
102427
103050
  taskId: result.taskId,
102428
- ...result.parentTaskId ? { parentTaskId: result.parentTaskId } : {},
103051
+ ...result.siblingOfTaskId ? { siblingOfTaskId: result.siblingOfTaskId } : {},
103052
+ ...result.groupId ? { groupId: result.groupId } : {},
102429
103053
  sessionId: result.sessionId
102430
103054
  });
102431
103055
  } catch (err2) {
@@ -102616,6 +103240,19 @@ var init_sessions2 = __esm({
102616
103240
  next(err2);
102617
103241
  }
102618
103242
  });
103243
+ sessionsRouter.post("/:sessionId/recheck", async (req, res, next) => {
103244
+ try {
103245
+ const sessionId = req.params.sessionId;
103246
+ const result = await recheckSession(sessionId);
103247
+ res.json(result);
103248
+ } catch (err2) {
103249
+ if (err2 instanceof SessionControlError) {
103250
+ res.status(err2.statusCode).json({ error: err2.message });
103251
+ return;
103252
+ }
103253
+ next(err2);
103254
+ }
103255
+ });
102619
103256
  sessionsRouter.post("/:sessionId/restart", async (req, res, next) => {
102620
103257
  const sessionId = req.params.sessionId;
102621
103258
  const startedAt = Date.now();
@@ -103107,7 +103744,7 @@ var micro_claude_exports = {};
103107
103744
  __export(micro_claude_exports, {
103108
103745
  runMicroClaude: () => runMicroClaude
103109
103746
  });
103110
- import { randomUUID as randomUUID6 } from "crypto";
103747
+ import { randomUUID as randomUUID7 } from "crypto";
103111
103748
  async function runMicroClaude(opts) {
103112
103749
  if (opts.warm) {
103113
103750
  const run10 = await runWarmMicroClaude({
@@ -103116,7 +103753,7 @@ async function runMicroClaude(opts) {
103116
103753
  tools: opts.tools ?? [],
103117
103754
  prompt: opts.prompt,
103118
103755
  timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2,
103119
- toolUseId: opts.toolUseId ?? `micro-claude-${randomUUID6()}`,
103756
+ toolUseId: opts.toolUseId ?? `micro-claude-${randomUUID7()}`,
103120
103757
  ...opts.onBlock ? { onBlock: opts.onBlock } : {}
103121
103758
  });
103122
103759
  return { response: run10.response, costUsd: run10.costUsd, durationMs: run10.durationMs };
@@ -103126,7 +103763,7 @@ async function runMicroClaude(opts) {
103126
103763
  systemPrompt: opts.system,
103127
103764
  model: opts.model ?? DEFAULT_MODEL2,
103128
103765
  timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2,
103129
- toolUseId: opts.toolUseId ?? `micro-claude-${randomUUID6()}`,
103766
+ toolUseId: opts.toolUseId ?? `micro-claude-${randomUUID7()}`,
103130
103767
  slim: true,
103131
103768
  thinking: opts.thinking ?? false,
103132
103769
  ...opts.tools && opts.tools.length > 0 ? { tools: opts.tools } : {},
@@ -103951,7 +104588,7 @@ __export(micro_agent_exports, {
103951
104588
  async function resolveTierModel(tier) {
103952
104589
  const { getConfig: getConfig2 } = await Promise.resolve().then(() => (init_config_manager(), config_manager_exports));
103953
104590
  const config = await getConfig2();
103954
- const provider = config.agent?.main_provider ?? "bedrock";
104591
+ const provider = resolveMainProviderName(config);
103955
104592
  const { MODEL_CATALOG: MODEL_CATALOG3 } = await Promise.resolve().then(() => (init_model_catalog(), model_catalog_exports));
103956
104593
  const entry = MODEL_CATALOG3[provider]?.find((m6) => {
103957
104594
  const id = m6.id.toLowerCase();
@@ -104020,6 +104657,7 @@ var DEFAULT_TIER, DEFAULT_MAX_TOKENS2, DEFAULT_MAX_ROUNDS, DEFAULT_TIMEOUT_MS3;
104020
104657
  var init_micro_agent = __esm({
104021
104658
  "src/agent/micro-agent.ts"() {
104022
104659
  "use strict";
104660
+ init_default_provider();
104023
104661
  DEFAULT_TIER = "sonnet";
104024
104662
  DEFAULT_MAX_TOKENS2 = 2e3;
104025
104663
  DEFAULT_MAX_ROUNDS = 3;
@@ -104035,7 +104673,7 @@ __export(task_search_agent_exports, {
104035
104673
  prewarmAgentSearchChild: () => prewarmAgentSearchChild,
104036
104674
  runTaskSearchAgent: () => runTaskSearchAgent
104037
104675
  });
104038
- import { randomUUID as randomUUID7 } from "crypto";
104676
+ import { randomUUID as randomUUID8 } from "crypto";
104039
104677
  function useCliEngine() {
104040
104678
  return process.env.WALNUT_AGENT_SEARCH_ENGINE !== "inprocess";
104041
104679
  }
@@ -104115,7 +104753,7 @@ async function claudeCliEngine(userPrompt, options) {
104115
104753
  model: options.model,
104116
104754
  timeoutMs: options.timeoutMs,
104117
104755
  tools: ["Bash"],
104118
- toolUseId: `task-search-${randomUUID7()}`,
104756
+ toolUseId: `task-search-${randomUUID8()}`,
104119
104757
  warm: true,
104120
104758
  onBlock
104121
104759
  });
@@ -105319,9 +105957,13 @@ var init_credential_verify = __esm({
105319
105957
  var config_exports = {};
105320
105958
  __export(config_exports, {
105321
105959
  buildProvidersPayload: () => buildProvidersPayload,
105322
- configRouter: () => configRouter
105960
+ configRouter: () => configRouter,
105961
+ setStaticRootReporter: () => setStaticRootReporter
105323
105962
  });
105324
105963
  import { Router as Router14 } from "express";
105964
+ function setStaticRootReporter(fn) {
105965
+ staticRootReporter = fn;
105966
+ }
105325
105967
  async function fetchOllamaModels(baseUrl) {
105326
105968
  if (Date.now() - _ollamaCache.ts < OLLAMA_CACHE_TTL) return _ollamaCache.models;
105327
105969
  const url = (baseUrl ?? DEFAULT_BASE_URLS["ollama"] ?? "http://localhost:11434") + "/api/tags";
@@ -105374,10 +106016,11 @@ async function buildProvidersPayload() {
105374
106016
  providers[name] = {
105375
106017
  api: prov.api,
105376
106018
  base_url: prov.base_url,
105377
- status: caps.subscriptionReady ? "ready" : "no_key",
106019
+ status: caps.installed ? "ready" : "no_key",
105378
106020
  auto_detected: !explicitNames.has(name),
105379
106021
  models: getModelsForProvider(name, prov.models),
105380
- credential_source: caps.subscriptionReady ? "subscription" : caps.installed ? "cli_no_subscription" : "cli_not_installed"
106022
+ credential_source: caps.installed ? `cli_${caps.auth.mode}` : "cli_not_installed",
106023
+ credential_detail: caps.installed ? caps.auth.label : void 0
105381
106024
  };
105382
106025
  continue;
105383
106026
  }
@@ -105436,10 +106079,11 @@ async function buildProvidersPayload() {
105436
106079
  const caps = detectClaudeCli2();
105437
106080
  providers[name] = {
105438
106081
  api: template.api,
105439
- status: caps.subscriptionReady ? "ready" : "no_key",
106082
+ status: caps.installed ? "ready" : "no_key",
105440
106083
  auto_detected: false,
105441
106084
  models: getModelsForProvider(name),
105442
- credential_source: caps.subscriptionReady ? "subscription" : caps.installed ? "cli_no_subscription" : "cli_not_installed"
106085
+ credential_source: caps.installed ? `cli_${caps.auth.mode}` : "cli_not_installed",
106086
+ credential_detail: caps.installed ? caps.auth.label : void 0
105443
106087
  };
105444
106088
  continue;
105445
106089
  }
@@ -105454,7 +106098,7 @@ async function buildProvidersPayload() {
105454
106098
  }
105455
106099
  return { providers };
105456
106100
  }
105457
- var configRouter, _ollamaCache, OLLAMA_CACHE_TTL;
106101
+ var configRouter, staticRootReporter, _ollamaCache, OLLAMA_CACHE_TTL;
105458
106102
  var init_config11 = __esm({
105459
106103
  "src/web/routes/config.ts"() {
105460
106104
  "use strict";
@@ -105469,6 +106113,7 @@ var init_config11 = __esm({
105469
106113
  init_defaults();
105470
106114
  init_config_redact();
105471
106115
  configRouter = Router14();
106116
+ staticRootReporter = null;
105472
106117
  configRouter.get("/", async (_req, res, next) => {
105473
106118
  try {
105474
106119
  const config = await getConfig();
@@ -105495,7 +106140,8 @@ var init_config11 = __esm({
105495
106140
  remoteIdUniquenessGaps = getExtIndexUniquenessGaps2();
105496
106141
  } catch {
105497
106142
  }
105498
- 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 });
106143
+ const webAssets = staticRootReporter?.() ?? null;
106144
+ 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 });
105499
106145
  } catch (err2) {
105500
106146
  next(err2);
105501
106147
  }
@@ -105593,7 +106239,7 @@ var init_config11 = __esm({
105593
106239
  const {
105594
106240
  traceCredentialResolution: traceCredentialResolution2,
105595
106241
  resolveCredentials: resolveCredentials2,
105596
- readClaudeSettingsEnv: readClaudeSettingsEnv2,
106242
+ readClaudeSettingsEnv: readClaudeSettingsEnv3,
105597
106243
  readClaudeCredentialExport: readClaudeCredentialExport2
105598
106244
  } = await Promise.resolve().then(() => (init_credential_resolver(), credential_resolver_exports));
105599
106245
  const fs121 = await import("fs");
@@ -105609,7 +106255,7 @@ var init_config11 = __esm({
105609
106255
  };
105610
106256
  const trace = traceCredentialResolution2({
105611
106257
  config,
105612
- claudeEnv: readClaudeSettingsEnv2(),
106258
+ claudeEnv: readClaudeSettingsEnv3(),
105613
106259
  processEnv: process.env,
105614
106260
  awsFiles: {
105615
106261
  credentials: exists3(path193.join(home, ".aws", "credentials")),
@@ -110364,6 +111010,9 @@ var init_session_chat = __esm({
110364
111010
 
110365
111011
  // src/web/routes/browser-logs.ts
110366
111012
  import { Router as Router25 } from "express";
111013
+ function entrySubsystem(entry) {
111014
+ return entry.subsystem && ENTRY_SUBSYSTEMS.has(entry.subsystem) ? entry.subsystem : "browser";
111015
+ }
110367
111016
  function checkRate(client, entryCount) {
110368
111017
  const now = Date.now();
110369
111018
  let state9 = clientRates.get(client);
@@ -110405,10 +111054,11 @@ function writeEntries(entries) {
110405
111054
  if (entry.args) meta.args = truncate(entry.args, 1e3);
110406
111055
  if (entry.url) meta.url = entry.url;
110407
111056
  if (entry.count && entry.count > 1) meta.count = entry.count;
111057
+ if (entry.client === "mac-app") meta.client = entry.client;
110408
111058
  writeLogEntry({
110409
111059
  time: entry.time,
110410
111060
  level: mapLevel(entry.level),
110411
- subsystem: "browser",
111061
+ subsystem: entrySubsystem(entry),
110412
111062
  message: truncate(entry.message, 2e3),
110413
111063
  browserLevel: entry.level,
110414
111064
  // preserve original level (e.g. 'log' vs 'info')
@@ -110431,13 +111081,14 @@ function registerBrowserLogsRpc() {
110431
111081
  writeEntries(entries);
110432
111082
  });
110433
111083
  }
110434
- var RATE_WINDOW_MS, RATE_LIMIT, clientRates, REST_MAX_ENTRIES, browserLogsRouter;
111084
+ var ENTRY_SUBSYSTEMS, RATE_WINDOW_MS, RATE_LIMIT, clientRates, REST_MAX_ENTRIES, browserLogsRouter;
110435
111085
  var init_browser_logs = __esm({
110436
111086
  "src/web/routes/browser-logs.ts"() {
110437
111087
  "use strict";
110438
111088
  init_handler();
110439
111089
  init_logging();
110440
111090
  init_logger();
111091
+ ENTRY_SUBSYSTEMS = /* @__PURE__ */ new Set(["browser", "desktop"]);
110441
111092
  RATE_WINDOW_MS = 1e4;
110442
111093
  RATE_LIMIT = 500;
110443
111094
  clientRates = /* @__PURE__ */ new WeakMap();
@@ -118518,6 +119169,7 @@ var init_session_health_monitor = __esm({
118518
119169
  log.session.info("health monitor: drained orphan dead-pool", { orphanCount: orphanIds.length, written: written.length });
118519
119170
  }
118520
119171
  if (sessions3.length === 0) {
119172
+ if (!ctx.overBudget()) await this.checkSnapshotPull(sessions3, ctx, allSessions);
118521
119173
  const total = Date.now() - checkT0;
118522
119174
  if (total > 500) log.session.warn("health monitor: check() slow (no active sessions)", { totalMs: total, orphanMs: tOrphan - checkT0, recoverMs: tRecover - tOrphan });
118523
119175
  return;
@@ -118547,7 +119199,7 @@ var init_session_health_monitor = __esm({
118547
119199
  await this.reconcilePendingBackgroundTasks(sessions3);
118548
119200
  endPhase("pendingBgTasks");
118549
119201
  if (ctx.overBudget()) return;
118550
- await this.checkSnapshotPull(sessions3, ctx);
119202
+ await this.checkSnapshotPull(sessions3, ctx, allSessions);
118551
119203
  endPhase("snapshotPull");
118552
119204
  if (ctx.overBudget()) return;
118553
119205
  const reconciledIds = await this.reconcileStuckRunningSessions(sessions3, taskMap);
@@ -119041,15 +119693,39 @@ var init_session_health_monitor = __esm({
119041
119693
  bus.emit(EventNames.SESSION_WILL_REAP, payload, ["*"], { source: "health-monitor" });
119042
119694
  }
119043
119695
  /**
119044
- * C2 snapshot pull channel (contract §5): every 30s tick, for records in
119045
- * {running, idle} on a native (non-codex, non-embedded/sdk) engine whose
119046
- * host has a POOLED CONNECTED DaemonConnection advertising 'snapshot-v1',
119047
- * PULL getState and feed the snapshot to applySnapshot. NEVER dials a new
119048
- * connection. Sequential, capped at 10 sids/tick, per-sid 25s spacing.
119696
+ * C2 snapshot pull channel (contract §5): every 30s tick, PULL getState from
119697
+ * the host's POOLED CONNECTED DaemonConnection advertising 'snapshot-v1' and
119698
+ * feed the answer to applySnapshot. NEVER dials a new connection, always
119699
+ * sequential. TWO candidate classes with SEPARATE budgets and cadences:
119700
+ *
119701
+ * - LIVE — {running, idle}: 10 sids/tick, 25s per-sid spacing. The steady
119702
+ * state channel.
119703
+ * - RE-EXAMINATION — {error, stopped} whose cause is NOT positively terminal
119704
+ * (classifySessionError) and whose last_status_change is inside
119705
+ * RESCUABLE_STOPPED_WINDOW_MS: 5 sids/tick, 5 min per-sid spacing. Same
119706
+ * engine/provider/awaiting_spawn/archived exclusions as the live class.
119707
+ *
119708
+ * WHY RE-EXAMINATION EXISTS — do not "optimize" the error class back out. A
119709
+ * wrongly-'error' record was a two-sided deadlock: this lane is the ONE writer
119710
+ * the enforce-mode gate sanctions for a snapshot-covered session, and while it
119711
+ * looked at live records only it never re-derived status for an 'error' one.
119712
+ * recoverInfraFailedSessions DOES look, but the fix it writes
119713
+ * (('health-monitor','auto_recovered_dead')) is itself category-①, so the gate
119714
+ * drops the whole patch — the lane allowed to write refused to look, and the
119715
+ * lane that looked was not allowed to write. Records froze in 'error' with
119716
+ * "Connection lost — unable to reach remote host" for hours after their host
119717
+ * came back (2026-09-03, a remote dev host whose tunnel flapped: prose still on
119718
+ * the record 2h after the reconnect), burning a 30s log-churn loop the whole
119719
+ * time. Terminality is decided STRUCTURALLY, never by matching errorMessage
119720
+ * prose — see the header of session-error-kind.ts for why that ban exists.
119049
119721
  *
119050
- * TWO fairness/budget properties this loop must keep (C8/C9/C12/C29):
119722
+ * Re-examination candidates come from a WIDER pool than the live ones: the
119723
+ * tick's own working set drops every 'error' row (isTerminalSession), so the
119724
+ * caller passes the unfiltered health-scan set for this class only.
119051
119725
  *
119052
- * - It honors the TickContext budget INSIDE the loop (same discipline as
119726
+ * THREE fairness/budget properties both classes must keep (C8/C9/C12/C29):
119727
+ *
119728
+ * - The TickContext budget is honored INSIDE the loop (same discipline as
119053
119729
  * checkHungSessions). Each iteration is a real daemon RPC with a
119054
119730
  * probe-timeout ceiling, so 10 slow hosts could burn 10 × the probe
119055
119731
  * timeout sequentially BEFORE the authoritative reconcile phases that run
@@ -119058,70 +119734,101 @@ var init_session_health_monitor = __esm({
119058
119734
  * next tick (see the rotation below).
119059
119735
  *
119060
119736
  * - Candidates are ordered by lastPullAt ASCENDING (never-pulled first),
119061
- * which turns the 10/tick cap into a round-robin. In list order the SAME
119062
- * first ten sids were pulled every tick and — because the 25s spacing is
119063
- * shorter than the 30s cadence — sids 11+ were never pulled at all: the
119064
- * pull channel silently did not exist for them.
119737
+ * which turns each cap into a round-robin. In list order the SAME first ten
119738
+ * sids were pulled every tick and — because the 25s spacing is shorter than
119739
+ * the 30s cadence — sids 11+ were never pulled at all: the pull channel
119740
+ * silently did not exist for them. Each class rotates on its OWN timestamps.
119741
+ *
119742
+ * - The classes cannot spend each other's budget. Re-examination is
119743
+ * speculative ("is this record still true?") and a settled-death cohort
119744
+ * dwarfs the live set (the live DB measured 100 stopped candidates against
119745
+ * 4 error rows), so it must cost a handful of cheap RPCs per tick and
119746
+ * nothing more — the same reasoning as MAX_STOPPED_PROBES_PER_TICK below.
119065
119747
  */
119066
119748
  snapshotPullAt = /* @__PURE__ */ new Map();
119067
- async checkSnapshotPull(sessions3, ctx) {
119749
+ /** Separate from snapshotPullAt on purpose: the two classes rotate
119750
+ * independently, so a live pull can never reset the slow re-examination
119751
+ * cadence (or vice versa) when a record moves between them. */
119752
+ snapshotReexamPullAt = /* @__PURE__ */ new Map();
119753
+ async checkSnapshotPull(sessions3, ctx, reexamPool = sessions3) {
119068
119754
  const PULL_MIN_GAP_MS = 25e3;
119069
119755
  const MAX_PULLS_PER_TICK = 10;
119756
+ const REEXAM_MIN_GAP_MS = 5 * 6e4;
119757
+ const MAX_REEXAM_PULLS_PER_TICK = 5;
119070
119758
  try {
119071
119759
  const { getSnapshotStatusMode: getSnapshotStatusMode2 } = await Promise.resolve().then(() => (init_session_snapshot_gate(), session_snapshot_gate_exports));
119072
119760
  if (getSnapshotStatusMode2() === "off") return;
119073
119761
  const { getPooledSnapshotConnection: getPooledSnapshotConnection2 } = await Promise.resolve().then(() => (init_daemon_connection(), daemon_connection_exports));
119074
119762
  const { applySnapshot: applySnapshot2 } = await Promise.resolve().then(() => (init_session_snapshot_apply(), session_snapshot_apply_exports));
119075
119763
  const now = Date.now();
119076
- const candidates = sessions3.filter((s4) => {
119077
- if (s4.process_status !== "running" && s4.process_status !== "idle") return false;
119764
+ const pullable = (s4) => {
119765
+ if (s4.archived) return false;
119078
119766
  if (!engineCaps(s4.engine).snapshotPull) return false;
119079
119767
  if (s4.provider === "embedded" || s4.provider === "sdk") return false;
119080
- if (s4.status_reason === "awaiting_spawn") return false;
119081
- const lastPull = this.snapshotPullAt.get(s4.claudeSessionId) ?? 0;
119082
- return now - lastPull >= PULL_MIN_GAP_MS;
119083
- }).sort((a8, b8) => (this.snapshotPullAt.get(a8.claudeSessionId) ?? 0) - (this.snapshotPullAt.get(b8.claudeSessionId) ?? 0));
119084
- let pulled = 0;
119085
- let capped = false;
119086
- for (const s4 of candidates) {
119087
- if (ctx?.overBudget()) {
119088
- log.session.warn("health monitor: checkSnapshotPull abandoned mid-loop (over budget)", {
119089
- pulled,
119090
- candidateCount: candidates.length
119091
- });
119092
- break;
119093
- }
119094
- const conn = getPooledSnapshotConnection2(s4.host);
119095
- if (!conn) continue;
119096
- if (pulled >= MAX_PULLS_PER_TICK) {
119097
- capped = true;
119098
- break;
119099
- }
119100
- pulled++;
119101
- this.snapshotPullAt.set(s4.claudeSessionId, now);
119102
- try {
119103
- const resp = await probeWithTimeout(
119104
- conn.send("getState", { sid: s4.claudeSessionId }),
119105
- null,
119106
- "snapshot-pull-getState",
119107
- s4.claudeSessionId
119108
- );
119109
- const snapshot2 = resp?.ok ? resp.snapshot : void 0;
119110
- if (snapshot2) await applySnapshot2(s4.claudeSessionId, snapshot2, "pull-30s");
119111
- } catch (err2) {
119112
- log.session.debug("health monitor: snapshot pull failed", {
119113
- sessionId: s4.claudeSessionId,
119114
- host: s4.host ?? "__local__",
119115
- error: err2 instanceof Error ? err2.message : String(err2)
119116
- });
119768
+ return s4.status_reason !== "awaiting_spawn";
119769
+ };
119770
+ const byPullAge = (at) => (a8, b8) => (at.get(a8.claudeSessionId) ?? 0) - (at.get(b8.claudeSessionId) ?? 0);
119771
+ const live = sessions3.filter((s4) => {
119772
+ if (s4.process_status !== "running" && s4.process_status !== "idle") return false;
119773
+ if (!pullable(s4)) return false;
119774
+ return now - (this.snapshotPullAt.get(s4.claudeSessionId) ?? 0) >= PULL_MIN_GAP_MS;
119775
+ }).sort(byPullAge(this.snapshotPullAt));
119776
+ const reexam = reexamPool.filter((s4) => {
119777
+ if (s4.process_status !== "error" && s4.process_status !== "stopped") return false;
119778
+ if (!pullable(s4)) return false;
119779
+ if (classifySessionError(s4) === "terminal") return false;
119780
+ const changedAt = s4.last_status_change ? Date.parse(s4.last_status_change) : NaN;
119781
+ if (!Number.isFinite(changedAt)) return false;
119782
+ if (now - changedAt > RESCUABLE_STOPPED_WINDOW_MS) return false;
119783
+ return now - (this.snapshotReexamPullAt.get(s4.claudeSessionId) ?? 0) >= REEXAM_MIN_GAP_MS;
119784
+ }).sort(byPullAge(this.snapshotReexamPullAt));
119785
+ let abandoned = false;
119786
+ const runClass = async (candidates, cap2, at, candidateClass) => {
119787
+ let pulled = 0;
119788
+ for (const s4 of candidates) {
119789
+ if (ctx?.overBudget()) {
119790
+ abandoned = true;
119791
+ log.session.warn("health monitor: checkSnapshotPull abandoned mid-loop (over budget)", {
119792
+ pulled,
119793
+ candidateCount: candidates.length,
119794
+ candidateClass
119795
+ });
119796
+ break;
119797
+ }
119798
+ const conn = getPooledSnapshotConnection2(s4.host);
119799
+ if (!conn) continue;
119800
+ if (pulled >= cap2) {
119801
+ log.session.info("health monitor: snapshot pull capped this tick", {
119802
+ cap: cap2,
119803
+ sessionCount: sessions3.length,
119804
+ candidateCount: candidates.length,
119805
+ candidateClass
119806
+ });
119807
+ break;
119808
+ }
119809
+ pulled++;
119810
+ at.set(s4.claudeSessionId, now);
119811
+ try {
119812
+ const resp = await probeWithTimeout(
119813
+ conn.send("getState", { sid: s4.claudeSessionId }),
119814
+ null,
119815
+ "snapshot-pull-getState",
119816
+ s4.claudeSessionId
119817
+ );
119818
+ const snapshot2 = resp?.ok ? resp.snapshot : void 0;
119819
+ if (snapshot2) await applySnapshot2(s4.claudeSessionId, snapshot2, "pull-30s");
119820
+ } catch (err2) {
119821
+ log.session.debug("health monitor: snapshot pull failed", {
119822
+ sessionId: s4.claudeSessionId,
119823
+ host: s4.host ?? "__local__",
119824
+ error: err2 instanceof Error ? err2.message : String(err2)
119825
+ });
119826
+ }
119117
119827
  }
119118
- }
119119
- if (capped) {
119120
- log.session.info("health monitor: snapshot pull capped this tick", {
119121
- cap: MAX_PULLS_PER_TICK,
119122
- sessionCount: sessions3.length,
119123
- candidateCount: candidates.length
119124
- });
119828
+ };
119829
+ await runClass(live, MAX_PULLS_PER_TICK, this.snapshotPullAt, "live");
119830
+ if (!abandoned) {
119831
+ await runClass(reexam, MAX_REEXAM_PULLS_PER_TICK, this.snapshotReexamPullAt, "reexam");
119125
119832
  }
119126
119833
  if (this.snapshotPullAt.size > 200) {
119127
119834
  const liveIds = new Set(sessions3.map((s4) => s4.claudeSessionId));
@@ -119129,6 +119836,12 @@ var init_session_health_monitor = __esm({
119129
119836
  if (!liveIds.has(sid)) this.snapshotPullAt.delete(sid);
119130
119837
  }
119131
119838
  }
119839
+ if (this.snapshotReexamPullAt.size > 200) {
119840
+ const poolIds = new Set(reexamPool.map((s4) => s4.claudeSessionId));
119841
+ for (const sid of this.snapshotReexamPullAt.keys()) {
119842
+ if (!poolIds.has(sid)) this.snapshotReexamPullAt.delete(sid);
119843
+ }
119844
+ }
119132
119845
  } catch (err2) {
119133
119846
  log.session.warn("health monitor: checkSnapshotPull failed", {
119134
119847
  error: err2 instanceof Error ? err2.message : String(err2)
@@ -119291,6 +120004,31 @@ var init_session_health_monitor = __esm({
119291
120004
  });
119292
120005
  }
119293
120006
  }
120007
+ /**
120008
+ * Can a scheduled auto-recovery actually FIRE for this record?
120009
+ *
120010
+ * fire() (session-auto-recover) hard-requires the linked task to still be
120011
+ * IN_PROGRESS — AGENT_COMPLETE means the work was already handed back and
120012
+ * resuming would talk over the human. Arming a recovery that cannot pass that
120013
+ * check is not free: the 30s tick re-armed it forever and each arming logged
120014
+ * an abort 20s later (measured running for 2+ hours on one session), while
120015
+ * `if (armed) continue` also skipped the phase sync that IS appropriate there.
120016
+ *
120017
+ * A lookup that cannot answer (no task id, deleted task, DB error) returns
120018
+ * true: only a KNOWN non-IN_PROGRESS phase blocks arming, so a transient read
120019
+ * failure can never disable recovery. schedule()'s own guards still apply.
120020
+ */
120021
+ async autoRecoverCanFire(s4) {
120022
+ if (!s4.taskId) return true;
120023
+ try {
120024
+ const { getTask: getTask2 } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
120025
+ const task = await getTask2(s4.taskId);
120026
+ if (!task) return true;
120027
+ return task.phase === "IN_PROGRESS";
120028
+ } catch {
120029
+ return true;
120030
+ }
120031
+ }
119294
120032
  /**
119295
120033
  * Auto-recover remote sessions stuck in 'error' with "Connection lost" message.
119296
120034
  *
@@ -119377,28 +120115,31 @@ var init_session_health_monitor = __esm({
119377
120115
  } else if (wasStopped) {
119378
120116
  continue;
119379
120117
  } else {
119380
- const updated = await updateSessionRecord2(s4.claudeSessionId, {
119381
- process_status: "stopped",
119382
- errorMessage: void 0,
119383
- errorKind: "infra",
119384
- activity: void 0,
119385
- last_status_change: now,
119386
- status_reason: "auto_recovered_dead",
119387
- status_changed_by: "health-monitor"
119388
- });
119389
- emitSessionStatusChanged(
119390
- updated,
119391
- {},
119392
- ["*"],
119393
- { source: "health-monitor", urgency: "urgent" }
119394
- );
119395
- log.session.info("health monitor: auto-recovered connection-lost session (process dead)", {
119396
- sessionId: s4.claudeSessionId,
119397
- host: s4.host,
119398
- alive: false
119399
- });
120118
+ const { isSnapshotCovered: isSnapshotCovered2 } = await Promise.resolve().then(() => (init_session_snapshot_gate(), session_snapshot_gate_exports));
120119
+ if (!isSnapshotCovered2(s4.claudeSessionId)) {
120120
+ const updated = await updateSessionRecord2(s4.claudeSessionId, {
120121
+ process_status: "stopped",
120122
+ errorMessage: void 0,
120123
+ errorKind: "infra",
120124
+ activity: void 0,
120125
+ last_status_change: now,
120126
+ status_reason: "auto_recovered_dead",
120127
+ status_changed_by: "health-monitor"
120128
+ });
120129
+ emitSessionStatusChanged(
120130
+ updated,
120131
+ {},
120132
+ ["*"],
120133
+ { source: "health-monitor", urgency: "urgent" }
120134
+ );
120135
+ log.session.info("health monitor: auto-recovered connection-lost session (process dead)", {
120136
+ sessionId: s4.claudeSessionId,
120137
+ host: s4.host,
120138
+ alive: false
120139
+ });
120140
+ }
119400
120141
  const { scheduleSessionAutoRecover: scheduleSessionAutoRecover2 } = await Promise.resolve().then(() => (init_session_auto_recover(), session_auto_recover_exports));
119401
- const armed = scheduleSessionAutoRecover2(s4, s4.status_reason ?? "daemon_reported_exit");
120142
+ const armed = await this.autoRecoverCanFire(s4) && scheduleSessionAutoRecover2(s4, s4.status_reason ?? "daemon_reported_exit");
119402
120143
  if (armed) continue;
119403
120144
  if (s4.taskId) {
119404
120145
  try {
@@ -132723,6 +133464,20 @@ var init_session_lifecycle_v1 = __esm({
132723
133464
  next(err2);
132724
133465
  }
132725
133466
  });
133467
+ sessionLifecycleV1Router.post("/sessions/:id/recheck", async (req, res, next) => {
133468
+ try {
133469
+ const sessionId = validSid2(req, res);
133470
+ if (!sessionId) return;
133471
+ if (CLOUD_MODE) {
133472
+ await relayControlAction(res, "recheck", sessionId, void 0, 200);
133473
+ return;
133474
+ }
133475
+ const { recheckSession: recheckSession2 } = await Promise.resolve().then(() => (init_session_lifecycle(), session_lifecycle_exports));
133476
+ await runLocal(res, next, 200, () => recheckSession2(sessionId));
133477
+ } catch (err2) {
133478
+ next(err2);
133479
+ }
133480
+ });
132726
133481
  sessionLifecycleV1Router.post("/sessions/:id/permission", async (req, res, next) => {
132727
133482
  try {
132728
133483
  const sessionId = validSid2(req, res);
@@ -132935,7 +133690,7 @@ __export(session_requests_exports, {
132935
133690
  settleNotified: () => settleNotified,
132936
133691
  settleReplied: () => settleReplied
132937
133692
  });
132938
- import { createHash as createHash18, randomUUID as randomUUID8 } from "crypto";
133693
+ import { createHash as createHash18, randomUUID as randomUUID9 } from "crypto";
132939
133694
  import path137 from "path";
132940
133695
  function oneLine(text, max = 160) {
132941
133696
  const flat = text.replace(/\s+/g, " ").trim();
@@ -132959,7 +133714,7 @@ function clampReplyTimeoutSecs(secs) {
132959
133714
  }
132960
133715
  async function createSessionRequest(input) {
132961
133716
  const request3 = {
132962
- id: `rq-${randomUUID8().replace(/-/g, "").slice(0, 12)}`,
133717
+ id: `rq-${randomUUID9().replace(/-/g, "").slice(0, 12)}`,
132963
133718
  fromSessionId: input.fromSessionId,
132964
133719
  ...input.toSessionId ? { toSessionId: input.toSessionId } : {},
132965
133720
  ...input.toTaskId ? { toTaskId: input.toTaskId } : {},
@@ -133392,7 +134147,7 @@ __export(task_start_exports, {
133392
134147
  SessionExistsError: () => SessionExistsError,
133393
134148
  startSessionForTask: () => startSessionForTask
133394
134149
  });
133395
- import { randomUUID as randomUUID9 } from "crypto";
134150
+ import { randomUUID as randomUUID10 } from "crypto";
133396
134151
  async function startSessionForTask(params) {
133397
134152
  const { taskIdPrefix, source } = params;
133398
134153
  let task;
@@ -133443,7 +134198,7 @@ async function startSessionForTask(params) {
133443
134198
  }
133444
134199
  let message = params.message?.trim() || `Working on task: ${task.title}`;
133445
134200
  const engine = normalizeEngine(params.engine);
133446
- const preassignedSessionId = engineCaps(engine).idProvisioning === "provider-issued" ? void 0 : randomUUID9();
134201
+ const preassignedSessionId = engineCaps(engine).idProvisioning === "provider-issued" ? void 0 : randomUUID10();
133447
134202
  let requestId;
133448
134203
  if (params.expectReply) {
133449
134204
  const { resolveCaller: resolveCaller2 } = await Promise.resolve().then(() => (init_session_send_core(), session_send_core_exports));
@@ -148941,7 +149696,7 @@ async function generateLedgerDesc(task, timeoutMs = 15e3) {
148941
149696
  try {
148942
149697
  const { getConfig: getConfig2 } = await Promise.resolve().then(() => (init_config_manager(), config_manager_exports));
148943
149698
  const config = await getConfig2();
148944
- const providerName = config.agent?.main_provider ?? "bedrock";
149699
+ const providerName = resolveMainProviderName(config);
148945
149700
  const model = cheapModelFor(providerName);
148946
149701
  const detail = (task.description ?? "").trim().slice(0, 800);
148947
149702
  const project = (task.project ?? "").trim();
@@ -149000,6 +149755,7 @@ var init_task_ledger_desc = __esm({
149000
149755
  "use strict";
149001
149756
  init_model();
149002
149757
  init_model_catalog();
149758
+ init_default_provider();
149003
149759
  init_logging();
149004
149760
  init_task_manager();
149005
149761
  MAX_DESC_LEN = 120;
@@ -149023,6 +149779,10 @@ function diffStreamedVsPersisted(streamedTextMsgIds, persistedMsgIds) {
149023
149779
  }
149024
149780
  function armStreamConvergenceCheck(sessionId, streamedTextMsgIds) {
149025
149781
  if (streamedTextMsgIds.length === 0) return;
149782
+ if (consumeWarmupTurn(sessionId)) {
149783
+ log.obs.debug("stream-convergence: skipped hidden warm-up turn", { sessionId });
149784
+ return;
149785
+ }
149026
149786
  const timer3 = setTimeout(() => {
149027
149787
  void runCheck(sessionId, streamedTextMsgIds).catch((err2) => {
149028
149788
  log.obs.warn("stream-convergence check failed", {
@@ -149088,6 +149848,7 @@ var init_stream_convergence = __esm({
149088
149848
  "src/core/observability/stream-convergence.ts"() {
149089
149849
  "use strict";
149090
149850
  init_logging();
149851
+ init_side_thread_warmup();
149091
149852
  CHECK_DELAY_MS = 15e3;
149092
149853
  INCIDENT_DEDUPE_MS = 30 * 6e4;
149093
149854
  lastIncidentAt = /* @__PURE__ */ new Map();
@@ -149338,12 +150099,17 @@ function getSystemHealth() {
149338
150099
  }
149339
150100
  async function refreshSystemHealth() {
149340
150101
  systemHealth.claudeCliAvailable = checkClaudeCliAvailable();
149341
- const cred = await resolveCredentialHealth();
150102
+ applyCredentialHealth(await resolveCredentialHealth());
150103
+ broadcastEvent("system:health", systemHealth);
150104
+ return systemHealth;
150105
+ }
150106
+ function applyCredentialHealth(cred) {
149342
150107
  systemHealth.hasReadyProvider = cred.hasReadyProvider;
149343
150108
  systemHealth.credentialSource = cred.source;
149344
150109
  systemHealth.credentialDetail = cred.detail;
149345
- broadcastEvent("system:health", systemHealth);
149346
- return systemHealth;
150110
+ systemHealth.mainProvider = cred.mainProvider;
150111
+ systemHealth.mainProviderImplicit = cred.mainProviderImplicit;
150112
+ systemHealth.claudeCliAuth = cred.claudeCliAuth;
149347
150113
  }
149348
150114
  function checkClaudeCliAvailable() {
149349
150115
  return isClaudeCliInstalled();
@@ -149353,31 +150119,50 @@ async function resolveCredentialHealth() {
149353
150119
  const { getConfig: getConfig2 } = await Promise.resolve().then(() => (init_config_manager(), config_manager_exports));
149354
150120
  const { resolveCredentials: resolveCredentials2 } = await Promise.resolve().then(() => (init_credential_resolver(), credential_resolver_exports));
149355
150121
  const { buildProviderMap: buildProviderMap2 } = await Promise.resolve().then(() => (init_registry2(), registry_exports));
150122
+ const { resolveMainProviderName: resolveMainProviderName2, mainProviderIsImplicit: mainProviderIsImplicit2 } = await Promise.resolve().then(() => (init_default_provider(), default_provider_exports));
150123
+ const { detectClaudeCli: detectClaudeCli2 } = await Promise.resolve().then(() => (init_claude_cli_detect(), claude_cli_detect_exports));
149356
150124
  const config = await getConfig2();
150125
+ const cli = detectClaudeCli2();
150126
+ const mainProvider = resolveMainProviderName2(config, cli.installed);
150127
+ const base = {
150128
+ mainProvider,
150129
+ mainProviderImplicit: mainProviderIsImplicit2(config),
150130
+ claudeCliAuth: cli.installed ? cli.auth.label : void 0
150131
+ };
150132
+ if (mainProvider === "claude_cli") {
150133
+ return cli.installed ? { ...base, hasReadyProvider: true, source: "config", detail: `claude-cli (${cli.auth.label})` } : { ...base, hasReadyProvider: false, source: "none" };
150134
+ }
149357
150135
  const cred = resolveCredentials2(config);
149358
150136
  if (cred.source !== "none") {
149359
- return { hasReadyProvider: true, source: cred.source, detail: cred.detail };
150137
+ return { ...base, hasReadyProvider: true, source: cred.source, detail: cred.detail };
149360
150138
  }
149361
150139
  const providers = buildProviderMap2(config.providers, config);
149362
150140
  for (const [, prov] of Object.entries(providers)) {
149363
150141
  if (prov.api === "bedrock" || prov.api === "ollama") continue;
149364
150142
  if (prov.api === "claude-cli") {
149365
- const { detectClaudeCli: detectClaudeCli2 } = await Promise.resolve().then(() => (init_claude_cli_detect(), claude_cli_detect_exports));
149366
- if (detectClaudeCli2().subscriptionReady) {
149367
- return { hasReadyProvider: true, source: "config", detail: "claude-cli (subscription)" };
149368
- }
150143
+ if (cli.installed) return { ...base, hasReadyProvider: true, source: "config", detail: `claude-cli (${cli.auth.label})` };
149369
150144
  continue;
149370
150145
  }
149371
150146
  const implemented = prov.api === "anthropic-messages" || prov.api === "openai-chat" || prov.api === "google-generative-ai";
149372
150147
  if (implemented && (prov.api_key || prov.bearer_token)) {
149373
- return { hasReadyProvider: true, source: "config", detail: prov.api };
150148
+ return { ...base, hasReadyProvider: true, source: "config", detail: prov.api };
149374
150149
  }
149375
150150
  }
149376
- return { hasReadyProvider: false, source: "none" };
150151
+ return { ...base, hasReadyProvider: false, source: "none" };
149377
150152
  } catch {
149378
150153
  return { hasReadyProvider: false, source: "none" };
149379
150154
  }
149380
150155
  }
150156
+ async function isWalnutAnsweringOn(port) {
150157
+ try {
150158
+ const res = await fetch(`http://127.0.0.1:${port}/api/system/health`, { signal: AbortSignal.timeout(1500) });
150159
+ if (!res.ok) return false;
150160
+ const body3 = await res.json();
150161
+ return !!body3 && typeof body3 === "object" && "hasReadyProvider" in body3;
150162
+ } catch {
150163
+ return false;
150164
+ }
150165
+ }
149381
150166
  async function startServer(options = {}) {
149382
150167
  if (httpServer) throw new Error("Server already running. Call stopServer() first.");
149383
150168
  bootCompleted = false;
@@ -149402,11 +150187,11 @@ async function startServer(options = {}) {
149402
150187
  }
149403
150188
  {
149404
150189
  const cred = await resolveCredentialHealth();
149405
- systemHealth.hasReadyProvider = cred.hasReadyProvider;
149406
- systemHealth.credentialSource = cred.source;
149407
- systemHealth.credentialDetail = cred.detail;
150190
+ applyCredentialHealth(cred);
149408
150191
  if (!systemHealth.hasReadyProvider) {
149409
150192
  log.web.warn("No AI provider configured \u2014 configure one in Settings");
150193
+ } else if (cred.mainProvider === "claude_cli") {
150194
+ log.web.info("AI provider: Claude Code", { auth: cred.claudeCliAuth, implicit: cred.mainProviderImplicit });
149410
150195
  } else if (cred.source && cred.source !== "config") {
149411
150196
  log.web.info("Auto-detected Bedrock credential", { source: cred.source, detail: cred.detail });
149412
150197
  }
@@ -149904,6 +150689,7 @@ async function startServer(options = {}) {
149904
150689
  });
149905
150690
  if (!dev) {
149906
150691
  const staticDir = (() => {
150692
+ if (process.env.WALNUT_WEB_STATIC_DIR) return process.env.WALNUT_WEB_STATIC_DIR;
149907
150693
  let dir2 = path157.dirname(fileURLToPath13(import.meta.url));
149908
150694
  for (let i8 = 0; i8 < 5; i8++) {
149909
150695
  const candidate = path157.join(dir2, "web", "static", "index.html");
@@ -149920,11 +150706,65 @@ async function startServer(options = {}) {
149920
150706
  }
149921
150707
  return path157.join(process.cwd(), "dist", "web", "static");
149922
150708
  })();
150709
+ const mirrorDir = process.env.WALNUT_WEB_STATIC_MIRROR || path157.join(WALNUT_HOME, "cache", "web-static");
150710
+ let mirrorReady = false;
150711
+ try {
150712
+ const want = fs98.readFileSync(path157.join(staticDir, "index.html"), "utf-8");
150713
+ let have = null;
150714
+ try {
150715
+ have = fs98.readFileSync(path157.join(mirrorDir, "index.html"), "utf-8");
150716
+ } catch {
150717
+ }
150718
+ if (have !== want) {
150719
+ fs98.rmSync(mirrorDir, { recursive: true, force: true });
150720
+ fs98.mkdirSync(path157.dirname(mirrorDir), { recursive: true });
150721
+ fs98.cpSync(staticDir, mirrorDir, { recursive: true });
150722
+ }
150723
+ } catch (err2) {
150724
+ log.web.warn("could not refresh the web-asset mirror", {
150725
+ mirrorDir,
150726
+ error: err2 instanceof Error ? err2.message : String(err2)
150727
+ });
150728
+ }
150729
+ try {
150730
+ mirrorReady = fs98.statSync(path157.join(mirrorDir, "index.html")).isFile();
150731
+ } catch {
150732
+ mirrorReady = false;
150733
+ }
149923
150734
  app.use(express3.static(staticDir));
150735
+ if (mirrorReady) app.use(express3.static(mirrorDir));
150736
+ const checkStaticRoot = () => {
150737
+ try {
150738
+ return fs98.statSync(path157.join(staticDir, "index.html")).isFile();
150739
+ } catch {
150740
+ return false;
150741
+ }
150742
+ };
150743
+ let staticRootOk = checkStaticRoot();
150744
+ if (!staticRootOk) {
150745
+ log.web.error("web assets are NOT servable at startup", { staticDir, mirrorReady });
150746
+ }
150747
+ const staticRootTimer = setInterval(() => {
150748
+ const ok2 = checkStaticRoot();
150749
+ if (ok2 === staticRootOk) return;
150750
+ staticRootOk = ok2;
150751
+ if (ok2) log.web.info("web assets are servable again", { staticDir });
150752
+ else log.web.error("web assets VANISHED from under the running server", { staticDir, mirrorReady });
150753
+ }, 6e4);
150754
+ staticRootTimer.unref();
150755
+ setStaticRootReporter(() => ({ staticDir, ok: staticRootOk, mirrorDir, mirrorReady }));
149924
150756
  app.use((req, res, next) => {
149925
150757
  if (req.method !== "GET" || req.path.startsWith("/api/")) return next();
149926
150758
  if (isStaticAssetPath(req.path)) return next();
149927
- res.sendFile("index.html", { root: staticDir });
150759
+ res.sendFile("index.html", { root: staticDir }, (err2) => {
150760
+ if (!err2) return;
150761
+ if (mirrorReady) {
150762
+ res.sendFile("index.html", { root: mirrorDir }, () => {
150763
+ });
150764
+ return;
150765
+ }
150766
+ next(err2);
150767
+ });
149928
150768
  });
149929
150769
  }
149930
150770
  app.use("/api", notFoundHandler);
@@ -149944,9 +150784,21 @@ async function startServer(options = {}) {
149944
150784
  }
149945
150785
  httpServer = createServer(app);
149946
150786
  attachWss(httpServer);
149947
- await new Promise((resolve, reject) => {
149948
- httpServer.listen(port, () => resolve());
149949
- httpServer.once("error", reject);
150787
+ try {
150788
+ await new Promise((resolve, reject) => {
150789
+ httpServer.once("error", reject);
150790
+ httpServer.listen(port, () => resolve());
150791
+ });
150792
+ } catch (err2) {
150793
+ if (err2?.code !== "EADDRINUSE") throw err2;
150794
+ const already = await isWalnutAnsweringOn(port);
150795
+ throw new Error(
150796
+ 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:
150797
+ open-walnut web --port ${port + 1}`
150798
+ );
150799
+ }
150800
+ httpServer.on("error", (err2) => {
150801
+ log.web.error("http server error after listen", { error: err2 instanceof Error ? err2.message : String(err2) });
149950
150802
  });
149951
150803
  const label = dev ? "dev" : "production";
149952
150804
  log.web.info(`server listening on http://localhost:${port}`, { mode: label, port });
@@ -149956,6 +150808,7 @@ async function startServer(options = {}) {
149956
150808
  const { updateInstanceLockPort: updateInstanceLockPort2 } = await Promise.resolve().then(() => (init_instance_lock(), instance_lock_exports));
149957
150809
  updateInstanceLockPort2(bound.port);
149958
150810
  setPluginApiBase(`http://127.0.0.1:${bound.port}`);
150811
+ process.env.WALNUT_SERVER_URL = `http://localhost:${bound.port}`;
149959
150812
  }
149960
150813
  }
149961
150814
  if (CLOUD_MODE) {
@@ -150618,10 +151471,9 @@ async function startServer(options = {}) {
150618
151471
  const prev = systemHealth.hasReadyProvider;
150619
151472
  const prevSource = systemHealth.credentialSource;
150620
151473
  const cred = await resolveCredentialHealth();
150621
- systemHealth.hasReadyProvider = cred.hasReadyProvider;
150622
- systemHealth.credentialSource = cred.source;
150623
- systemHealth.credentialDetail = cred.detail;
150624
- if (cred.hasReadyProvider !== prev || cred.source !== prevSource) {
151474
+ const prevProvider = systemHealth.mainProvider;
151475
+ applyCredentialHealth(cred);
151476
+ if (cred.hasReadyProvider !== prev || cred.source !== prevSource || cred.mainProvider !== prevProvider) {
150625
151477
  broadcastEvent("system:health", systemHealth);
150626
151478
  }
150627
151479
  } catch {
@@ -158048,11 +158900,12 @@ __export(session_controls_exports, {
158048
158900
  applySessionModelChange: () => applySessionModelChange,
158049
158901
  computeModelOptions: () => computeModelOptions,
158050
158902
  controlErrorKind: () => controlErrorKind,
158903
+ createForkSiblingTask: () => createForkSiblingTask,
158051
158904
  forkSessionToTask: () => forkSessionToTask,
158052
158905
  handleSessionControlRelay: () => handleSessionControlRelay,
158053
158906
  modelReadBackMatches: () => modelReadBackMatches
158054
158907
  });
158055
- import { randomUUID as randomUUID10 } from "crypto";
158908
+ import { randomUUID as randomUUID11 } from "crypto";
158056
158909
  function controlErrorKind(status2) {
158057
158910
  if (status2 === 404) return "not_found";
158058
158911
  if (status2 === 405) return "method_not_allowed";
@@ -158225,6 +159078,124 @@ async function computeModelOptions(sessionId) {
158225
159078
  currentEffort: liveEffort ?? record.effectiveEffort ?? record.effort ?? null
158226
159079
  };
158227
159080
  }
159081
+ async function createForkSiblingTask(sourceTaskId, opts) {
159082
+ const {
159083
+ getTask: getTask2,
159084
+ addTask: addTask2,
159085
+ updateTask: updateTask2,
159086
+ groupTasks: groupTasks2,
159087
+ addToGroup: addToGroup2,
159088
+ renameGroup: renameGroup2
159089
+ } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
159090
+ let sourceTask;
159091
+ try {
159092
+ sourceTask = await getTask2(sourceTaskId);
159093
+ } catch {
159094
+ throw new SessionControlError(`Source task "${sourceTaskId}" not found`, 404);
159095
+ }
159096
+ const sourceBaseTitle = sourceTask.title.replace(/^Fork of\s+/i, "").replace(/\s+-\s+fork of\s+.*$/i, "").trim() || sourceTask.title;
159097
+ const explicitTitle = opts.childTitle || (opts.titlePrefix ? `${opts.titlePrefix} - fork of ${sourceBaseTitle}` : void 0);
159098
+ const autoTitle = !explicitTitle;
159099
+ const newTitle = explicitTitle || `Fork of ${sourceBaseTitle}`;
159100
+ let inheritedTier;
159101
+ if (sourceTask.pinned && sourceTask.focus_tier) {
159102
+ const { getCustomTiers: getCustomTiers2 } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
159103
+ const known = ["focus", "backlog", "wait"].includes(sourceTask.focus_tier) || (await getCustomTiers2()).some((t3) => t3.id === sourceTask.focus_tier);
159104
+ if (known) inheritedTier = sourceTask.focus_tier;
159105
+ else {
159106
+ log.session.warn("fork: source tier is not registered, filing the fork in Satellite", {
159107
+ sourceTaskId: sourceTask.id,
159108
+ tier: sourceTask.focus_tier
159109
+ });
159110
+ }
159111
+ }
159112
+ const { task: newFork } = await addTask2({
159113
+ title: newTitle,
159114
+ project: sourceTask.project || "",
159115
+ source: sourceTask.source,
159116
+ ...opts.description ? { description: opts.description } : {},
159117
+ // A fork is a person splitting off live work, so it joins the board like
159118
+ // any other hand-made task (Satellite = pinned, no stored tier).
159119
+ pinned: true,
159120
+ ...inheritedTier ? { focus_tier: inheritedTier } : {},
159121
+ // Forking an Ask Walnut conversation yields another Ask Walnut
159122
+ // conversation: without the flag the fork loses the amber marker AND the
159123
+ // persona drift repair (both key on task.walnut_agent, never on project).
159124
+ ...sourceTask.walnut_agent ? { walnut_agent: true } : {}
159125
+ });
159126
+ let forkGroupId;
159127
+ try {
159128
+ if (sourceTask.group_id) {
159129
+ const r8 = await addToGroup2(sourceTask.group_id, [newFork.id]);
159130
+ forkGroupId = r8.group_id;
159131
+ } else {
159132
+ const r8 = await groupTasks2([sourceTask.id, newFork.id], sourceTask.title);
159133
+ forkGroupId = r8.group_id;
159134
+ }
159135
+ } catch (err2) {
159136
+ log.session.warn("fork: failed to group source + fork", {
159137
+ sourceTaskId: sourceTask.id,
159138
+ forkTaskId: newFork.id,
159139
+ error: err2 instanceof Error ? err2.message : String(err2)
159140
+ });
159141
+ }
159142
+ let task;
159143
+ try {
159144
+ task = await getTask2(newFork.id);
159145
+ } catch {
159146
+ task = newFork;
159147
+ }
159148
+ bus.emit(EventNames.TASK_CREATED, { task }, ["web-ui", "main-agent"], { source: opts.source });
159149
+ if (autoTitle && opts.prompt?.trim()) {
159150
+ const forkId = newFork.id;
159151
+ const sourceTitle = sourceBaseTitle;
159152
+ const placeholderTitle = newTitle;
159153
+ const prompt = opts.prompt;
159154
+ const titleSource = `${opts.source}-title`;
159155
+ void (async () => {
159156
+ try {
159157
+ const { summarizeForkPrompt: summarizeForkPrompt2 } = await Promise.resolve().then(() => (init_fork_title(), fork_title_exports));
159158
+ const label = await summarizeForkPrompt2(prompt);
159159
+ if (!label) return;
159160
+ const current5 = await getTask2(forkId);
159161
+ if (current5.title !== placeholderTitle) {
159162
+ log.session.info("fork title refine skipped \u2014 title changed since fork", { taskId: forkId });
159163
+ return;
159164
+ }
159165
+ const refinedTitle = `${label} - fork of ${sourceTitle}`;
159166
+ const { task: updated } = await updateTask2(forkId, { title: refinedTitle }, { source: titleSource });
159167
+ bus.emit(EventNames.TASK_UPDATED, { task: updated }, ["web-ui", "main-agent"], { source: titleSource });
159168
+ log.session.info("fork title refined", { taskId: forkId, title: refinedTitle });
159169
+ } catch (err2) {
159170
+ log.session.warn("fork title refine failed", {
159171
+ taskId: forkId,
159172
+ error: err2 instanceof Error ? err2.message : String(err2)
159173
+ });
159174
+ }
159175
+ })();
159176
+ }
159177
+ if (forkGroupId && !sourceTask.group_id) {
159178
+ const gid = forkGroupId;
159179
+ const seedTitles = [sourceTask.title, newTitle];
159180
+ const groupSource = opts.source;
159181
+ void (async () => {
159182
+ try {
159183
+ const { summarizeGroupLabel: summarizeGroupLabel2 } = await Promise.resolve().then(() => (init_fork_title(), fork_title_exports));
159184
+ const label = await summarizeGroupLabel2(seedTitles);
159185
+ if (!label) return;
159186
+ await renameGroup2(gid, label);
159187
+ bus.emit(EventNames.TASK_GROUPS_CHANGED, { group_id: gid, label }, ["web-ui", "main-agent"], { source: groupSource });
159188
+ log.session.info("fork group label refined", { groupId: gid, label });
159189
+ } catch (err2) {
159190
+ log.session.warn("fork group label refine failed", {
159191
+ groupId: gid,
159192
+ error: err2 instanceof Error ? err2.message : String(err2)
159193
+ });
159194
+ }
159195
+ })();
159196
+ }
159197
+ return { task, ...forkGroupId ? { groupId: forkGroupId } : {} };
159198
+ }
158228
159199
  async function forkSessionToTask(sourceSessionId, input, source = "web-api") {
158229
159200
  const { task_id, create_child_task, child_title, message, title, model, imageContext } = input;
158230
159201
  if (!task_id && !create_child_task) {
@@ -158234,14 +159205,7 @@ async function forkSessionToTask(sourceSessionId, input, source = "web-api") {
158234
159205
  throw new SessionControlError("task_id and create_child_task are mutually exclusive", 400);
158235
159206
  }
158236
159207
  const { getSessionByClaudeId: getSessionByClaudeId2, getSessionsForTask: getSessionsForTask2, createSessionRecord: createSessionRecord2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
158237
- const {
158238
- getTask: getTask2,
158239
- addTask: addTask2,
158240
- updateTask: updateTask2,
158241
- groupTasks: groupTasks2,
158242
- addToGroup: addToGroup2,
158243
- renameGroup: renameGroup2
158244
- } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
159208
+ const { getTask: getTask2 } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
158245
159209
  const sourceRecord = await getSessionByClaudeId2(sourceSessionId);
158246
159210
  if (!sourceRecord) throw new SessionControlError("Source session not found", 404);
158247
159211
  if (!engineCaps(sourceRecord.engine).fork) {
@@ -158258,108 +159222,13 @@ async function forkSessionToTask(sourceSessionId, input, source = "web-api") {
158258
159222
  if (!sourceRecord.taskId) {
158259
159223
  throw new SessionControlError("Source session has no task \u2014 cannot create fork task", 400);
158260
159224
  }
158261
- let sourceTask;
158262
- try {
158263
- sourceTask = await getTask2(sourceRecord.taskId);
158264
- } catch {
158265
- throw new SessionControlError(`Source task "${sourceRecord.taskId}" not found`, 404);
158266
- }
158267
- const sourceBaseTitle = sourceTask.title.replace(/^Fork of\s+/i, "").replace(/\s+-\s+fork of\s+.*$/i, "").trim() || sourceTask.title;
158268
- const autoTitle = !child_title;
158269
- const newTitle = child_title || `Fork of ${sourceBaseTitle}`;
158270
- let inheritedTier;
158271
- if (sourceTask.pinned && sourceTask.focus_tier) {
158272
- const { getCustomTiers: getCustomTiers2 } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
158273
- const known = ["focus", "backlog", "wait"].includes(sourceTask.focus_tier) || (await getCustomTiers2()).some((t3) => t3.id === sourceTask.focus_tier);
158274
- if (known) inheritedTier = sourceTask.focus_tier;
158275
- else {
158276
- log.session.warn("fork: source tier is not registered, filing the fork in Satellite", {
158277
- sourceTaskId: sourceTask.id,
158278
- tier: sourceTask.focus_tier
158279
- });
158280
- }
158281
- }
158282
- const { task: newFork } = await addTask2({
158283
- title: newTitle,
158284
- project: sourceTask.project || "",
158285
- source: sourceTask.source,
158286
- // A fork is a person splitting off live work, so it joins the board like
158287
- // any other hand-made task (Satellite = pinned, no stored tier).
158288
- pinned: true,
158289
- ...inheritedTier ? { focus_tier: inheritedTier } : {},
158290
- // Forking an Ask Walnut conversation yields another Ask Walnut
158291
- // conversation: without the flag the fork loses the amber marker AND the
158292
- // persona drift repair (both key on task.walnut_agent, never on project).
158293
- ...sourceTask.walnut_agent ? { walnut_agent: true } : {}
158294
- });
158295
- let forkGroupId;
158296
- try {
158297
- if (sourceTask.group_id) {
158298
- const r8 = await addToGroup2(sourceTask.group_id, [newFork.id]);
158299
- forkGroupId = r8.group_id;
158300
- } else {
158301
- const r8 = await groupTasks2([sourceTask.id, newFork.id], sourceTask.title);
158302
- forkGroupId = r8.group_id;
158303
- }
158304
- } catch (err2) {
158305
- log.session.warn("fork: failed to group source + fork", {
158306
- sourceTaskId: sourceTask.id,
158307
- forkTaskId: newFork.id,
158308
- error: err2 instanceof Error ? err2.message : String(err2)
158309
- });
158310
- }
158311
- try {
158312
- task = await getTask2(newFork.id);
158313
- } catch {
158314
- task = newFork;
158315
- }
158316
- bus.emit(EventNames.TASK_CREATED, { task }, ["web-ui", "main-agent"], { source: "fork" });
159225
+ const created = await createForkSiblingTask(sourceRecord.taskId, {
159226
+ childTitle: child_title,
159227
+ prompt: message,
159228
+ source: "fork"
159229
+ });
159230
+ task = created.task;
158317
159231
  childTaskCreated = true;
158318
- if (autoTitle && message?.trim()) {
158319
- const forkId = newFork.id;
158320
- const sourceTitle = sourceBaseTitle;
158321
- const placeholderTitle = newTitle;
158322
- void (async () => {
158323
- try {
158324
- const { summarizeForkPrompt: summarizeForkPrompt2 } = await Promise.resolve().then(() => (init_fork_title(), fork_title_exports));
158325
- const label = await summarizeForkPrompt2(message);
158326
- if (!label) return;
158327
- const current5 = await getTask2(forkId);
158328
- if (current5.title !== placeholderTitle) {
158329
- log.session.info("fork title refine skipped \u2014 title changed since fork", { taskId: forkId });
158330
- return;
158331
- }
158332
- const refinedTitle = `${label} - fork of ${sourceTitle}`;
158333
- const { task: updated } = await updateTask2(forkId, { title: refinedTitle }, { source: "fork-title" });
158334
- bus.emit(EventNames.TASK_UPDATED, { task: updated }, ["web-ui", "main-agent"], { source: "fork-title" });
158335
- log.session.info("fork title refined", { taskId: forkId, title: refinedTitle });
158336
- } catch (err2) {
158337
- log.session.warn("fork title refine failed", {
158338
- taskId: forkId,
158339
- error: err2 instanceof Error ? err2.message : String(err2)
158340
- });
158341
- }
158342
- })();
158343
- }
158344
- if (forkGroupId && !sourceTask.group_id) {
158345
- const gid = forkGroupId;
158346
- const seedTitles = [sourceTask.title, newTitle];
158347
- void (async () => {
158348
- try {
158349
- const { summarizeGroupLabel: summarizeGroupLabel2 } = await Promise.resolve().then(() => (init_fork_title(), fork_title_exports));
158350
- const label = await summarizeGroupLabel2(seedTitles);
158351
- if (!label) return;
158352
- await renameGroup2(gid, label);
158353
- bus.emit(EventNames.TASK_GROUPS_CHANGED, { group_id: gid, label }, ["web-ui", "main-agent"], { source: "fork" });
158354
- log.session.info("fork group label refined", { groupId: gid, label });
158355
- } catch (err2) {
158356
- log.session.warn("fork group label refine failed", {
158357
- groupId: gid,
158358
- error: err2 instanceof Error ? err2.message : String(err2)
158359
- });
158360
- }
158361
- })();
158362
- }
158363
159232
  } else {
158364
159233
  try {
158365
159234
  task = await getTask2(task_id);
@@ -158381,7 +159250,7 @@ async function forkSessionToTask(sourceSessionId, input, source = "web-api") {
158381
159250
  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";
158382
159251
  const forkMessage = `${FORK_FOCUS_PREFIX}${imageContext ?? ""}New request:
158383
159252
  ${userRequest}`;
158384
- const forkSessionId = randomUUID10();
159253
+ const forkSessionId = randomUUID11();
158385
159254
  const forkTitle = title ?? `Fork of ${sourceRecord.title ?? sourceSessionId.slice(0, 16)}`;
158386
159255
  try {
158387
159256
  await createSessionRecord2(forkSessionId, task.id, task.project ?? "", sourceRecord.cwd, {
@@ -158532,6 +159401,11 @@ async function handleSessionControlRelay(action, sessionId, params) {
158532
159401
  result = await retrySession2(sessionId);
158533
159402
  break;
158534
159403
  }
159404
+ case "recheck": {
159405
+ const { recheckSession: recheckSession2 } = await Promise.resolve().then(() => (init_session_lifecycle(), session_lifecycle_exports));
159406
+ result = await recheckSession2(sessionId);
159407
+ break;
159408
+ }
158535
159409
  case "permission": {
158536
159410
  const { respondSessionPermission: respondSessionPermission2 } = await Promise.resolve().then(() => (init_session_lifecycle(), session_lifecycle_exports));
158537
159411
  result = await respondSessionPermission2(sessionId, p6.requestId, p6.allow, p6.message, p6.optionId, p6.answers);
@@ -159099,7 +159973,7 @@ __export(session_rewind_exports, {
159099
159973
  probeRewindViaDaemon: () => probeRewindViaDaemon,
159100
159974
  rewindSessionToMessage: () => rewindSessionToMessage
159101
159975
  });
159102
- import { randomUUID as randomUUID11 } from "crypto";
159976
+ import { randomUUID as randomUUID12 } from "crypto";
159103
159977
  function cutAncestorHistoryAtRewindPoint(ancestorMessages, rewoundAtMessageUuid) {
159104
159978
  if (!rewoundAtMessageUuid) {
159105
159979
  return { messages: ancestorMessages, found: false, dropped: 0 };
@@ -159300,7 +160174,7 @@ async function rewindSessionToMessage(sessionId, input, source = "web-api") {
159300
160174
  error: err2 instanceof Error ? err2.message : String(err2)
159301
160175
  });
159302
160176
  }
159303
- const rewoundId = randomUUID11();
160177
+ const rewoundId = randomUUID12();
159304
160178
  const baseTitle = (record.title ?? sessionId.slice(0, 16)).replace(/\s*\(rewound\)$/i, "");
159305
160179
  const title = `${baseTitle} (rewound)`.slice(0, 200);
159306
160180
  const { createSessionRecord: createSessionRecord2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
@@ -160020,6 +160894,7 @@ function parseSessionMessages(content, opts) {
160020
160894
  }
160021
160895
  }
160022
160896
  let lastPlanContent = null;
160897
+ let dropWarmupReply = false;
160023
160898
  const resultParentIds = [];
160024
160899
  const result = [];
160025
160900
  for (const [mapMsgId, msg] of messageMap) {
@@ -160097,10 +160972,18 @@ ${imgPath}` : imgPath;
160097
160972
  let text = textParts.join("\n").trim();
160098
160973
  if (!text && tools2.length === 0 && !thinking) continue;
160099
160974
  if (msg.role === "user" && text.startsWith("<")) {
160975
+ if (isCacheWarmupText(text)) {
160976
+ dropWarmupReply = true;
160977
+ continue;
160978
+ }
160100
160979
  const transformed = transformInjectedUserText(text);
160101
160980
  if (transformed === null) continue;
160102
160981
  if (transformed !== void 0) text = transformed;
160103
160982
  }
160983
+ if (msg.role === "assistant" && dropWarmupReply) {
160984
+ dropWarmupReply = false;
160985
+ if (tools2.length === 0) continue;
160986
+ }
160104
160987
  if (msg.role === "user") text = stripOutputModeWrappers(text);
160105
160988
  if (msg.role === "assistant" && !text && tools2.length === 0) continue;
160106
160989
  result.push({
@@ -161051,6 +161934,7 @@ var init_session_history = __esm({
161051
161934
  init_session_io();
161052
161935
  init_constants();
161053
161936
  init_remote_image_mirror();
161937
+ init_side_thread_warmup();
161054
161938
  init_session_file_reader();
161055
161939
  LOCAL_HOME = os36.homedir();
161056
161940
  TAIL_SEGMENT_ROLL_BYTES = 4 * 1024 * 1024;
@@ -161198,7 +162082,7 @@ var init_context_window = __esm({
161198
162082
  });
161199
162083
 
161200
162084
  // src/providers/turn-ledger.ts
161201
- import { randomUUID as randomUUID12 } from "crypto";
162085
+ import { randomUUID as randomUUID13 } from "crypto";
161202
162086
  function openTurn(sessionId) {
161203
162087
  const existing = openTurns.get(sessionId);
161204
162088
  if (existing && !existing.settled) {
@@ -161208,7 +162092,7 @@ function openTurn(sessionId) {
161208
162092
  });
161209
162093
  return { turnId: existing.turnId, promise: existing.promise };
161210
162094
  }
161211
- const turnId = randomUUID12();
162095
+ const turnId = randomUUID13();
161212
162096
  let resolve;
161213
162097
  let reject;
161214
162098
  const promise = new Promise((res, rej) => {
@@ -167956,11 +168840,39 @@ var init_claude_code_session = __esm({
167956
168840
  session2.pendingDescription = message.slice(0, 500);
167957
168841
  let appendSystemPrompt;
167958
168842
  const isFork = !!data.forkedFromSessionId;
167959
- if (data.appendSystemPrompt && !isFork) {
167960
- appendSystemPrompt = data.appendSystemPrompt;
167961
- log.session.info("using caller-provided system prompt", { taskId: taskId2, promptLength: data.appendSystemPrompt.length });
167962
- }
167963
- {
168843
+ const { parseSideLaneKey: parseSideLaneKey2 } = await Promise.resolve().then(() => (init_side_thread_fork(), side_thread_fork_exports));
168844
+ const sideIds = parseSideLaneKey2(data.lane);
168845
+ if (sideIds) {
168846
+ let exact = data.appendSystemPromptExact;
168847
+ let source = "live-or-record";
168848
+ if (exact === void 0) {
168849
+ try {
168850
+ const { getSessionByClaudeId: getSessionByClaudeId2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
168851
+ const { spawnPrefixFromRecord: spawnPrefixFromRecord2 } = await Promise.resolve().then(() => (init_spawn_prefix(), spawn_prefix_exports));
168852
+ const parentRecord = await getSessionByClaudeId2(sideIds.parentSid);
168853
+ const prefix = parentRecord ? spawnPrefixFromRecord2(parentRecord) : null;
168854
+ exact = prefix?.appendSystemPrompt ?? null;
168855
+ source = prefix?.source ?? "no-parent-record";
168856
+ } catch (err2) {
168857
+ exact = null;
168858
+ source = "lookup-failed";
168859
+ log.session.warn("side thread: parent prompt lookup failed \u2014 spawning without an append prompt", {
168860
+ parentSid: sideIds.parentSid,
168861
+ error: err2 instanceof Error ? err2.message : String(err2)
168862
+ });
168863
+ }
168864
+ }
168865
+ appendSystemPrompt = exact ?? void 0;
168866
+ log.session.info("side thread: prefix prompt resolved", {
168867
+ parentSid: sideIds.parentSid,
168868
+ source,
168869
+ promptLength: appendSystemPrompt?.length ?? 0
168870
+ });
168871
+ } else {
168872
+ if (data.appendSystemPrompt && !isFork) {
168873
+ appendSystemPrompt = data.appendSystemPrompt;
168874
+ log.session.info("using caller-provided system prompt", { taskId: taskId2, promptLength: data.appendSystemPrompt.length });
168875
+ }
167964
168876
  try {
167965
168877
  const { buildSessionContext: buildSessionContext2 } = await Promise.resolve().then(() => (init_session_context(), session_context_exports));
167966
168878
  const ctx = await buildSessionContext2(taskId2 ?? "", cwd, data.host);
@@ -167978,32 +168890,6 @@ ${ctx.systemPrompt}` : ctx.systemPrompt;
167978
168890
  log.session.warn("failed to build session context", { taskId: taskId2, error: err2 instanceof Error ? err2.message : String(err2) });
167979
168891
  }
167980
168892
  }
167981
- {
167982
- const { parseSideLaneKey: parseSideLaneKey2 } = await Promise.resolve().then(() => (init_side_thread_fork(), side_thread_fork_exports));
167983
- const sideIds = parseSideLaneKey2(data.lane);
167984
- if (sideIds) {
167985
- try {
167986
- const { getSessionByClaudeId: getSessionByClaudeId2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
167987
- const parentRecord = await getSessionByClaudeId2(sideIds.parentSid);
167988
- const inherited = parentRecord?.appliedAppendSystemPrompt;
167989
- if (inherited && Buffer.byteLength(inherited, "utf8") <= 65536) {
167990
- appendSystemPrompt = inherited;
167991
- log.session.info("side thread: inherited parent system prompt verbatim", {
167992
- parentSid: sideIds.parentSid,
167993
- promptLength: appendSystemPrompt.length
167994
- });
167995
- } else {
167996
- log.session.info("side thread: parent has no stored system prompt \u2014 fresh build (cache prefix will diverge)", {
167997
- parentSid: sideIds.parentSid
167998
- });
167999
- }
168000
- } catch (err2) {
168001
- log.session.warn("side thread: parent prompt lookup failed \u2014 fresh build", {
168002
- error: err2 instanceof Error ? err2.message : String(err2)
168003
- });
168004
- }
168005
- }
168006
- }
168007
168893
  if (data.preassignedSessionId && appendSystemPrompt && Buffer.byteLength(appendSystemPrompt, "utf8") <= 65536) {
168008
168894
  const promptToStore = appendSystemPrompt;
168009
168895
  const sidToStore = data.preassignedSessionId;
@@ -177002,7 +177888,7 @@ __export(mobile_launch_exports, {
177002
177888
  performMobileLaunch: () => performMobileLaunch,
177003
177889
  validateMobileLaunchBody: () => validateMobileLaunchBody
177004
177890
  });
177005
- import { randomUUID as randomUUID13 } from "crypto";
177891
+ import { randomUUID as randomUUID14 } from "crypto";
177006
177892
  function launchErrorCode(status2) {
177007
177893
  if (status2 === 404) return "not_found";
177008
177894
  if (status2 === 409) return "conflict";
@@ -177124,7 +178010,7 @@ async function performMobileLaunch(input, source) {
177124
178010
  );
177125
178011
  }
177126
178012
  }
177127
- const preassignedSessionId = engineCaps(input.engine).idProvisioning === "provider-issued" ? void 0 : randomUUID13();
178013
+ const preassignedSessionId = engineCaps(input.engine).idProvisioning === "provider-issued" ? void 0 : randomUUID14();
177128
178014
  const task = await quickStartSession({
177129
178015
  message: input.message,
177130
178016
  cwd: input.cwd,
@@ -177551,6 +178437,7 @@ __export(daemon_connection_exports, {
177551
178437
  isDaemonConnected: () => isDaemonConnected,
177552
178438
  isPooledConnection: () => isPooledConnection,
177553
178439
  probeDaemonSession: () => probeDaemonSession,
178440
+ probeDaemonSessionArgs: () => probeDaemonSessionArgs,
177554
178441
  pushDaemonHooksToAllHosts: () => pushDaemonHooksToAllHosts,
177555
178442
  setOnDaemonHostConnected: () => setOnDaemonHostConnected,
177556
178443
  setOnDaemonStatusChange: () => setOnDaemonStatusChange,
@@ -177727,6 +178614,28 @@ async function probeDaemonSession(hostKey2, sessionId) {
177727
178614
  return null;
177728
178615
  }
177729
178616
  }
178617
+ async function probeDaemonSessionArgs(hostKey2, sessionId, deadlineMs = 2e3) {
178618
+ const conn = getConnectedDaemonConnection(hostKey2);
178619
+ if (!conn) return null;
178620
+ try {
178621
+ const result = await Promise.race([
178622
+ conn.send("status", { sid: sessionId, includeArgs: true }),
178623
+ new Promise((_, reject) => setTimeout(
178624
+ () => reject(new Error(`status probe exceeded ${deadlineMs}ms`)),
178625
+ deadlineMs
178626
+ ).unref?.())
178627
+ ]);
178628
+ if (!result.ok || !result.alive || !Array.isArray(result.args)) return null;
178629
+ return result.args.filter((a8) => typeof a8 === "string");
178630
+ } catch (err2) {
178631
+ log.session.debug("probeDaemonSessionArgs: status probe failed", {
178632
+ hostKey: hostKey2,
178633
+ sessionId,
178634
+ error: err2 instanceof Error ? err2.message : String(err2)
178635
+ });
178636
+ return null;
178637
+ }
178638
+ }
177730
178639
  var execFileAsync23, MOBILE_ENQUEUE_LEDGER_MAX, recentMobileEnqueues, DaemonConnection, onPoolStatusChange, onHostConnected, hostConnectedListeners, connectionPool, connectingPromises, failureCache, FAILURE_CACHE_TTL_MS;
177731
178640
  var init_daemon_connection = __esm({
177732
178641
  "src/providers/daemon-connection.ts"() {
@@ -182294,6 +183203,7 @@ async function createSessionRecord(claudeSessionId, taskId2, project, cwd, extra
182294
183203
  ...extra?.effort ? { effort: extra.effort } : {},
182295
183204
  ...extra?.profile ? { profile: extra.profile } : {},
182296
183205
  ...extra?.lane ? { lane: extra.lane } : {},
183206
+ ...extra?.appliedAppendSystemPrompt !== void 0 ? { appliedAppendSystemPrompt: extra.appliedAppendSystemPrompt } : {},
182297
183207
  ...extra?.engine ? { engine: extra.engine } : {},
182298
183208
  ...extra?.acpRuntimeId ? { acpRuntimeId: extra.acpRuntimeId } : {},
182299
183209
  ...extra?.acpJournalPath ? { acpJournalPath: extra.acpJournalPath } : {},
@@ -182448,7 +183358,7 @@ function applyUpdateToSession(session2, updates, logLabel) {
182448
183358
  };
182449
183359
  if (count2 === 1) log.session.info("legacy status write suppressed", payload);
182450
183360
  else log.session.debug("legacy status write suppressed", payload);
182451
- if (updates.process_status === "error") {
183361
+ if (updates.process_status === "error" || updates.process_status === "stopped") {
182452
183362
  noteSuppressedErrorReason(session2.claudeSessionId, {
182453
183363
  reason: typeof reason === "string" ? reason : "unknown",
182454
183364
  message: typeof updates.errorMessage === "string" ? updates.errorMessage : void 0,