open-walnut 0.4.1 → 0.4.3

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 (34) hide show
  1. package/README.md +2 -5
  2. package/dist/cli-fast.js +69 -46
  3. package/dist/cli.js +882 -450
  4. package/dist/daemon-binaries/acp-worker.js.version +1 -1
  5. package/dist/integrations/calendar/index.js +42 -36
  6. package/dist/integrations/jira/index.js +100 -78
  7. package/dist/integrations/ms-todo/index.js +94 -72
  8. package/dist/web/server.js +930 -498
  9. package/dist/web/static/assets/{OfficePreview-C8OMbtiq.js → OfficePreview-DcPiG7nq.js} +2 -2
  10. package/dist/web/static/assets/{docx-preview-hDgtrOpv.js → docx-preview-BWSwr6o8.js} +1 -1
  11. package/dist/web/static/assets/index-1mQq6abn.css +32 -0
  12. package/dist/web/static/assets/{index-CPHXL-wy.js → index-A2G2h66W.js} +1 -1
  13. package/dist/web/static/assets/{index-B2-N8jTH.js → index-BWTJQD8q.js} +1 -1
  14. package/dist/web/static/assets/{index-CBHRNnCk.js → index-BcxyDA3F.js} +1 -1
  15. package/dist/web/static/assets/{index-EowLf_Xf.js → index-CEldo7Tq.js} +1 -1
  16. package/dist/web/static/assets/{index-KY36bfpA.js → index-CYY_N36O.js} +1 -1
  17. package/dist/web/static/assets/{index-ph8Hxff-.js → index-CakB6vm_.js} +1 -1
  18. package/dist/web/static/assets/{index-D81YKipC.js → index-Ch4LQw8-.js} +1 -1
  19. package/dist/web/static/assets/{index-BAb6UVet.js → index-CnmikCRj.js} +1 -1
  20. package/dist/web/static/assets/{index-Caj09uf7.js → index-CtytO7Ce.js} +1 -1
  21. package/dist/web/static/assets/{index-gv-TJwHT.js → index-CvUyfkEn.js} +1 -1
  22. package/dist/web/static/assets/{index-D1uUk0AQ.js → index-Cw6d8sbx.js} +289 -289
  23. package/dist/web/static/assets/{index-CWIydpPr.js → index-DK9AxMTA.js} +1 -1
  24. package/dist/web/static/assets/{index-DtRt21Zi.js → index-DPeOcZuH.js} +1 -1
  25. package/dist/web/static/assets/{index-nRBC-NJN.js → index-DUWox7Cm.js} +1 -1
  26. package/dist/web/static/assets/{index-bfZj1PAc.js → index-Zb9B6znf.js} +1 -1
  27. package/dist/web/static/assets/{index-C5-vT4kT.js → index-a6-uB8ph.js} +1 -1
  28. package/dist/web/static/assets/{index-VcSnwuHG.js → index-aAfs_cWd.js} +1 -1
  29. package/dist/web/static/assets/{index-B35fidy6.js → index-xpwLXfWl.js} +1 -1
  30. package/dist/web/static/assets/{jszip.min-DyrlNEg4.js → jszip.min-BXx8_j4Y.js} +1 -1
  31. package/dist/web/static/assets/{pptx-preview.es-DjND6y6O.js → pptx-preview.es-ByVfeUXp.js} +1 -1
  32. package/dist/web/static/index.html +2 -2
  33. package/package.json +1 -1
  34. package/dist/web/static/assets/index-Btn4AoIy.css +0 -32
package/dist/cli.js CHANGED
@@ -2996,10 +2996,155 @@ var init_model_catalog = __esm({
2996
2996
  }
2997
2997
  });
2998
2998
 
2999
- // src/core/ssh-config-scanner.ts
3000
- import fs6 from "fs/promises";
3001
- import path8 from "path";
2999
+ // src/core/claude-cli-detect.ts
3000
+ var claude_cli_detect_exports = {};
3001
+ __export(claude_cli_detect_exports, {
3002
+ _CLAUDE_CREDENTIALS_HOME: () => _CLAUDE_CREDENTIALS_HOME,
3003
+ describeClaudeCliAuth: () => describeClaudeCliAuth,
3004
+ detectClaudeCli: () => detectClaudeCli,
3005
+ hasClaudeSubscriptionAuth: () => hasClaudeSubscriptionAuth,
3006
+ isClaudeCliInstalled: () => isClaudeCliInstalled,
3007
+ resolveClaudeCliExecutable: () => resolveClaudeCliExecutable
3008
+ });
3009
+ import { execFileSync } from "child_process";
3010
+ import fs6 from "fs";
3002
3011
  import os3 from "os";
3012
+ import path8 from "path";
3013
+ function resolveClaudeCliExecutable(env3 = process.env) {
3014
+ const home = env3.HOME || os3.homedir();
3015
+ const pathDirs = (env3.PATH ?? "").split(path8.delimiter).filter(Boolean);
3016
+ const fallbackDirs = [
3017
+ path8.join(home, ".toolbox", "bin"),
3018
+ path8.join(home, ".local", "bin"),
3019
+ path8.join(home, ".npm-global", "bin"),
3020
+ path8.join(home, ".bun", "bin"),
3021
+ "/usr/local/bin",
3022
+ "/opt/homebrew/bin",
3023
+ "/usr/bin",
3024
+ "/bin"
3025
+ ];
3026
+ for (const dir2 of [.../* @__PURE__ */ new Set([...pathDirs, ...fallbackDirs])]) {
3027
+ const candidate = path8.join(dir2, "claude");
3028
+ try {
3029
+ fs6.accessSync(candidate, fs6.constants.X_OK);
3030
+ return candidate;
3031
+ } catch {
3032
+ }
3033
+ }
3034
+ return null;
3035
+ }
3036
+ function isClaudeCliInstalled() {
3037
+ return resolveClaudeCliExecutable() !== null;
3038
+ }
3039
+ function hasClaudeSubscriptionAuth() {
3040
+ if (process.platform === "darwin") {
3041
+ try {
3042
+ execFileSync("security", ["find-generic-password", "-s", KEYCHAIN_OAUTH_SERVICE], {
3043
+ stdio: "ignore"
3044
+ });
3045
+ return true;
3046
+ } catch {
3047
+ }
3048
+ }
3049
+ return fileCredentialExists();
3050
+ }
3051
+ function fileCredentialExists() {
3052
+ try {
3053
+ return fs6.existsSync(CLAUDE_CREDENTIALS_FILE);
3054
+ } catch {
3055
+ return false;
3056
+ }
3057
+ }
3058
+ function detectClaudeCli() {
3059
+ const installed2 = isClaudeCliInstalled();
3060
+ const subscriptionAuth = installed2 && hasClaudeSubscriptionAuth();
3061
+ return {
3062
+ installed: installed2,
3063
+ subscriptionAuth,
3064
+ subscriptionReady: installed2 && subscriptionAuth,
3065
+ ready: installed2,
3066
+ auth: installed2 ? describeClaudeCliAuth() : { mode: "unknown", label: "not installed" }
3067
+ };
3068
+ }
3069
+ function describeClaudeCliAuth(env3 = process.env, settingsEnv = readClaudeSettingsEnv()) {
3070
+ const merged = { ...env3, ...settingsEnv };
3071
+ const truthy = (v3) => !!v3 && v3 !== "0" && v3.toLowerCase() !== "false";
3072
+ if (truthy(merged.CLAUDE_CODE_USE_BEDROCK)) {
3073
+ const region = merged.AWS_REGION || merged.AWS_DEFAULT_REGION;
3074
+ return { mode: "bedrock", label: region ? `Bedrock (${region})` : "Bedrock" };
3075
+ }
3076
+ if (truthy(merged.CLAUDE_CODE_USE_VERTEX)) return { mode: "vertex", label: "Vertex AI" };
3077
+ if (merged.ANTHROPIC_API_KEY || merged.ANTHROPIC_AUTH_TOKEN) return { mode: "api-key", label: "an Anthropic API key" };
3078
+ if (hasClaudeSubscriptionAuth()) return { mode: "subscription", label: "your Claude subscription" };
3079
+ return { mode: "unknown", label: "its own login" };
3080
+ }
3081
+ function readClaudeSettingsEnv() {
3082
+ try {
3083
+ const raw = fs6.readFileSync(path8.join(os3.homedir(), ".claude", "settings.json"), "utf8");
3084
+ const env3 = JSON.parse(raw).env ?? {};
3085
+ return Object.fromEntries(Object.entries(env3).filter(([, v3]) => typeof v3 === "string"));
3086
+ } catch {
3087
+ return {};
3088
+ }
3089
+ }
3090
+ var KEYCHAIN_OAUTH_SERVICE, _CLAUDE_CREDENTIALS_HOME;
3091
+ var init_claude_cli_detect = __esm({
3092
+ "src/core/claude-cli-detect.ts"() {
3093
+ "use strict";
3094
+ init_constants();
3095
+ KEYCHAIN_OAUTH_SERVICE = "Claude Code-credentials";
3096
+ _CLAUDE_CREDENTIALS_HOME = os3.homedir();
3097
+ }
3098
+ });
3099
+
3100
+ // src/agent/providers/default-provider.ts
3101
+ var default_provider_exports = {};
3102
+ __export(default_provider_exports, {
3103
+ CLAUDE_CLI_PROVIDER: () => CLAUDE_CLI_PROVIDER,
3104
+ LEGACY_DEFAULT_PROVIDER: () => LEGACY_DEFAULT_PROVIDER,
3105
+ _resetDefaultProviderCacheForTesting: () => _resetDefaultProviderCacheForTesting,
3106
+ hasExplicitBedrockConfig: () => hasExplicitBedrockConfig,
3107
+ mainProviderIsImplicit: () => mainProviderIsImplicit,
3108
+ resolveMainProviderName: () => resolveMainProviderName
3109
+ });
3110
+ function claudeCliInstalledCached() {
3111
+ const now = Date.now();
3112
+ if (!cliProbe || now - cliProbe.at > CLI_PROBE_TTL_MS) {
3113
+ cliProbe = { at: now, installed: isClaudeCliInstalled() };
3114
+ }
3115
+ return cliProbe.installed;
3116
+ }
3117
+ function _resetDefaultProviderCacheForTesting() {
3118
+ cliProbe = void 0;
3119
+ }
3120
+ function hasExplicitBedrockConfig(config) {
3121
+ const b8 = config.providers?.bedrock;
3122
+ if (b8 && (b8.bearer_token || b8.aws_access_key_id || b8.aws_profile || b8.aws_credential_export)) return true;
3123
+ return !!config.provider?.bedrock_bearer_token;
3124
+ }
3125
+ function resolveMainProviderName(config, claudeInstalled = claudeCliInstalledCached()) {
3126
+ if (config.agent?.main_provider) return config.agent.main_provider;
3127
+ if (claudeInstalled) return CLAUDE_CLI_PROVIDER;
3128
+ return LEGACY_DEFAULT_PROVIDER;
3129
+ }
3130
+ function mainProviderIsImplicit(config) {
3131
+ return !config.agent?.main_provider;
3132
+ }
3133
+ var LEGACY_DEFAULT_PROVIDER, CLAUDE_CLI_PROVIDER, CLI_PROBE_TTL_MS, cliProbe;
3134
+ var init_default_provider = __esm({
3135
+ "src/agent/providers/default-provider.ts"() {
3136
+ "use strict";
3137
+ init_claude_cli_detect();
3138
+ LEGACY_DEFAULT_PROVIDER = "bedrock";
3139
+ CLAUDE_CLI_PROVIDER = "claude_cli";
3140
+ CLI_PROBE_TTL_MS = 6e4;
3141
+ }
3142
+ });
3143
+
3144
+ // src/core/ssh-config-scanner.ts
3145
+ import fs7 from "fs/promises";
3146
+ import path9 from "path";
3147
+ import os4 from "os";
3003
3148
  function isGitServerHost(h8) {
3004
3149
  if (h8.user === "git") return true;
3005
3150
  const gitPrefix = /^(git|github|gitlab|bitbucket|codecommit|ssh\.github)\./i;
@@ -3015,10 +3160,10 @@ function shortLabelForFqdn(hostname) {
3015
3160
  return `${first.slice(0, 12)}\u2026${first.slice(-8)}`;
3016
3161
  }
3017
3162
  async function scanSshConfig(configPath) {
3018
- const sshConfigPath = configPath ?? path8.join(os3.homedir(), ".ssh", "config");
3163
+ const sshConfigPath = configPath ?? path9.join(os4.homedir(), ".ssh", "config");
3019
3164
  let mtimeMs2;
3020
3165
  try {
3021
- mtimeMs2 = (await fs6.stat(sshConfigPath)).mtimeMs;
3166
+ mtimeMs2 = (await fs7.stat(sshConfigPath)).mtimeMs;
3022
3167
  } catch {
3023
3168
  return /* @__PURE__ */ new Map();
3024
3169
  }
@@ -3031,7 +3176,7 @@ async function scanSshConfig(configPath) {
3031
3176
  async function parseSshConfigFile(sshConfigPath) {
3032
3177
  const discovered = /* @__PURE__ */ new Map();
3033
3178
  try {
3034
- const content = await fs6.readFile(sshConfigPath, "utf-8");
3179
+ const content = await fs7.readFile(sshConfigPath, "utf-8");
3035
3180
  const lines = content.split("\n");
3036
3181
  let currentHost = null;
3037
3182
  for (const rawLine of lines) {
@@ -3132,23 +3277,24 @@ __export(config_manager_exports, {
3132
3277
  updatePushTokens: () => updatePushTokens,
3133
3278
  writeFileAtomic: () => writeFileAtomic2
3134
3279
  });
3135
- import fs7 from "fs/promises";
3280
+ import fs8 from "fs/promises";
3136
3281
  import yaml from "js-yaml";
3137
- function resolveAgentEngineProvider(config) {
3282
+ function resolveAgentEngineProvider(config, claudeInstalled) {
3138
3283
  if (CLOUD_MODE) return "walnut-agent";
3139
3284
  const raw = config.agent?.provider;
3140
3285
  if (typeof raw === "string" && VALID_AGENT_ENGINE_PROVIDERS.has(raw)) {
3141
3286
  return raw;
3142
3287
  }
3143
- if (raw !== void 0) {
3144
- log.session.warn("config-manager: agent.provider is not a known engine \u2014 using the default", {
3145
- value: typeof raw === "string" ? raw : `<${typeof raw}>`,
3146
- using: FALLBACK_AGENT_ENGINE_PROVIDER,
3147
- valid: [...VALID_AGENT_ENGINE_PROVIDERS].join(", ")
3148
- });
3149
- return FALLBACK_AGENT_ENGINE_PROVIDER;
3288
+ if (raw === void 0) {
3289
+ const main = claudeInstalled === void 0 ? resolveMainProviderName(config) : resolveMainProviderName(config, claudeInstalled);
3290
+ return main === CLAUDE_CLI_PROVIDER ? "claude-code" : "walnut-agent";
3150
3291
  }
3151
- return DEFAULT_AGENT_ENGINE_PROVIDER;
3292
+ log.session.warn("config-manager: agent.provider is not a known engine \u2014 using the fallback", {
3293
+ value: typeof raw === "string" ? raw : `<${typeof raw}>`,
3294
+ using: FALLBACK_AGENT_ENGINE_PROVIDER,
3295
+ valid: [...VALID_AGENT_ENGINE_PROVIDERS].join(", ")
3296
+ });
3297
+ return FALLBACK_AGENT_ENGINE_PROVIDER;
3152
3298
  }
3153
3299
  function migrateConfigToProjectOnly(config) {
3154
3300
  let changed = false;
@@ -3221,13 +3367,13 @@ function migrateConfigToProjectOnly(config) {
3221
3367
  }
3222
3368
  async function readRawConfigContent() {
3223
3369
  try {
3224
- const primary = await fs7.readFile(CONFIG_FILE, "utf-8");
3370
+ const primary = await fs8.readFile(CONFIG_FILE, "utf-8");
3225
3371
  if (primary.trim()) return primary;
3226
3372
  throw new Error("config.yaml is empty");
3227
3373
  } catch (primaryErr) {
3228
3374
  let backup;
3229
3375
  try {
3230
- backup = await fs7.readFile(CONFIG_BACKUP_FILE, "utf-8");
3376
+ backup = await fs8.readFile(CONFIG_BACKUP_FILE, "utf-8");
3231
3377
  } catch {
3232
3378
  return null;
3233
3379
  }
@@ -3237,7 +3383,7 @@ async function readRawConfigContent() {
3237
3383
  backup: CONFIG_BACKUP_FILE
3238
3384
  });
3239
3385
  try {
3240
- await fs7.writeFile(CONFIG_FILE, backup, "utf-8");
3386
+ await fs8.writeFile(CONFIG_FILE, backup, "utf-8");
3241
3387
  } catch {
3242
3388
  }
3243
3389
  return backup;
@@ -3246,10 +3392,10 @@ async function readRawConfigContent() {
3246
3392
  async function writeFileAtomic2(filePath, content) {
3247
3393
  const tmp = `${filePath}.tmp-${process.pid}-${Date.now().toString(36)}`;
3248
3394
  try {
3249
- await fs7.writeFile(tmp, content, "utf-8");
3250
- await fs7.rename(tmp, filePath);
3395
+ await fs8.writeFile(tmp, content, "utf-8");
3396
+ await fs8.rename(tmp, filePath);
3251
3397
  } catch (err2) {
3252
- await fs7.rm(tmp, { force: true }).catch(() => {
3398
+ await fs8.rm(tmp, { force: true }).catch(() => {
3253
3399
  });
3254
3400
  throw err2;
3255
3401
  }
@@ -3447,7 +3593,7 @@ async function seedConfigDefaults() {
3447
3593
  const config = await getConfig();
3448
3594
  let needsWrite = false;
3449
3595
  try {
3450
- const raw = await fs7.readFile(CONFIG_FILE, "utf-8");
3596
+ const raw = await fs8.readFile(CONFIG_FILE, "utf-8");
3451
3597
  if (!raw.includes("available_models")) {
3452
3598
  needsWrite = true;
3453
3599
  }
@@ -3475,6 +3621,7 @@ var init_config_manager = __esm({
3475
3621
  init_constants();
3476
3622
  init_types();
3477
3623
  init_model_catalog();
3624
+ init_default_provider();
3478
3625
  init_ssh_config_scanner();
3479
3626
  DEFAULT_CONFIG = {
3480
3627
  version: 1,
@@ -3486,12 +3633,12 @@ var init_config_manager = __esm({
3486
3633
  // so this never re-routes an established setup.
3487
3634
  defaults: { priority: "none", platform: "local" },
3488
3635
  provider: { type: "claude-code" },
3489
- // Which ENGINE answers a Personal AI chat turn. 'walnut-agent' = today's in-process
3490
- // agent loop; 'claude-code' routes the turn into a lane-bound `claude` session.
3491
- // NOTE: getConfig() spreads the parsed file OVER these defaults at the TOP
3492
- // level, so a config.yaml with any `agent:` section replaces this whole object
3493
- // — resolveAgentEngineProvider() (below) is what actually applies the default.
3494
- agent: { provider: DEFAULT_AGENT_ENGINE_PROVIDER }
3636
+ // Which ENGINE answers a Personal AI chat turn is NOT defaulted here: it follows
3637
+ // the AI provider (Settings → AI Provider) — see resolveAgentEngineProvider().
3638
+ // getConfig() spreads the parsed file OVER these defaults at the TOP level, so
3639
+ // anything put in this object is dropped by any config.yaml with an `agent:`
3640
+ // section; the resolver is what actually applies the rule.
3641
+ agent: {}
3495
3642
  };
3496
3643
  CONFIG_BACKUP_FILE = `${CONFIG_FILE}.bak`;
3497
3644
  writeLock = Promise.resolve();
@@ -3500,15 +3647,15 @@ var init_config_manager = __esm({
3500
3647
 
3501
3648
  // src/core/tmp-sweep.ts
3502
3649
  import fsp5 from "fs/promises";
3503
- import path9 from "path";
3650
+ import path10 from "path";
3504
3651
  function sweepDirs() {
3505
3652
  return [
3506
3653
  WALNUT_HOME,
3507
3654
  SYNC_DIR,
3508
- path9.join(WALNUT_HOME, "sessions"),
3509
- path9.join(WALNUT_HOME, "sessions", "transcripts"),
3655
+ path10.join(WALNUT_HOME, "sessions"),
3656
+ path10.join(WALNUT_HOME, "sessions", "transcripts"),
3510
3657
  TASKS_DIR,
3511
- path9.join(TASKS_DIR, "outbox"),
3658
+ path10.join(TASKS_DIR, "outbox"),
3512
3659
  CONVERSATIONS_DIR,
3513
3660
  MEMORY_DIR,
3514
3661
  DAILY_DIR,
@@ -3529,7 +3676,7 @@ async function sweepOrphanAtomicTmpFiles() {
3529
3676
  const entries = await fsp5.readdir(dir2, { withFileTypes: true });
3530
3677
  for (const entry of entries) {
3531
3678
  if (!entry.isFile() || !ORPHAN_RE.test(entry.name)) continue;
3532
- const full = path9.join(dir2, entry.name);
3679
+ const full = path10.join(dir2, entry.name);
3533
3680
  try {
3534
3681
  const st = await fsp5.stat(full);
3535
3682
  if (st.mtimeMs > cutoff) continue;
@@ -3566,10 +3713,10 @@ __export(init_exports, {
3566
3713
  initDirectories: () => initDirectories
3567
3714
  });
3568
3715
  import fsp6 from "fs/promises";
3569
- import fs8 from "fs";
3716
+ import fs9 from "fs";
3570
3717
  async function migrateLegacyMemoryFile() {
3571
3718
  try {
3572
- if (fs8.existsSync(LEGACY_MEMORY_FILE) && !fs8.existsSync(MEMORY_FILE)) {
3719
+ if (fs9.existsSync(LEGACY_MEMORY_FILE) && !fs9.existsSync(MEMORY_FILE)) {
3573
3720
  await fsp6.rename(LEGACY_MEMORY_FILE, MEMORY_FILE);
3574
3721
  log.memory.info("Migrated MEMORY.md into memory/ folder", { from: LEGACY_MEMORY_FILE, to: MEMORY_FILE });
3575
3722
  }
@@ -4524,15 +4671,15 @@ __export(task_db_exports, {
4524
4671
  transaction: () => transaction
4525
4672
  });
4526
4673
  import Database from "better-sqlite3";
4527
- import fs9 from "fs";
4528
- import path10 from "path";
4674
+ import fs10 from "fs";
4675
+ import path11 from "path";
4529
4676
  import yaml2 from "js-yaml";
4530
4677
  function getDb() {
4531
4678
  if (db) return db;
4532
4679
  if (initAttempted) throw initError;
4533
4680
  initAttempted = true;
4534
4681
  try {
4535
- fs9.mkdirSync(path10.dirname(TASK_DB_PATH), { recursive: true });
4682
+ fs10.mkdirSync(path11.dirname(TASK_DB_PATH), { recursive: true });
4536
4683
  const handle2 = new Database(TASK_DB_PATH);
4537
4684
  handle2.pragma("journal_mode = WAL");
4538
4685
  handle2.pragma("busy_timeout = 5000");
@@ -4762,8 +4909,8 @@ function migrateToProjectOnly(handle2) {
4762
4909
  }
4763
4910
  try {
4764
4911
  handle2.pragma("wal_checkpoint(TRUNCATE)");
4765
- if (!fs9.existsSync(TASK_DB_PRE_V5_BACKUP_PATH)) {
4766
- fs9.copyFileSync(TASK_DB_PATH, TASK_DB_PRE_V5_BACKUP_PATH);
4912
+ if (!fs10.existsSync(TASK_DB_PRE_V5_BACKUP_PATH)) {
4913
+ fs10.copyFileSync(TASK_DB_PATH, TASK_DB_PRE_V5_BACKUP_PATH);
4767
4914
  log.task.info("task-db v5: wrote pre-migration backup", { path: TASK_DB_PRE_V5_BACKUP_PATH });
4768
4915
  }
4769
4916
  } catch (err2) {
@@ -5273,7 +5420,7 @@ var init_task_db = __esm({
5273
5420
  init_constants();
5274
5421
  init_logging();
5275
5422
  init_phase2();
5276
- TASK_DB_PATH = path10.join(TASKS_DIR, "tasks.sqlite");
5423
+ TASK_DB_PATH = path11.join(TASKS_DIR, "tasks.sqlite");
5277
5424
  EXPLICIT_TASK_COLUMNS = [
5278
5425
  "id",
5279
5426
  "title",
@@ -5436,8 +5583,8 @@ var init_task_db = __esm({
5436
5583
  });
5437
5584
 
5438
5585
  // src/core/task-db-migration.ts
5439
- import fs10 from "fs";
5440
- import path11 from "path";
5586
+ import fs11 from "fs";
5587
+ import path12 from "path";
5441
5588
  import yaml3 from "js-yaml";
5442
5589
  async function runMigrationIfNeeded() {
5443
5590
  const db5 = getDb();
@@ -5448,7 +5595,7 @@ async function runMigrationIfNeeded() {
5448
5595
  if (existing.n > 0) {
5449
5596
  return { migrated: false, count: existing.n };
5450
5597
  }
5451
- if (!fs10.existsSync(TASKS_FILE)) {
5598
+ if (!fs11.existsSync(TASKS_FILE)) {
5452
5599
  return { migrated: false, count: 0 };
5453
5600
  }
5454
5601
  const store3 = await readJsonFile(TASKS_FILE, { version: 1, tasks: [] });
@@ -5605,9 +5752,9 @@ async function runMigrationIfNeeded() {
5605
5752
  }
5606
5753
  const after = db5.prepare("SELECT COUNT(*) AS n FROM tasks").get();
5607
5754
  const count2 = after.n;
5608
- const backupPath = path11.join(path11.dirname(TASKS_FILE), "tasks.json.migrated-from-json.backup");
5755
+ const backupPath = path12.join(path12.dirname(TASKS_FILE), "tasks.json.migrated-from-json.backup");
5609
5756
  try {
5610
- fs10.copyFileSync(TASKS_FILE, backupPath);
5757
+ fs11.copyFileSync(TASKS_FILE, backupPath);
5611
5758
  } catch (err2) {
5612
5759
  log.task.warn("task-db migration: backup copy failed (non-fatal)", {
5613
5760
  path: backupPath,
@@ -5631,9 +5778,9 @@ var init_task_db_migration = __esm({
5631
5778
  });
5632
5779
 
5633
5780
  // src/core/memory-dir-migration.ts
5634
- import fs11 from "fs";
5781
+ import fs12 from "fs";
5635
5782
  import fsp7 from "fs/promises";
5636
- import path12 from "path";
5783
+ import path13 from "path";
5637
5784
  function bodyOf(content) {
5638
5785
  const fm = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
5639
5786
  return fm ? content.slice(fm[0].length) : content;
@@ -5651,7 +5798,7 @@ ${legacyBody}
5651
5798
  }
5652
5799
  function mtimeMs(file) {
5653
5800
  try {
5654
- return fs11.statSync(file).mtimeMs;
5801
+ return fs12.statSync(file).mtimeMs;
5655
5802
  } catch {
5656
5803
  return 0;
5657
5804
  }
@@ -5660,24 +5807,24 @@ function findCandidates(root6) {
5660
5807
  const out = [];
5661
5808
  let level1;
5662
5809
  try {
5663
- level1 = fs11.readdirSync(root6, { withFileTypes: true });
5810
+ level1 = fs12.readdirSync(root6, { withFileTypes: true });
5664
5811
  } catch {
5665
5812
  return out;
5666
5813
  }
5667
5814
  for (const cat of level1) {
5668
5815
  if (!cat.isDirectory()) continue;
5669
- const catDir = path12.join(root6, cat.name);
5670
- if (fs11.existsSync(path12.join(catDir, MEMORY_FILE_NAME))) continue;
5816
+ const catDir = path13.join(root6, cat.name);
5817
+ if (fs12.existsSync(path13.join(catDir, MEMORY_FILE_NAME))) continue;
5671
5818
  let level2;
5672
5819
  try {
5673
- level2 = fs11.readdirSync(catDir, { withFileTypes: true });
5820
+ level2 = fs12.readdirSync(catDir, { withFileTypes: true });
5674
5821
  } catch {
5675
5822
  continue;
5676
5823
  }
5677
5824
  for (const proj of level2) {
5678
5825
  if (!proj.isDirectory()) continue;
5679
- const sourceDir = path12.join(catDir, proj.name);
5680
- if (!fs11.existsSync(path12.join(sourceDir, MEMORY_FILE_NAME))) continue;
5826
+ const sourceDir = path13.join(catDir, proj.name);
5827
+ if (!fs12.existsSync(path13.join(sourceDir, MEMORY_FILE_NAME))) continue;
5681
5828
  out.push({ category: cat.name, project: proj.name, sourceDir });
5682
5829
  }
5683
5830
  }
@@ -5691,11 +5838,11 @@ async function drainDir(from, to) {
5691
5838
  return;
5692
5839
  }
5693
5840
  for (const name of entries) {
5694
- const target = path12.join(to, name);
5695
- if (fs11.existsSync(target)) continue;
5696
- await fsp7.rename(path12.join(from, name), target).catch((err2) => {
5841
+ const target = path13.join(to, name);
5842
+ if (fs12.existsSync(target)) continue;
5843
+ await fsp7.rename(path13.join(from, name), target).catch((err2) => {
5697
5844
  log.memory.warn("memory-dir migration: entry move failed", {
5698
- from: path12.join(from, name),
5845
+ from: path13.join(from, name),
5699
5846
  to: target,
5700
5847
  error: String(err2)
5701
5848
  });
@@ -5707,23 +5854,23 @@ async function drainDir(from, to) {
5707
5854
  async function migrateProjectMemoryDirs() {
5708
5855
  const result = { ran: false, moved: 0, merged: 0 };
5709
5856
  try {
5710
- if (!fs11.existsSync(PROJECTS_MEMORY_DIR)) return result;
5711
- if (fs11.existsSync(MEMORY_DIR_MIGRATION_MARKER)) return result;
5857
+ if (!fs12.existsSync(PROJECTS_MEMORY_DIR)) return result;
5858
+ if (fs12.existsSync(MEMORY_DIR_MIGRATION_MARKER)) return result;
5712
5859
  result.ran = true;
5713
5860
  const candidates = findCandidates(PROJECTS_MEMORY_DIR);
5714
5861
  const touchedCategories = new Set(candidates.map((c8) => c8.category));
5715
5862
  for (const c8 of candidates) {
5716
- const targetDir = path12.join(PROJECTS_MEMORY_DIR, c8.project);
5863
+ const targetDir = path13.join(PROJECTS_MEMORY_DIR, c8.project);
5717
5864
  const label = `${c8.category}/${c8.project}`;
5718
5865
  try {
5719
- if (!fs11.existsSync(targetDir)) {
5866
+ if (!fs12.existsSync(targetDir)) {
5720
5867
  await fsp7.rename(c8.sourceDir, targetDir);
5721
5868
  result.moved += 1;
5722
5869
  continue;
5723
5870
  }
5724
- const sourceMem = path12.join(c8.sourceDir, MEMORY_FILE_NAME);
5725
- const targetMem = path12.join(targetDir, MEMORY_FILE_NAME);
5726
- if (!fs11.existsSync(targetMem)) {
5871
+ const sourceMem = path13.join(c8.sourceDir, MEMORY_FILE_NAME);
5872
+ const targetMem = path13.join(targetDir, MEMORY_FILE_NAME);
5873
+ if (!fs12.existsSync(targetMem)) {
5727
5874
  await fsp7.rename(sourceMem, targetMem);
5728
5875
  await drainDir(c8.sourceDir, targetDir);
5729
5876
  result.moved += 1;
@@ -5745,7 +5892,7 @@ async function migrateProjectMemoryDirs() {
5745
5892
  }
5746
5893
  }
5747
5894
  for (const cat of touchedCategories) {
5748
- await fsp7.rmdir(path12.join(PROJECTS_MEMORY_DIR, cat)).catch(() => {
5895
+ await fsp7.rmdir(path13.join(PROJECTS_MEMORY_DIR, cat)).catch(() => {
5749
5896
  });
5750
5897
  }
5751
5898
  await fsp7.writeFile(
@@ -5773,7 +5920,7 @@ var init_memory_dir_migration = __esm({
5773
5920
  "use strict";
5774
5921
  init_constants();
5775
5922
  init_logging();
5776
- MEMORY_DIR_MIGRATION_MARKER = path12.join(PROJECTS_MEMORY_DIR, ".migrated-project-only");
5923
+ MEMORY_DIR_MIGRATION_MARKER = path13.join(PROJECTS_MEMORY_DIR, ".migrated-project-only");
5777
5924
  MEMORY_FILE_NAME = "MEMORY.md";
5778
5925
  }
5779
5926
  });
@@ -5989,7 +6136,7 @@ __export(microsoft_todo_exports, {
5989
6136
  });
5990
6137
  import * as msal from "@azure/msal-node";
5991
6138
  import https from "https";
5992
- import path13 from "path";
6139
+ import path14 from "path";
5993
6140
  function getMsTodoConfig(config) {
5994
6141
  return config.plugins?.["ms-todo"];
5995
6142
  }
@@ -7208,8 +7355,8 @@ var init_microsoft_todo = __esm({
7208
7355
  init_format();
7209
7356
  init_task_manager();
7210
7357
  init_task_remote_links();
7211
- TOKENS_FILE = path13.join(SYNC_DIR, "ms-todo-tokens.json");
7212
- DELTA_FILE = path13.join(SYNC_DIR, "ms-todo-delta.json");
7358
+ TOKENS_FILE = path14.join(SYNC_DIR, "ms-todo-tokens.json");
7359
+ DELTA_FILE = path14.join(SYNC_DIR, "ms-todo-delta.json");
7213
7360
  GRAPH_BASE = "https://graph.microsoft.com/v1.0";
7214
7361
  SCOPES = ["Tasks.ReadWrite"];
7215
7362
  PRIORITY_TO_IMPORTANCE = {
@@ -7332,8 +7479,8 @@ __export(daemon_version_check_exports, {
7332
7479
  verifyDaemonBinaryVersion: () => verifyDaemonBinaryVersion
7333
7480
  });
7334
7481
  import { createHash } from "crypto";
7335
- import fs12 from "fs";
7336
- import path14 from "path";
7482
+ import fs13 from "fs";
7483
+ import path15 from "path";
7337
7484
  import { spawnSync } from "child_process";
7338
7485
  import { fileURLToPath as fileURLToPath2 } from "url";
7339
7486
  function computeExpectedDaemonVersion() {
@@ -7342,11 +7489,11 @@ function computeExpectedDaemonVersion() {
7342
7489
  const hash = createHash("sha256");
7343
7490
  const NUL = Buffer.from([0]);
7344
7491
  for (const rel of DAEMON_SOURCE_FILES) {
7345
- const abs = path14.join(repoRoot, rel);
7492
+ const abs = path15.join(repoRoot, rel);
7346
7493
  try {
7347
7494
  hash.update(Buffer.from(rel));
7348
7495
  hash.update(NUL);
7349
- hash.update(fs12.readFileSync(abs));
7496
+ hash.update(fs13.readFileSync(abs));
7350
7497
  hash.update(NUL);
7351
7498
  } catch {
7352
7499
  return null;
@@ -7355,12 +7502,12 @@ function computeExpectedDaemonVersion() {
7355
7502
  return "walnut-daemon-" + hash.digest("hex").slice(0, 12);
7356
7503
  }
7357
7504
  function findRepoRoot() {
7358
- let dir2 = path14.dirname(fileURLToPath2(import.meta.url));
7505
+ let dir2 = path15.dirname(fileURLToPath2(import.meta.url));
7359
7506
  for (let i8 = 0; i8 < 10; i8++) {
7360
- if (fs12.existsSync(path14.join(dir2, "package.json")) && fs12.existsSync(path14.join(dir2, "src", "providers", "daemon-standalone.ts"))) {
7507
+ if (fs13.existsSync(path15.join(dir2, "package.json")) && fs13.existsSync(path15.join(dir2, "src", "providers", "daemon-standalone.ts"))) {
7361
7508
  return dir2;
7362
7509
  }
7363
- const parent = path14.dirname(dir2);
7510
+ const parent = path15.dirname(dir2);
7364
7511
  if (parent === dir2) break;
7365
7512
  dir2 = parent;
7366
7513
  }
@@ -7369,14 +7516,14 @@ function findRepoRoot() {
7369
7516
  function bunAvailable() {
7370
7517
  const home = process.env.HOME ?? "";
7371
7518
  const candidates = [
7372
- ...(process.env.PATH ?? "").split(path14.delimiter).filter(Boolean).map((d8) => path14.join(d8, "bun")),
7373
- path14.join(home, ".bun", "bin", "bun"),
7519
+ ...(process.env.PATH ?? "").split(path15.delimiter).filter(Boolean).map((d8) => path15.join(d8, "bun")),
7520
+ path15.join(home, ".bun", "bin", "bun"),
7374
7521
  "/opt/homebrew/bin/bun",
7375
7522
  "/usr/local/bin/bun"
7376
7523
  ];
7377
7524
  return candidates.some((p6) => {
7378
7525
  try {
7379
- fs12.accessSync(p6, fs12.constants.X_OK);
7526
+ fs13.accessSync(p6, fs13.constants.X_OK);
7380
7527
  return true;
7381
7528
  } catch {
7382
7529
  return false;
@@ -7386,10 +7533,10 @@ function bunAvailable() {
7386
7533
  function readBuiltVersions() {
7387
7534
  const out = /* @__PURE__ */ new Map();
7388
7535
  try {
7389
- for (const f8 of fs12.readdirSync(DAEMON_BINARIES_DIR)) {
7536
+ for (const f8 of fs13.readdirSync(DAEMON_BINARIES_DIR)) {
7390
7537
  if (!f8.endsWith(".version")) continue;
7391
7538
  const arch = f8.slice(0, -".version".length);
7392
- const v3 = fs12.readFileSync(path14.join(DAEMON_BINARIES_DIR, f8), "utf-8").trim();
7539
+ const v3 = fs13.readFileSync(path15.join(DAEMON_BINARIES_DIR, f8), "utf-8").trim();
7393
7540
  if (v3) out.set(arch, v3);
7394
7541
  }
7395
7542
  } catch {
@@ -7849,12 +7996,12 @@ var init_daemon_fold = __esm({
7849
7996
  });
7850
7997
 
7851
7998
  // src/providers/daemon-source.ts
7852
- import fs13 from "fs";
7853
- import path15 from "path";
7999
+ import fs14 from "fs";
8000
+ import path16 from "path";
7854
8001
  import { fileURLToPath as fileURLToPath3 } from "url";
7855
8002
  function resolveDaemonSourceVersion() {
7856
8003
  const here = fileURLToPath3(import.meta.url);
7857
- const bundled = here.split(path15.sep).includes("dist");
8004
+ const bundled = here.split(path16.sep).includes("dist");
7858
8005
  if (bundled) {
7859
8006
  const sidecar = readSidecarDaemonVersion(here);
7860
8007
  if (sidecar) return sidecar;
@@ -7862,32 +8009,32 @@ function resolveDaemonSourceVersion() {
7862
8009
  const computed = computeExpectedDaemonVersion();
7863
8010
  if (computed) return computed;
7864
8011
  if (process.env.DAEMON_VERSION) return process.env.DAEMON_VERSION;
7865
- let dir2 = path15.dirname(fileURLToPath3(import.meta.url));
8012
+ let dir2 = path16.dirname(fileURLToPath3(import.meta.url));
7866
8013
  for (let i8 = 0; i8 < 10; i8++) {
7867
8014
  try {
7868
- const pkg = JSON.parse(fs13.readFileSync(path15.join(dir2, "package.json"), "utf-8"));
8015
+ const pkg = JSON.parse(fs14.readFileSync(path16.join(dir2, "package.json"), "utf-8"));
7869
8016
  if (pkg.name === "open-walnut" && pkg.version) return `walnut-daemon-pkg-${pkg.version}`;
7870
8017
  } catch {
7871
8018
  }
7872
- const parent = path15.dirname(dir2);
8019
+ const parent = path16.dirname(dir2);
7873
8020
  if (parent === dir2) break;
7874
8021
  dir2 = parent;
7875
8022
  }
7876
8023
  return "dev-source";
7877
8024
  }
7878
8025
  function readSidecarDaemonVersion(fromPath) {
7879
- let dir2 = path15.dirname(fromPath);
8026
+ let dir2 = path16.dirname(fromPath);
7880
8027
  for (let i8 = 0; i8 < 5; i8++) {
7881
- const binDir = path15.join(dir2, "daemon-binaries");
8028
+ const binDir = path16.join(dir2, "daemon-binaries");
7882
8029
  try {
7883
- for (const name of fs13.readdirSync(binDir)) {
8030
+ for (const name of fs14.readdirSync(binDir)) {
7884
8031
  if (!name.endsWith(".version")) continue;
7885
- const v3 = fs13.readFileSync(path15.join(binDir, name), "utf-8").trim();
8032
+ const v3 = fs14.readFileSync(path16.join(binDir, name), "utf-8").trim();
7886
8033
  if (v3) return v3;
7887
8034
  }
7888
8035
  } catch {
7889
8036
  }
7890
- const parent = path15.dirname(dir2);
8037
+ const parent = path16.dirname(dir2);
7891
8038
  if (parent === dir2) break;
7892
8039
  dir2 = parent;
7893
8040
  }
@@ -8815,24 +8962,25 @@ function reapSession(sid, code, reason) {
8815
8962
  let jsonlAgeMs = null;
8816
8963
  try { jsonlAgeMs = Date.now() - fs.statSync(session.jsonlPath).mtimeMs; } catch {}
8817
8964
  const cleanExit = isTurnCompleteExit(session.jsonlPath);
8818
- // An INTENTIONAL daemon-initiated reclamation is not a failure, and the JSONL
8819
- // tail cannot tell us it happened. The idle scanner stamps idleReclaimAt
8820
- // before it signals; the death then lands here through whichever path notices
8821
- // it (usually orphan-poll ESRCH \u2192 code -1). Verified production case: the last
8822
- // line was a control_response (the reply to a Walnut-issued control request \u2014
8823
- // a routine shape), so isTurnCompleteExit said false, exitCode stayed -1, and
8824
- // the projection painted a red "Error" over a healthy, fully resumable session
8825
- // we reclaimed on purpose. Checked FIRST and unconditionally: the flag carries
8826
- // information the tail does not contain, which is why isTurnCompleteExit is
8827
- // left alone. Keep in sync with daemon-core.ts reapSession.
8828
- const intentionalReclaim = session.idleReclaimAt != null;
8829
- if (intentionalReclaim && code !== 0) {
8830
- logMsg('info', 'reapSession: intentional idle reclamation, normalizing exit code', {
8965
+ // A DELIBERATE stop is not a failure, and the JSONL tail cannot tell us it
8966
+ // happened. Both writers (our idle scanner, and any stop RPC via cmdStop)
8967
+ // stamp intentionalStopAt before signalling; the death then lands here through
8968
+ // whichever path notices it (usually orphan-poll ESRCH \u2192 code -1). Verified
8969
+ // production case: the last line was a control_response (the reply to a
8970
+ // Walnut-issued control request \u2014 a routine shape), so isTurnCompleteExit said
8971
+ // false, exitCode stayed -1, and the projection painted a red "Error" over a
8972
+ // healthy, fully resumable session that was stopped on purpose. Checked FIRST
8973
+ // and unconditionally: the flag carries information the tail does not contain,
8974
+ // which is why isTurnCompleteExit is left alone.
8975
+ // Keep in sync with daemon-core.ts reapSession.
8976
+ const intentionalStop = session.intentionalStopAt != null;
8977
+ if (intentionalStop && code !== 0) {
8978
+ logMsg('info', 'reapSession: intentional stop, normalizing exit code', {
8831
8979
  sid, pid: session.pid, originalCode: code, originalReason: reason,
8832
- jsonlAgeMs, cleanExit, idleReclaimAt: session.idleReclaimAt,
8980
+ jsonlAgeMs, cleanExit, intentionalStopAt: session.intentionalStopAt,
8833
8981
  });
8834
8982
  code = 0;
8835
- reason = reason + '+intentional-idle-reclaim';
8983
+ reason = reason + '+intentional-stop';
8836
8984
  } else if (code !== 0 && cleanExit) {
8837
8985
  logMsg('info', 'reapSession: turn-complete detected, normalizing exit code', {
8838
8986
  sid, pid: session.pid, originalCode: code, originalReason: reason, jsonlAgeMs,
@@ -12486,6 +12634,15 @@ function cmdStop(ws, id, cmd) {
12486
12634
  const pid = session.pid;
12487
12635
  logMsg('info', 'cmdStop: stopping session (process group kill)', { sid, pid });
12488
12636
 
12637
+ // Nobody sends stop by accident, so record the intent BEFORE the first signal:
12638
+ // the death lands asynchronously (orphan poll \u2192 ESRCH \u2192 code -1) and by then
12639
+ // nothing remembers this was asked for. This path is how the SERVER's idle
12640
+ // reaper kills (mgr.kill() \u2192 stop RPC), not the daemon's own scan, so it needs
12641
+ // its own stamp: a LOCAL session auto-stopped after 2h idle surfaced as
12642
+ // "Session ended unexpectedly and no cause was recorded" (2026-09-03).
12643
+ // Keep in sync with daemon-standalone.ts cmdStop.
12644
+ session.intentionalStopAt = Date.now();
12645
+
12489
12646
  // An explicit stop is a human decision \u2014 never undone by a pending auto-retry
12490
12647
  // respawning the CLI minutes later. Cancel the timer AND clear the streak so
12491
12648
  // an in-flight result line can't re-arm one either.
@@ -12522,7 +12679,7 @@ function cmdStop(ws, id, cmd) {
12522
12679
 
12523
12680
  // \u2500\u2500 Status \u2500\u2500
12524
12681
  function cmdStatus(ws, id, cmd) {
12525
- const { sid } = cmd;
12682
+ const { sid, includeArgs } = cmd;
12526
12683
  if (!sid) return sendError(ws, id, 'status: missing sid');
12527
12684
 
12528
12685
  const session = sessions.get(sid);
@@ -12553,6 +12710,11 @@ function cmdStatus(ws, id, cmd) {
12553
12710
  exitCode: session.exitCode,
12554
12711
  exitReason: session.exitReason,
12555
12712
  pendingCtrl: session.pendingCtrl,
12713
+ // Opt-in: the argv the live process was launched with (a fork copies it to
12714
+ // reproduce the parent's prompt-cache prefix). Off by default because the
12715
+ // health monitor polls status for every session and the argv carries the
12716
+ // multi-KB --append-system-prompt.
12717
+ ...(includeArgs && session.args && session.args.length > 0 ? { args: session.args } : {}),
12556
12718
  });
12557
12719
  }
12558
12720
 
@@ -14605,7 +14767,7 @@ function scanIdleSessions() {
14605
14767
  // end" for any session whose last line isn't a type:result \u2014 and the
14606
14768
  // projection then shows a red Error for our own routine reclamation.
14607
14769
  // Keep in sync with daemon-standalone.ts scanIdleSessions.
14608
- session.idleReclaimAt = now;
14770
+ session.intentionalStopAt = now;
14609
14771
  killSessionProcessGroup(pid, sid);
14610
14772
  }
14611
14773
  }
@@ -14986,7 +15148,7 @@ if (action === '--start') {
14986
15148
  });
14987
15149
 
14988
15150
  // src/core/jsonl-tailer.ts
14989
- import fs14 from "fs";
15151
+ import fs15 from "fs";
14990
15152
  var init_jsonl_tailer = __esm({
14991
15153
  "src/core/jsonl-tailer.ts"() {
14992
15154
  "use strict";
@@ -14995,10 +15157,10 @@ var init_jsonl_tailer = __esm({
14995
15157
  });
14996
15158
 
14997
15159
  // src/providers/session-io.ts
14998
- import fs15 from "fs";
15160
+ import fs16 from "fs";
14999
15161
  import fsp8 from "fs/promises";
15000
- import path16 from "path";
15001
- import { execFileSync } from "child_process";
15162
+ import path17 from "path";
15163
+ import { execFileSync as execFileSync2 } from "child_process";
15002
15164
  function shellQuote(s4) {
15003
15165
  return "'" + s4.replace(/'/g, "'\\''") + "'";
15004
15166
  }
@@ -15043,7 +15205,7 @@ function findRelativeImageNames(text, opts) {
15043
15205
  function findLocalImagePaths(text) {
15044
15206
  return findImagePaths(text).filter((p6) => {
15045
15207
  try {
15046
- return fs15.statSync(p6).isFile();
15208
+ return fs16.statSync(p6).isFile();
15047
15209
  } catch {
15048
15210
  return false;
15049
15211
  }
@@ -15289,19 +15451,19 @@ function createDaemonCore(deps) {
15289
15451
  jsonlAgeMs = clock() - fs121.statSync(session2.jsonlPath).mtimeMs;
15290
15452
  } catch {
15291
15453
  }
15292
- const intentionalReclaim = session2.idleReclaimAt != null;
15293
- if (intentionalReclaim && code !== 0) {
15294
- logger2("info", "reapSession: intentional idle reclamation, normalizing exit code", {
15454
+ const intentionalStop = session2.intentionalStopAt != null;
15455
+ if (intentionalStop && code !== 0) {
15456
+ logger2("info", "reapSession: intentional stop, normalizing exit code", {
15295
15457
  sid,
15296
15458
  pid: session2.pid,
15297
15459
  originalCode: code,
15298
15460
  originalReason: reason,
15299
15461
  jsonlAgeMs,
15300
15462
  cleanExit,
15301
- idleReclaimAt: session2.idleReclaimAt
15463
+ intentionalStopAt: session2.intentionalStopAt
15302
15464
  });
15303
15465
  code = 0;
15304
- reason = reason + "+intentional-idle-reclaim";
15466
+ reason = reason + "+intentional-stop";
15305
15467
  } else if (cleanExit && code !== 0) {
15306
15468
  logger2("info", "reapSession: turn-complete detected, normalizing exit code", {
15307
15469
  sid,
@@ -16088,83 +16250,6 @@ var init_daemon_core = __esm({
16088
16250
  }
16089
16251
  });
16090
16252
 
16091
- // src/core/claude-cli-detect.ts
16092
- var claude_cli_detect_exports = {};
16093
- __export(claude_cli_detect_exports, {
16094
- _CLAUDE_CREDENTIALS_HOME: () => _CLAUDE_CREDENTIALS_HOME,
16095
- detectClaudeCli: () => detectClaudeCli,
16096
- hasClaudeSubscriptionAuth: () => hasClaudeSubscriptionAuth,
16097
- isClaudeCliInstalled: () => isClaudeCliInstalled,
16098
- resolveClaudeCliExecutable: () => resolveClaudeCliExecutable
16099
- });
16100
- import { execFileSync as execFileSync2 } from "child_process";
16101
- import fs16 from "fs";
16102
- import os4 from "os";
16103
- import path17 from "path";
16104
- function resolveClaudeCliExecutable(env3 = process.env) {
16105
- const home = env3.HOME || os4.homedir();
16106
- const pathDirs = (env3.PATH ?? "").split(path17.delimiter).filter(Boolean);
16107
- const fallbackDirs = [
16108
- path17.join(home, ".toolbox", "bin"),
16109
- path17.join(home, ".local", "bin"),
16110
- path17.join(home, ".npm-global", "bin"),
16111
- path17.join(home, ".bun", "bin"),
16112
- "/usr/local/bin",
16113
- "/opt/homebrew/bin",
16114
- "/usr/bin",
16115
- "/bin"
16116
- ];
16117
- for (const dir2 of [.../* @__PURE__ */ new Set([...pathDirs, ...fallbackDirs])]) {
16118
- const candidate = path17.join(dir2, "claude");
16119
- try {
16120
- fs16.accessSync(candidate, fs16.constants.X_OK);
16121
- return candidate;
16122
- } catch {
16123
- }
16124
- }
16125
- return null;
16126
- }
16127
- function isClaudeCliInstalled() {
16128
- return resolveClaudeCliExecutable() !== null;
16129
- }
16130
- function hasClaudeSubscriptionAuth() {
16131
- if (process.platform === "darwin") {
16132
- try {
16133
- execFileSync2("security", ["find-generic-password", "-s", KEYCHAIN_OAUTH_SERVICE], {
16134
- stdio: "ignore"
16135
- });
16136
- return true;
16137
- } catch {
16138
- }
16139
- }
16140
- return fileCredentialExists();
16141
- }
16142
- function fileCredentialExists() {
16143
- try {
16144
- return fs16.existsSync(CLAUDE_CREDENTIALS_FILE);
16145
- } catch {
16146
- return false;
16147
- }
16148
- }
16149
- function detectClaudeCli() {
16150
- const installed2 = isClaudeCliInstalled();
16151
- const subscriptionAuth = installed2 && hasClaudeSubscriptionAuth();
16152
- return {
16153
- installed: installed2,
16154
- subscriptionAuth,
16155
- subscriptionReady: installed2 && subscriptionAuth
16156
- };
16157
- }
16158
- var KEYCHAIN_OAUTH_SERVICE, _CLAUDE_CREDENTIALS_HOME;
16159
- var init_claude_cli_detect = __esm({
16160
- "src/core/claude-cli-detect.ts"() {
16161
- "use strict";
16162
- init_constants();
16163
- KEYCHAIN_OAUTH_SERVICE = "Claude Code-credentials";
16164
- _CLAUDE_CREDENTIALS_HOME = os4.homedir();
16165
- }
16166
- });
16167
-
16168
16253
  // src/providers/daemon-ownership.ts
16169
16254
  import fs17 from "fs";
16170
16255
  import os5 from "os";
@@ -23524,7 +23609,7 @@ __export(credential_resolver_exports, {
23524
23609
  DEFAULT_REGION: () => DEFAULT_REGION,
23525
23610
  hasResolvableCredential: () => hasResolvableCredential,
23526
23611
  readClaudeCredentialExport: () => readClaudeCredentialExport,
23527
- readClaudeSettingsEnv: () => readClaudeSettingsEnv,
23612
+ readClaudeSettingsEnv: () => readClaudeSettingsEnv2,
23528
23613
  resolveCredentialFrom: () => resolveCredentialFrom,
23529
23614
  resolveCredentials: () => resolveCredentials,
23530
23615
  traceCredentialResolution: () => traceCredentialResolution
@@ -23679,7 +23764,7 @@ function traceCredentialResolution(inputs) {
23679
23764
  region: { value: winner.region ?? DEFAULT_REGION, source: regionSource }
23680
23765
  };
23681
23766
  }
23682
- function readClaudeSettingsEnv(settingsFile = CLAUDE_SETTINGS_FILE) {
23767
+ function readClaudeSettingsEnv2(settingsFile = CLAUDE_SETTINGS_FILE) {
23683
23768
  try {
23684
23769
  const raw = fs30.readFileSync(settingsFile, "utf-8");
23685
23770
  const parsed = JSON.parse(raw);
@@ -23719,7 +23804,7 @@ function probeAwsFiles() {
23719
23804
  function resolveCredentials(config) {
23720
23805
  const result = resolveCredentialFrom({
23721
23806
  config,
23722
- claudeEnv: readClaudeSettingsEnv(),
23807
+ claudeEnv: readClaudeSettingsEnv2(),
23723
23808
  processEnv: process.env,
23724
23809
  awsFiles: probeAwsFiles(),
23725
23810
  claudeCredentialExport: readClaudeCredentialExport()
@@ -66501,10 +66586,9 @@ function buildArgs(opts) {
66501
66586
  // Input stays default (text): we write the whole prompt to stdin and close.
66502
66587
  // (stream-json INPUT would force us into a JSON envelope for no gain here.)
66503
66588
  "--tools",
66504
- "",
66589
+ ""
66505
66590
  // disable ALL built-in tools (deny-all)
66506
- "--settings",
66507
- FORCE_SUBSCRIPTION_SETTINGS
66591
+ // No --settings override: the user's settings.json (and so their auth) applies as-is.
66508
66592
  ];
66509
66593
  let systemText = systemToText(opts.system).trim();
66510
66594
  if (opts.tools && opts.tools.length > 0) {
@@ -66533,7 +66617,7 @@ function systemToText(system) {
66533
66617
  }
66534
66618
  function buildSpawnEnv(base = process.env) {
66535
66619
  const env3 = { ...base };
66536
- for (const key2 of AUTH_ENV_TO_STRIP) {
66620
+ for (const key2 of ENV_TO_STRIP) {
66537
66621
  delete env3[key2];
66538
66622
  }
66539
66623
  env3.CLAUDE_CODE_ENTRYPOINT = WALNUT_UTILITY_ENTRYPOINT;
@@ -66595,7 +66679,7 @@ function normalizeCliModel(model) {
66595
66679
  }
66596
66680
  return void 0;
66597
66681
  }
66598
- var AUTH_ENV_TO_STRIP, FORCE_SUBSCRIPTION_SETTINGS, CLI_TURN_TIMEOUT_MS, SESSION_REUSE_TTL_MS, MAX_TRACKED_SESSIONS, ClaudeCliAdapter;
66682
+ var ENV_TO_STRIP, MAX_CONCURRENT_CLI, CLI_TURN_TIMEOUT_MS, SESSION_REUSE_TTL_MS, MAX_TRACKED_SESSIONS, ClaudeCliAdapter;
66599
66683
  var init_adapter_claude_cli = __esm({
66600
66684
  "src/agent/providers/adapter-claude-cli.ts"() {
66601
66685
  "use strict";
@@ -66605,24 +66689,11 @@ var init_adapter_claude_cli = __esm({
66605
66689
  init_logging();
66606
66690
  init_claude_cli_detect();
66607
66691
  init_claude_cli_protocol();
66608
- AUTH_ENV_TO_STRIP = [
66609
- "ANTHROPIC_API_KEY",
66610
- "ANTHROPIC_AUTH_TOKEN",
66611
- "ANTHROPIC_BASE_URL",
66612
- "AWS_BEARER_TOKEN_BEDROCK",
66613
- "AWS_ACCESS_KEY_ID",
66614
- "AWS_SECRET_ACCESS_KEY",
66615
- "AWS_SESSION_TOKEN",
66616
- "AWS_PROFILE",
66617
- "CLAUDE_CODE_USE_BEDROCK",
66618
- "CLAUDE_CODE_USE_VERTEX",
66619
- // Nested-session marker — inline-subagent strips this too so the child doesn't
66620
- // think it's running inside another Claude session.
66621
- "CLAUDECODE"
66622
- ];
66623
- FORCE_SUBSCRIPTION_SETTINGS = JSON.stringify({
66624
- env: { CLAUDE_CODE_USE_BEDROCK: "", CLAUDE_CODE_USE_VERTEX: "" }
66625
- });
66692
+ ENV_TO_STRIP = ["CLAUDECODE"];
66693
+ MAX_CONCURRENT_CLI = (() => {
66694
+ const n6 = Number(process.env.WALNUT_CLAUDE_CLI_CONCURRENCY);
66695
+ return Number.isFinite(n6) && n6 > 0 ? Math.floor(n6) : 3;
66696
+ })();
66626
66697
  CLI_TURN_TIMEOUT_MS = 12e4;
66627
66698
  SESSION_REUSE_TTL_MS = 30 * 60 * 1e3;
66628
66699
  MAX_TRACKED_SESSIONS = 50;
@@ -66630,6 +66701,25 @@ var init_adapter_claude_cli = __esm({
66630
66701
  protocol = "claude-cli";
66631
66702
  /** conversationKey → CLI session for --resume chaining. In-process only. */
66632
66703
  sessions = /* @__PURE__ */ new Map();
66704
+ /** Process gate: MAX_CONCURRENT_CLI turns at once, the rest queue in order. */
66705
+ inFlight = 0;
66706
+ waiters = [];
66707
+ async withSlot(fn) {
66708
+ if (this.inFlight >= MAX_CONCURRENT_CLI) {
66709
+ await new Promise((resolve) => this.waiters.push(resolve));
66710
+ }
66711
+ this.inFlight++;
66712
+ try {
66713
+ return await fn();
66714
+ } finally {
66715
+ this.inFlight--;
66716
+ this.waiters.shift()?.();
66717
+ }
66718
+ }
66719
+ /** Test hook: how many turns are running / waiting right now. */
66720
+ _gateStateForTesting() {
66721
+ return { inFlight: this.inFlight, waiting: this.waiters.length };
66722
+ }
66633
66723
  resetClient() {
66634
66724
  this.sessions.clear();
66635
66725
  }
@@ -66707,7 +66797,7 @@ User: ${overridePrompt}` : "");
66707
66797
  }
66708
66798
  if (!prompt.trim()) prompt = "(continue)";
66709
66799
  const args = [...buildArgs(opts), session2.flag, session2.id];
66710
- return this.execCli(command6, args, env3, prompt, opts.signal, onTextDelta).then((result) => {
66800
+ return this.withSlot(() => this.execCli(command6, args, env3, prompt, opts.signal, onTextDelta)).then((result) => {
66711
66801
  this.sessions.set(key2, {
66712
66802
  sessionId: session2.id,
66713
66803
  seenMessages: opts.messages.length,
@@ -66726,7 +66816,7 @@ User: ${overridePrompt}` : "");
66726
66816
  User: ${overridePrompt}` : "");
66727
66817
  const freshId = randomUUID3();
66728
66818
  const freshArgs = [...buildArgs(opts), "--session-id", freshId];
66729
- return this.execCli(command6, freshArgs, env3, freshPrompt || "(continue)", opts.signal, onTextDelta).then((result) => {
66819
+ return this.withSlot(() => this.execCli(command6, freshArgs, env3, freshPrompt || "(continue)", opts.signal, onTextDelta)).then((result) => {
66730
66820
  this.sessions.set(key2, {
66731
66821
  sessionId: freshId,
66732
66822
  seenMessages: opts.messages.length,
@@ -66810,7 +66900,7 @@ User: ${overridePrompt}` : "");
66810
66900
  }
66811
66901
  const resultText = finalResult?.result || accumulatedText;
66812
66902
  if (resultText && resultText.length < 300 && /^\s*failed to authenticate|^\s*api error: 4\d\d/i.test(resultText)) {
66813
- reject(new Error(`claude-cli subscription auth failed \u2014 run \`claude login\` to re-authenticate. CLI said: ${resultText.slice(0, 200)}`));
66903
+ 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)}`));
66814
66904
  return;
66815
66905
  }
66816
66906
  if (code !== 0 && !accumulatedText && !finalResult) {
@@ -67044,7 +67134,7 @@ function getContextThreshold(model, percent) {
67044
67134
  }
67045
67135
  async function resolveForCall(config) {
67046
67136
  const fullConfig = await getConfig();
67047
- const providerName = config?.provider ?? fullConfig.agent?.main_provider ?? "bedrock";
67137
+ const providerName = config?.provider ?? resolveMainProviderName(fullConfig);
67048
67138
  const hasExplicitProviders = fullConfig.providers && Object.keys(fullConfig.providers).length > 0;
67049
67139
  let providers = hasExplicitProviders ? buildProviderMap(fullConfig.providers, fullConfig) : synthesizeFromLegacy(fullConfig);
67050
67140
  const resolved = resolveProvider(providerName, providers);
@@ -67114,6 +67204,7 @@ var init_model = __esm({
67114
67204
  "use strict";
67115
67205
  init_config_manager();
67116
67206
  init_registry2();
67207
+ init_default_provider();
67117
67208
  init_defaults();
67118
67209
  init_model_catalog();
67119
67210
  }
@@ -68554,7 +68645,7 @@ __export(cheap_model_exports, {
68554
68645
  });
68555
68646
  function fastModelFor(config) {
68556
68647
  if (config.agent?.fast_model) return config.agent.fast_model;
68557
- const providerName = config.agent?.main_provider ?? "bedrock";
68648
+ const providerName = resolveMainProviderName(config);
68558
68649
  return MODEL_CATALOG[providerName]?.find(
68559
68650
  (model) => model.id.toLowerCase().includes("haiku")
68560
68651
  )?.id;
@@ -68566,6 +68657,7 @@ var init_cheap_model = __esm({
68566
68657
  "src/core/cheap-model.ts"() {
68567
68658
  "use strict";
68568
68659
  init_model_catalog();
68660
+ init_default_provider();
68569
68661
  }
68570
68662
  });
68571
68663
 
@@ -70385,7 +70477,20 @@ async function applySnapshot(sessionId, snapshot2, source) {
70385
70477
  if (projected !== "error") {
70386
70478
  updates.errorMessage = void 0;
70387
70479
  updates.errorKind = void 0;
70388
- clearSuppressedErrorReason(sessionId);
70480
+ const stashedStop = projected === "stopped" ? takeSuppressedErrorReason(sessionId) : null;
70481
+ if (stashedStop?.message) {
70482
+ updates.errorMessage = stashedStop.message;
70483
+ if (stashedStop.kind) updates.errorKind = stashedStop.kind;
70484
+ log.session.info("snapshot projection adopted suppressed stop reason", {
70485
+ sessionId,
70486
+ suppressedReason: stashedStop.reason,
70487
+ kind: stashedStop.kind ?? null,
70488
+ projected,
70489
+ source
70490
+ });
70491
+ } else {
70492
+ clearSuppressedErrorReason(sessionId);
70493
+ }
70389
70494
  } else {
70390
70495
  let droppedDisproven = false;
70391
70496
  if (assertsHostUnreachable(record)) {
@@ -74459,6 +74564,26 @@ var init_transcript_chain = __esm({
74459
74564
  }
74460
74565
  });
74461
74566
 
74567
+ // src/core/sessions/side-thread-warmup.ts
74568
+ function isCacheWarmupText(text) {
74569
+ return text.startsWith(CACHE_WARMUP_TAG);
74570
+ }
74571
+ function markWarmupTurnPending(sessionId) {
74572
+ pendingWarmupTurns.add(sessionId);
74573
+ }
74574
+ function consumeWarmupTurn(sessionId) {
74575
+ return pendingWarmupTurns.delete(sessionId);
74576
+ }
74577
+ var CACHE_WARMUP_TAG, CACHE_WARMUP_MESSAGE, pendingWarmupTurns;
74578
+ var init_side_thread_warmup = __esm({
74579
+ "src/core/sessions/side-thread-warmup.ts"() {
74580
+ "use strict";
74581
+ CACHE_WARMUP_TAG = "<walnut-cache-warmup>";
74582
+ 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>`;
74583
+ pendingWarmupTurns = /* @__PURE__ */ new Set();
74584
+ }
74585
+ });
74586
+
74462
74587
  // src/core/history-disk-cache.ts
74463
74588
  var history_disk_cache_exports = {};
74464
74589
  __export(history_disk_cache_exports, {
@@ -76619,6 +76744,7 @@ __export(session_lifecycle_exports, {
76619
76744
  retrySession: () => retrySession,
76620
76745
  terminateSession: () => terminateSession
76621
76746
  });
76747
+ import { randomUUID as randomUUID4 } from "crypto";
76622
76748
  async function readProviderSessionHistory(sessionId, record, nativeHost, skipSubagents = true, opts) {
76623
76749
  if (record && engineCaps(record.engine).historySource === "acp-journal") {
76624
76750
  const { readAcpSessionHistoryState: readAcpSessionHistoryState2 } = await Promise.resolve().then(() => (init_acp_session_history(), acp_session_history_exports));
@@ -76729,6 +76855,33 @@ async function changeSessionMode(existingRecord, sessionId, mode, updates = {})
76729
76855
  });
76730
76856
  return updated;
76731
76857
  }
76858
+ function normalizePinQuote(value) {
76859
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
76860
+ throw new SessionControlError("pinned_messages[].quote must be an object", 400);
76861
+ }
76862
+ const quote = value;
76863
+ const exact = quote.exact;
76864
+ if (typeof exact !== "string" || !exact.trim() || exact.length > MAX_PIN_QUOTE_CHARS) {
76865
+ throw new SessionControlError(
76866
+ `pinned_messages[].quote.exact must be a non-empty string (max ${MAX_PIN_QUOTE_CHARS} chars)`,
76867
+ 400
76868
+ );
76869
+ }
76870
+ for (const side of ["prefix", "suffix"]) {
76871
+ const context = quote[side];
76872
+ if (context !== void 0 && context !== null && (typeof context !== "string" || context.length > MAX_PIN_QUOTE_CONTEXT_CHARS)) {
76873
+ throw new SessionControlError(
76874
+ `pinned_messages[].quote.${side} must be a string (max ${MAX_PIN_QUOTE_CONTEXT_CHARS} chars)`,
76875
+ 400
76876
+ );
76877
+ }
76878
+ }
76879
+ return {
76880
+ exact,
76881
+ ...typeof quote.prefix === "string" && quote.prefix ? { prefix: quote.prefix } : {},
76882
+ ...typeof quote.suffix === "string" && quote.suffix ? { suffix: quote.suffix } : {}
76883
+ };
76884
+ }
76732
76885
  function normalizePinnedMessages(value) {
76733
76886
  if (!Array.isArray(value)) {
76734
76887
  throw new SessionControlError("pinned_messages must be an array", 400);
@@ -76737,7 +76890,9 @@ function normalizePinnedMessages(value) {
76737
76890
  throw new SessionControlError(`pinned_messages holds at most ${MAX_PINNED_MESSAGES} entries`, 400);
76738
76891
  }
76739
76892
  const out = [];
76740
- const seen = /* @__PURE__ */ new Set();
76893
+ const seenWholeMessage = /* @__PURE__ */ new Set();
76894
+ const seenQuoteId = /* @__PURE__ */ new Set();
76895
+ const seenQuoteShape = /* @__PURE__ */ new Set();
76741
76896
  for (const raw of value) {
76742
76897
  if (!raw || typeof raw !== "object") {
76743
76898
  throw new SessionControlError("each pinned_messages entry must be an object", 400);
@@ -76747,13 +76902,40 @@ function normalizePinnedMessages(value) {
76747
76902
  if (typeof msgId !== "string" || !msgId.trim() || msgId.length > MAX_PIN_ID_CHARS) {
76748
76903
  throw new SessionControlError("pinned_messages[].msgId must be a non-empty string", 400);
76749
76904
  }
76750
- if (seen.has(msgId)) continue;
76751
- seen.add(msgId);
76905
+ const rawId = entry.id;
76906
+ if (rawId !== void 0 && rawId !== null && (typeof rawId !== "string" || !rawId.trim() || rawId.length > MAX_PIN_ENTRY_ID_CHARS)) {
76907
+ throw new SessionControlError(
76908
+ `pinned_messages[].id must be a non-empty string (max ${MAX_PIN_ENTRY_ID_CHARS} chars)`,
76909
+ 400
76910
+ );
76911
+ }
76912
+ const id = typeof rawId === "string" ? rawId : void 0;
76913
+ const quote = entry.quote === void 0 || entry.quote === null ? void 0 : normalizePinQuote(entry.quote);
76914
+ let pinId = id;
76915
+ if (quote) {
76916
+ if (id && seenQuoteId.has(id)) continue;
76917
+ const shape = [msgId, quote.exact, quote.prefix ?? "", quote.suffix ?? ""].join("\0");
76918
+ if (seenQuoteShape.has(shape)) continue;
76919
+ pinId = id ?? randomUUID4();
76920
+ seenQuoteId.add(pinId);
76921
+ seenQuoteShape.add(shape);
76922
+ } else {
76923
+ if (seenWholeMessage.has(msgId)) continue;
76924
+ seenWholeMessage.add(msgId);
76925
+ }
76752
76926
  const role = entry.role === "user" || entry.role === "assistant" || entry.role === "system" ? entry.role : "assistant";
76753
76927
  const label = typeof entry.label === "string" ? entry.label.trim().slice(0, MAX_PIN_LABEL_CHARS) : "";
76754
76928
  const timestamp = typeof entry.timestamp === "string" ? entry.timestamp : void 0;
76755
76929
  const pinnedAt = typeof entry.pinnedAt === "string" ? entry.pinnedAt : (/* @__PURE__ */ new Date()).toISOString();
76756
- out.push({ msgId, label, role, ...timestamp ? { timestamp } : {}, pinnedAt });
76930
+ out.push({
76931
+ msgId,
76932
+ label,
76933
+ role,
76934
+ ...timestamp ? { timestamp } : {},
76935
+ pinnedAt,
76936
+ ...pinId ? { id: pinId } : {},
76937
+ ...quote ? { quote } : {}
76938
+ });
76757
76939
  }
76758
76940
  return out;
76759
76941
  }
@@ -77412,7 +77594,7 @@ async function readSessionRichHistory(sessionId, tail) {
77412
77594
  ...historyWindowed ? { windowed: true } : {}
77413
77595
  };
77414
77596
  }
77415
- var CLAUDE_SESSION_MODES, MAX_PINNED_MESSAGES, MAX_PIN_LABEL_CHARS, MAX_PIN_ID_CHARS, RECHECK_RPC_TIMEOUT_MS, RECHECK_TIMED_OUT, GIT_BASES, MAX_FORK_DEPTH, HISTORY_STARTUP_GRACE_MS;
77597
+ 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;
77416
77598
  var init_session_lifecycle = __esm({
77417
77599
  "src/core/sessions/session-lifecycle.ts"() {
77418
77600
  "use strict";
@@ -77426,6 +77608,9 @@ var init_session_lifecycle = __esm({
77426
77608
  MAX_PINNED_MESSAGES = 200;
77427
77609
  MAX_PIN_LABEL_CHARS = 300;
77428
77610
  MAX_PIN_ID_CHARS = 200;
77611
+ MAX_PIN_ENTRY_ID_CHARS = 64;
77612
+ MAX_PIN_QUOTE_CHARS = 2e3;
77613
+ MAX_PIN_QUOTE_CONTEXT_CHARS = 64;
77429
77614
  RECHECK_RPC_TIMEOUT_MS = 5e3;
77430
77615
  RECHECK_TIMED_OUT = /* @__PURE__ */ Symbol("recheck-timeout");
77431
77616
  GIT_BASES = /* @__PURE__ */ new Set(["uncommitted", "previous", "remote"]);
@@ -77728,7 +77913,7 @@ __export(compact_inject_exports, {
77728
77913
  extractSessionMeta: () => extractSessionMeta,
77729
77914
  injectCompactBoundary: () => injectCompactBoundary
77730
77915
  });
77731
- import { randomUUID as randomUUID4 } from "crypto";
77916
+ import { randomUUID as randomUUID5 } from "crypto";
77732
77917
  import fsp26 from "fs/promises";
77733
77918
  async function extractSessionMeta(jsonlPath) {
77734
77919
  const content = await fsp26.readFile(jsonlPath, "utf-8");
@@ -77780,8 +77965,8 @@ async function injectCompactBoundary(jsonlPath, summary, meta) {
77780
77965
  log.session.error("Could not find last message UUID in JSONL", { jsonlPath });
77781
77966
  return null;
77782
77967
  }
77783
- const boundaryUuid = randomUUID4();
77784
- const summaryUuid = randomUUID4();
77968
+ const boundaryUuid = randomUUID5();
77969
+ const summaryUuid = randomUUID5();
77785
77970
  const now = /* @__PURE__ */ new Date();
77786
77971
  const boundaryTs = now.toISOString();
77787
77972
  const summaryTs = new Date(now.getTime() + 1e3).toISOString();
@@ -88565,6 +88750,82 @@ var init_task = __esm({
88565
88750
  }
88566
88751
  });
88567
88752
 
88753
+ // src/core/sessions/spawn-prefix.ts
88754
+ var spawn_prefix_exports = {};
88755
+ __export(spawn_prefix_exports, {
88756
+ MAX_SPAWN_PROMPT_BYTES: () => MAX_SPAWN_PROMPT_BYTES,
88757
+ parseSpawnPrefixFromArgs: () => parseSpawnPrefixFromArgs,
88758
+ readParentSpawnPrefix: () => readParentSpawnPrefix,
88759
+ spawnPrefixFromRecord: () => spawnPrefixFromRecord
88760
+ });
88761
+ function flagValue(args, flag) {
88762
+ const i8 = args.indexOf(flag);
88763
+ if (i8 < 0 || i8 + 1 >= args.length) return void 0;
88764
+ const v3 = args[i8 + 1];
88765
+ return v3 !== void 0 && !v3.startsWith("--") ? v3 : void 0;
88766
+ }
88767
+ function parseSpawnPrefixFromArgs(args) {
88768
+ const prompt = flagValue(args, "--append-system-prompt");
88769
+ return {
88770
+ appendSystemPrompt: prompt !== void 0 && Buffer.byteLength(prompt, "utf8") <= MAX_SPAWN_PROMPT_BYTES ? prompt : null,
88771
+ ...flagValue(args, "--model") ? { model: flagValue(args, "--model") } : {},
88772
+ ...EFFORTS.includes(flagValue(args, "--effort") ?? "") ? { effort: flagValue(args, "--effort") } : {},
88773
+ ...flagValue(args, "--permission-mode") ? { permissionMode: flagValue(args, "--permission-mode") } : {},
88774
+ source: "live-process"
88775
+ };
88776
+ }
88777
+ function spawnPrefixFromRecord(record) {
88778
+ const stored = record.appliedAppendSystemPrompt;
88779
+ if (stored === void 0 || stored === null) return { appendSystemPrompt: null, source: "unknown" };
88780
+ const usable = stored.length > 0 && Buffer.byteLength(stored, "utf8") <= MAX_SPAWN_PROMPT_BYTES;
88781
+ return { appendSystemPrompt: usable ? stored : null, source: "record" };
88782
+ }
88783
+ async function defaultLiveArgsReader(hostKey2, sessionId) {
88784
+ const { probeDaemonSessionArgs: probeDaemonSessionArgs2 } = await Promise.resolve().then(() => (init_daemon_connection(), daemon_connection_exports));
88785
+ return probeDaemonSessionArgs2(hostKey2, sessionId);
88786
+ }
88787
+ async function readParentSpawnPrefix(parent, deps = {}) {
88788
+ const readLiveArgs = deps.readLiveArgs ?? defaultLiveArgsReader;
88789
+ const hostKey2 = parent.host || "__local__";
88790
+ let live = null;
88791
+ try {
88792
+ live = await readLiveArgs(hostKey2, parent.claudeSessionId);
88793
+ } catch (err2) {
88794
+ log.session.debug("spawn prefix: live argv read threw", {
88795
+ sessionId: parent.claudeSessionId,
88796
+ error: err2 instanceof Error ? err2.message : String(err2)
88797
+ });
88798
+ }
88799
+ if (live && live.length > 0) {
88800
+ const prefix = parseSpawnPrefixFromArgs(live);
88801
+ const remembered = parent.appliedAppendSystemPrompt ?? void 0;
88802
+ const actual = prefix.appendSystemPrompt ?? "";
88803
+ if (remembered !== actual) {
88804
+ const persist4 = deps.persist ?? (async (sid, prompt) => {
88805
+ const { updateSessionRecord: updateSessionRecord2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
88806
+ return updateSessionRecord2(sid, { appliedAppendSystemPrompt: prompt });
88807
+ });
88808
+ void persist4(parent.claudeSessionId, actual).catch((err2) => {
88809
+ log.session.warn("spawn prefix: backfill of applied prompt failed", {
88810
+ sessionId: parent.claudeSessionId,
88811
+ error: err2 instanceof Error ? err2.message : String(err2)
88812
+ });
88813
+ });
88814
+ }
88815
+ return prefix;
88816
+ }
88817
+ return spawnPrefixFromRecord(parent);
88818
+ }
88819
+ var EFFORTS, MAX_SPAWN_PROMPT_BYTES;
88820
+ var init_spawn_prefix = __esm({
88821
+ "src/core/sessions/spawn-prefix.ts"() {
88822
+ "use strict";
88823
+ init_logging();
88824
+ EFFORTS = ["low", "medium", "high", "xhigh", "max"];
88825
+ MAX_SPAWN_PROMPT_BYTES = 65536;
88826
+ }
88827
+ });
88828
+
88568
88829
  // src/core/sessions/side-thread-fork.ts
88569
88830
  var side_thread_fork_exports = {};
88570
88831
  __export(side_thread_fork_exports, {
@@ -88621,6 +88882,10 @@ async function forkSideThreadSession(parentSid, threadId, opts) {
88621
88882
  const sessionId = crypto16.randomUUID();
88622
88883
  const lane = sideThreadLaneKey(parentSid, threadId);
88623
88884
  const mode = parent.mode !== "default" ? parent.mode : void 0;
88885
+ const { readParentSpawnPrefix: readParentSpawnPrefix2 } = await Promise.resolve().then(() => (init_spawn_prefix(), spawn_prefix_exports));
88886
+ const prefix = await readParentSpawnPrefix2(parent);
88887
+ const cliModel = prefix.model ?? parent.cliModel;
88888
+ const effort = prefix.effort ?? parent.effort;
88624
88889
  await createSessionRecord(sessionId, "", parent.project ?? "", parent.cwd, {
88625
88890
  title,
88626
88891
  ...mode ? { mode } : {},
@@ -88628,8 +88893,10 @@ async function forkSideThreadSession(parentSid, threadId, opts) {
88628
88893
  // The thread inherits the parent's launch bundle verbatim, re-applied from
88629
88894
  // the record on every cold resume.
88630
88895
  ...parent.profile ? { profile: parent.profile } : {},
88631
- ...parent.effort ? { effort: parent.effort } : {},
88632
- ...parent.cliModel ? { cliModel: parent.cliModel } : {},
88896
+ ...effort ? { effort } : {},
88897
+ ...cliModel ? { cliModel } : {},
88898
+ // '' = the parent runs without an append prompt, and so does the thread.
88899
+ appliedAppendSystemPrompt: prefix.appendSystemPrompt ?? "",
88633
88900
  lane,
88634
88901
  forkedFromSessionId: resumeFrom,
88635
88902
  initialProcessStatus: "idle",
@@ -88645,11 +88912,12 @@ async function forkSideThreadSession(parentSid, threadId, opts) {
88645
88912
  ...mode ? { mode } : {},
88646
88913
  ...parent.host ? { host: parent.host } : {},
88647
88914
  ...parent.profile ? { profile: parent.profile } : {},
88648
- ...parent.effort ? { effort: parent.effort } : {},
88915
+ ...effort ? { effort } : {},
88649
88916
  // Must be `cliModel` (the parent's verbatim --model arg, [1m] marker
88650
88917
  // included): the CLI does NOT inherit a model across --fork-session, and
88651
88918
  // the reported `model` has lost the marker.
88652
- ...parent.cliModel ? { model: parent.cliModel } : {},
88919
+ ...cliModel ? { model: cliModel } : {},
88920
+ appendSystemPromptExact: prefix.appendSystemPrompt,
88653
88921
  lane,
88654
88922
  forkedFromSessionId: resumeFrom
88655
88923
  }, ["session-runner"], { source: "side-thread-fork" });
@@ -88658,7 +88926,9 @@ async function forkSideThreadSession(parentSid, threadId, opts) {
88658
88926
  threadId,
88659
88927
  sessionId,
88660
88928
  resumeFrom,
88661
- initOnly: !opts?.message
88929
+ initOnly: !opts?.message,
88930
+ prefixSource: prefix.source,
88931
+ prefixPromptLength: prefix.appendSystemPrompt?.length ?? 0
88662
88932
  });
88663
88933
  return { sessionId, resumeFromSessionId: resumeFrom };
88664
88934
  }
@@ -100914,14 +101184,16 @@ function isLive(r8) {
100914
101184
  function isRevivable(r8) {
100915
101185
  return !!r8.outputFile || r8.consumedOffset !== void 0;
100916
101186
  }
100917
- var STANDBY_TTL_MS, MAX_LIVE_THREADS, IDLE_SWEEP_INTERVAL_MS, THREAD_IDLE_MS, SideThreadManager, sideThreadManager;
101187
+ var STANDBY_TTL_MS, WARMED_STANDBY_TTL_MS, MAX_LIVE_THREADS, IDLE_SWEEP_INTERVAL_MS, THREAD_IDLE_MS, SideThreadManager, sideThreadManager;
100918
101188
  var init_side_thread_manager = __esm({
100919
101189
  "src/core/sessions/side-thread-manager.ts"() {
100920
101190
  "use strict";
100921
101191
  init_logging();
100922
101192
  init_side_thread_fork();
100923
101193
  init_session_controls();
101194
+ init_side_thread_warmup();
100924
101195
  STANDBY_TTL_MS = 12e4;
101196
+ WARMED_STANDBY_TTL_MS = 15 * 6e4;
100925
101197
  MAX_LIVE_THREADS = 3;
100926
101198
  IDLE_SWEEP_INTERVAL_MS = 5 * 6e4;
100927
101199
  THREAD_IDLE_MS = 30 * 6e4;
@@ -100932,6 +101204,8 @@ var init_side_thread_manager = __esm({
100932
101204
  * In-memory only: a server restart loses it, but the boot sweep archives
100933
101205
  * every standby anyway, so a record can never outlive its map entry. */
100934
101206
  standbyParentOffsets = /* @__PURE__ */ new Map();
101207
+ /** parentSid → standby sid that already ran its cache warm-up turn. */
101208
+ warmedStandbys = /* @__PURE__ */ new Map();
100935
101209
  /** One in-flight standby/create op per parent — two concurrent asks must not
100936
101210
  * both consume the same standby (they would collide on its lane rename). */
100937
101211
  inFlight = /* @__PURE__ */ new Map();
@@ -100963,6 +101237,7 @@ var init_side_thread_manager = __esm({
100963
101237
  for (const timer3 of this.standbyTimers.values()) clearTimeout(timer3);
100964
101238
  this.standbyTimers.clear();
100965
101239
  this.standbyParentOffsets.clear();
101240
+ this.warmedStandbys.clear();
100966
101241
  this.inFlight.clear();
100967
101242
  }
100968
101243
  // ── Standby ────────────────────────────────────────────────────────────────
@@ -100978,10 +101253,12 @@ var init_side_thread_manager = __esm({
100978
101253
  const existing = await this.findStandby(parentSid);
100979
101254
  if (existing) {
100980
101255
  if (await this.isStandbyUsable(parentSid, existing)) {
100981
- this.armStandbyTtl(parentSid, existing.claudeSessionId);
101256
+ const warmed = this.warmedStandbys.get(parentSid) === existing.claudeSessionId;
101257
+ this.armStandbyTtl(parentSid, existing.claudeSessionId, warmed ? WARMED_STANDBY_TTL_MS : STANDBY_TTL_MS);
100982
101258
  return existing.claudeSessionId;
100983
101259
  }
100984
101260
  void this.retireSession(existing.claudeSessionId, "side_thread_standby_stale");
101261
+ this.warmedStandbys.delete(parentSid);
100985
101262
  }
100986
101263
  const parentOffset = parent.consumedOffset;
100987
101264
  const { sessionId } = await forkSideThreadSession(parentSid, STANDBY_THREAD_ID);
@@ -101007,7 +101284,7 @@ var init_side_thread_manager = __esm({
101007
101284
  if (!parent) return false;
101008
101285
  return parent.consumedOffset === this.standbyParentOffsets.get(parentSid);
101009
101286
  }
101010
- armStandbyTtl(parentSid, sessionId) {
101287
+ armStandbyTtl(parentSid, sessionId, ttlMs = STANDBY_TTL_MS) {
101011
101288
  const prev = this.standbyTimers.get(parentSid);
101012
101289
  if (prev) clearTimeout(prev);
101013
101290
  const timer3 = setTimeout(() => {
@@ -101017,13 +101294,41 @@ var init_side_thread_manager = __esm({
101017
101294
  const record = await getSessionByClaudeId2(sessionId).catch(() => null);
101018
101295
  if (!record || parseSideLaneKey(record.lane)?.threadId !== STANDBY_THREAD_ID) return;
101019
101296
  this.standbyParentOffsets.delete(parentSid);
101297
+ this.warmedStandbys.delete(parentSid);
101020
101298
  await this.retireSession(sessionId, "side_thread_standby_ttl");
101021
101299
  }).catch(() => {
101022
101300
  });
101023
- }, STANDBY_TTL_MS);
101301
+ }, ttlMs);
101024
101302
  timer3.unref?.();
101025
101303
  this.standbyTimers.set(parentSid, timer3);
101026
101304
  }
101305
+ /**
101306
+ * Run the standby's cache warm-up turn (see side-thread-warmup.ts). Called
101307
+ * when the user starts TYPING a new question: the fork's first API call pays
101308
+ * the full prefix write no matter what, so paying it while they type turns
101309
+ * the actual question into a cached follow-up. Idempotent per standby; a
101310
+ * missing or stale standby is a no-op (the ask path will fork fresh anyway).
101311
+ */
101312
+ async warmStandby(parentSid) {
101313
+ return this.serialize(parentSid, async () => {
101314
+ const standby = await this.findStandby(parentSid);
101315
+ if (!standby) return { warmed: false, reason: "no_standby" };
101316
+ if (!await this.isStandbyUsable(parentSid, standby)) return { warmed: false, reason: "stale" };
101317
+ if (this.warmedStandbys.get(parentSid) === standby.claudeSessionId) {
101318
+ return { warmed: true, reason: "already_warm" };
101319
+ }
101320
+ const { sendMessageToSession: sendMessageToSession2 } = await Promise.resolve().then(() => (init_session_message_queue(), session_message_queue_exports));
101321
+ markWarmupTurnPending(standby.claudeSessionId);
101322
+ await sendMessageToSession2(standby.claudeSessionId, CACHE_WARMUP_MESSAGE, { source: "side-thread-warmup" });
101323
+ this.warmedStandbys.set(parentSid, standby.claudeSessionId);
101324
+ this.armStandbyTtl(parentSid, standby.claudeSessionId, WARMED_STANDBY_TTL_MS);
101325
+ log.session.info("side thread: standby cache warm-up sent", {
101326
+ parentSid,
101327
+ standbySid: standby.claudeSessionId
101328
+ });
101329
+ return { warmed: true };
101330
+ });
101331
+ }
101027
101332
  // ── Threads ────────────────────────────────────────────────────────────────
101028
101333
  /**
101029
101334
  * Open a side thread on `parentSid` and ask `question` in it.
@@ -101034,6 +101339,10 @@ var init_side_thread_manager = __esm({
101034
101339
  * question as its spawn's first turn instead — a send issued right after
101035
101340
  * SESSION_START can lose that race and cold-`--resume` an id the CLI has never
101036
101341
  * seen (see personal-ai-lane.ts LaneSession.created).
101342
+ *
101343
+ * `imageContext` (attachment paths for the CLI to Read) rides the MESSAGE only.
101344
+ * The stored entry keeps the plain question, so the chip label, the injected
101345
+ * transcript and a promoted task never carry the machine preamble.
101037
101346
  */
101038
101347
  async createThread(parentSid, input) {
101039
101348
  const question = input.question?.trim();
@@ -101041,8 +101350,11 @@ var init_side_thread_manager = __esm({
101041
101350
  return this.serialize(parentSid, async () => {
101042
101351
  const threadId = mintSideThreadId();
101043
101352
  const title = input.title?.trim() || void 0;
101353
+ const message = input.imageContext ? `${input.imageContext}
101354
+
101355
+ ${question}` : question;
101044
101356
  const consumed = await this.consumeStandby(parentSid, threadId, title);
101045
- const threadSessionId = consumed ?? (await forkSideThreadSession(parentSid, threadId, { message: question, ...title ? { title } : {} })).sessionId;
101357
+ const threadSessionId = consumed ?? (await forkSideThreadSession(parentSid, threadId, { message, ...title ? { title } : {} })).sessionId;
101046
101358
  let entry;
101047
101359
  try {
101048
101360
  const { addSideThread: addSideThread2 } = await Promise.resolve().then(() => (init_side_questions(), side_questions_exports));
@@ -101054,7 +101366,7 @@ var init_side_thread_manager = __esm({
101054
101366
  });
101055
101367
  if (consumed) {
101056
101368
  const { sendMessageToSession: sendMessageToSession2 } = await Promise.resolve().then(() => (init_session_message_queue(), session_message_queue_exports));
101057
- await sendMessageToSession2(threadSessionId, question, { source: "side-thread" });
101369
+ await sendMessageToSession2(threadSessionId, message, { source: "side-thread" });
101058
101370
  }
101059
101371
  } catch (err2) {
101060
101372
  void this.retireSession(threadSessionId, "side_thread_create_failed");
@@ -101089,6 +101401,7 @@ var init_side_thread_manager = __esm({
101089
101401
  if (!await this.isStandbyUsable(parentSid, standby)) {
101090
101402
  void this.retireSession(standby.claudeSessionId, "side_thread_standby_stale");
101091
101403
  this.standbyParentOffsets.delete(parentSid);
101404
+ this.warmedStandbys.delete(parentSid);
101092
101405
  return null;
101093
101406
  }
101094
101407
  const timer3 = this.standbyTimers.get(parentSid);
@@ -101097,6 +101410,7 @@ var init_side_thread_manager = __esm({
101097
101410
  this.standbyTimers.delete(parentSid);
101098
101411
  }
101099
101412
  this.standbyParentOffsets.delete(parentSid);
101413
+ this.warmedStandbys.delete(parentSid);
101100
101414
  const lane = sideThreadLaneKey(parentSid, threadId);
101101
101415
  const { updateSessionRecord: updateSessionRecord2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
101102
101416
  await updateSessionRecord2(standby.claudeSessionId, {
@@ -101283,16 +101597,30 @@ async function promoteSideThread(parentSid, threadId, opts) {
101283
101597
  const threadRecord = await getSessionByClaudeId2(sessionId);
101284
101598
  if (!threadRecord) throw new SessionControlError("Side thread session not found", 404);
101285
101599
  const parentRecord = await getSessionByClaudeId2(parentSid);
101286
- const parentTaskId = parentRecord?.taskId?.trim() || void 0;
101287
- const title = opts?.title?.trim() || entry.title?.trim() || entry.question;
101600
+ const siblingOfTaskId = parentRecord?.taskId?.trim() || void 0;
101601
+ const threadLabel = opts?.title?.trim() || entry.title?.trim() || void 0;
101288
101602
  const { addTask: addTask2, linkSession: linkSession2, linkSessionSlot: linkSessionSlot2 } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
101289
- const { task } = await addTask2({
101290
- title,
101291
- description: entry.question,
101292
- ...parentTaskId ? { parent_task_id: parentTaskId } : {},
101293
- // A person clicked "promote" — same board default as any hand-made task.
101294
- pinned: true
101295
- });
101603
+ let groupId;
101604
+ let task;
101605
+ if (siblingOfTaskId) {
101606
+ const created = await createForkSiblingTask(siblingOfTaskId, {
101607
+ // A known label short-circuits the background refine; without one the
101608
+ // thread's first question is what gets summarized (same as a fork's message).
101609
+ titlePrefix: threadLabel,
101610
+ prompt: entry.question,
101611
+ description: entry.question,
101612
+ source: "side-thread-promote"
101613
+ });
101614
+ task = created.task;
101615
+ groupId = created.groupId;
101616
+ } else {
101617
+ ({ task } = await addTask2({
101618
+ title: threadLabel || entry.question,
101619
+ description: entry.question,
101620
+ // A person clicked "promote" — same board default as any hand-made task.
101621
+ pinned: true
101622
+ }));
101623
+ }
101296
101624
  const stamped = await markThreadPromoted2(parentSid, threadId, task.id);
101297
101625
  if (!stamped) {
101298
101626
  log.session.warn("side thread promote: entry vanished before stamp \u2014 promote proceeds, 409 guard is unarmed", {
@@ -101306,7 +101634,7 @@ async function promoteSideThread(parentSid, threadId, opts) {
101306
101634
  project: task.project ?? "",
101307
101635
  // Clearing the lane un-hides the session (see the file header).
101308
101636
  lane: void 0,
101309
- title
101637
+ title: task.title
101310
101638
  });
101311
101639
  try {
101312
101640
  const { sessionRunner: sessionRunner2 } = await Promise.resolve().then(() => (init_claude_code_session(), claude_code_session_exports));
@@ -101325,10 +101653,18 @@ async function promoteSideThread(parentSid, threadId, opts) {
101325
101653
  error: err2 instanceof Error ? err2.message : String(err2)
101326
101654
  });
101327
101655
  }
101328
- log.session.info("side thread: promoted to task", { parentSid, threadId, sessionId, taskId: task.id });
101656
+ log.session.info("side thread: promoted to task", {
101657
+ parentSid,
101658
+ threadId,
101659
+ sessionId,
101660
+ taskId: task.id,
101661
+ siblingOfTaskId,
101662
+ groupId
101663
+ });
101329
101664
  return {
101330
101665
  taskId: task.id,
101331
- ...task.parent_task_id ? { parentTaskId: task.parent_task_id } : {},
101666
+ ...siblingOfTaskId ? { siblingOfTaskId } : {},
101667
+ ...groupId ? { groupId } : {},
101332
101668
  sessionId
101333
101669
  };
101334
101670
  }
@@ -101343,7 +101679,7 @@ var init_side_thread_promote = __esm({
101343
101679
 
101344
101680
  // src/web/routes/sessions.ts
101345
101681
  import { Router as Router10 } from "express";
101346
- import { randomUUID as randomUUID5 } from "crypto";
101682
+ import { randomUUID as randomUUID6 } from "crypto";
101347
101683
  import os19 from "os";
101348
101684
  import path91 from "path";
101349
101685
  function condenseStaleReason(msg) {
@@ -101689,7 +102025,7 @@ var init_sessions2 = __esm({
101689
102025
  }
101690
102026
  const isNativeEngine = !isAcpEngine(engine);
101691
102027
  const clientSessionId = typeof req.body.sessionId === "string" && UUID_RE2.test(req.body.sessionId) ? req.body.sessionId : void 0;
101692
- const preassignedSessionId = isNativeEngine ? clientSessionId ?? randomUUID5() : void 0;
102028
+ const preassignedSessionId = isNativeEngine ? clientSessionId ?? randomUUID6() : void 0;
101693
102029
  const updatedTask = await quickStartSession({
101694
102030
  message: sessionMessage,
101695
102031
  messagePrefix,
@@ -102664,18 +103000,38 @@ var init_sessions2 = __esm({
102664
103000
  });
102665
103001
  }
102666
103002
  });
103003
+ sessionsRouter.post("/:sessionId/side-threads/standby/warm", async (req, res) => {
103004
+ if (refuseSideThreadsOnReplica(res)) return;
103005
+ const sessionId = String(req.params.sessionId);
103006
+ try {
103007
+ const { sideThreadManager: sideThreadManager2 } = await Promise.resolve().then(() => (init_side_thread_manager(), side_thread_manager_exports));
103008
+ res.json(await sideThreadManager2.warmStandby(sessionId));
103009
+ } catch (err2) {
103010
+ log.web.warn("side thread standby warm-up failed", {
103011
+ sessionId,
103012
+ error: err2 instanceof Error ? err2.message : String(err2)
103013
+ });
103014
+ res.json({ warmed: false, reason: "error" });
103015
+ }
103016
+ });
102667
103017
  sessionsRouter.post("/:sessionId/side-threads", async (req, res, next) => {
102668
103018
  if (refuseSideThreadsOnReplica(res)) return;
102669
103019
  try {
102670
- const { question, title } = req.body ?? {};
103020
+ const { question, title, images } = req.body ?? {};
102671
103021
  if (!question || typeof question !== "string" || !question.trim()) {
102672
103022
  res.status(400).json({ error: "question (non-empty string) is required" });
102673
103023
  return;
102674
103024
  }
103025
+ let imageContext = "";
103026
+ if (images && images.length > 0) {
103027
+ const processed = await processAndSaveImages(images);
103028
+ if (processed) imageContext = buildSessionImageContext(processed.savedImages);
103029
+ }
102675
103030
  const { sideThreadManager: sideThreadManager2 } = await Promise.resolve().then(() => (init_side_thread_manager(), side_thread_manager_exports));
102676
103031
  const thread = await sideThreadManager2.createThread(String(req.params.sessionId), {
102677
103032
  question,
102678
- ...typeof title === "string" && title.trim() ? { title } : {}
103033
+ ...typeof title === "string" && title.trim() ? { title } : {},
103034
+ ...imageContext ? { imageContext } : {}
102679
103035
  });
102680
103036
  res.json({ thread });
102681
103037
  } catch (err2) {
@@ -102694,7 +103050,8 @@ var init_sessions2 = __esm({
102694
103050
  );
102695
103051
  res.json({
102696
103052
  taskId: result.taskId,
102697
- ...result.parentTaskId ? { parentTaskId: result.parentTaskId } : {},
103053
+ ...result.siblingOfTaskId ? { siblingOfTaskId: result.siblingOfTaskId } : {},
103054
+ ...result.groupId ? { groupId: result.groupId } : {},
102698
103055
  sessionId: result.sessionId
102699
103056
  });
102700
103057
  } catch (err2) {
@@ -103389,7 +103746,7 @@ var micro_claude_exports = {};
103389
103746
  __export(micro_claude_exports, {
103390
103747
  runMicroClaude: () => runMicroClaude
103391
103748
  });
103392
- import { randomUUID as randomUUID6 } from "crypto";
103749
+ import { randomUUID as randomUUID7 } from "crypto";
103393
103750
  async function runMicroClaude(opts) {
103394
103751
  if (opts.warm) {
103395
103752
  const run10 = await runWarmMicroClaude({
@@ -103398,7 +103755,7 @@ async function runMicroClaude(opts) {
103398
103755
  tools: opts.tools ?? [],
103399
103756
  prompt: opts.prompt,
103400
103757
  timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2,
103401
- toolUseId: opts.toolUseId ?? `micro-claude-${randomUUID6()}`,
103758
+ toolUseId: opts.toolUseId ?? `micro-claude-${randomUUID7()}`,
103402
103759
  ...opts.onBlock ? { onBlock: opts.onBlock } : {}
103403
103760
  });
103404
103761
  return { response: run10.response, costUsd: run10.costUsd, durationMs: run10.durationMs };
@@ -103408,7 +103765,7 @@ async function runMicroClaude(opts) {
103408
103765
  systemPrompt: opts.system,
103409
103766
  model: opts.model ?? DEFAULT_MODEL2,
103410
103767
  timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2,
103411
- toolUseId: opts.toolUseId ?? `micro-claude-${randomUUID6()}`,
103768
+ toolUseId: opts.toolUseId ?? `micro-claude-${randomUUID7()}`,
103412
103769
  slim: true,
103413
103770
  thinking: opts.thinking ?? false,
103414
103771
  ...opts.tools && opts.tools.length > 0 ? { tools: opts.tools } : {},
@@ -104233,7 +104590,7 @@ __export(micro_agent_exports, {
104233
104590
  async function resolveTierModel(tier) {
104234
104591
  const { getConfig: getConfig2 } = await Promise.resolve().then(() => (init_config_manager(), config_manager_exports));
104235
104592
  const config = await getConfig2();
104236
- const provider = config.agent?.main_provider ?? "bedrock";
104593
+ const provider = resolveMainProviderName(config);
104237
104594
  const { MODEL_CATALOG: MODEL_CATALOG3 } = await Promise.resolve().then(() => (init_model_catalog(), model_catalog_exports));
104238
104595
  const entry = MODEL_CATALOG3[provider]?.find((m6) => {
104239
104596
  const id = m6.id.toLowerCase();
@@ -104302,6 +104659,7 @@ var DEFAULT_TIER, DEFAULT_MAX_TOKENS2, DEFAULT_MAX_ROUNDS, DEFAULT_TIMEOUT_MS3;
104302
104659
  var init_micro_agent = __esm({
104303
104660
  "src/agent/micro-agent.ts"() {
104304
104661
  "use strict";
104662
+ init_default_provider();
104305
104663
  DEFAULT_TIER = "sonnet";
104306
104664
  DEFAULT_MAX_TOKENS2 = 2e3;
104307
104665
  DEFAULT_MAX_ROUNDS = 3;
@@ -104317,7 +104675,7 @@ __export(task_search_agent_exports, {
104317
104675
  prewarmAgentSearchChild: () => prewarmAgentSearchChild,
104318
104676
  runTaskSearchAgent: () => runTaskSearchAgent
104319
104677
  });
104320
- import { randomUUID as randomUUID7 } from "crypto";
104678
+ import { randomUUID as randomUUID8 } from "crypto";
104321
104679
  function useCliEngine() {
104322
104680
  return process.env.WALNUT_AGENT_SEARCH_ENGINE !== "inprocess";
104323
104681
  }
@@ -104397,7 +104755,7 @@ async function claudeCliEngine(userPrompt, options) {
104397
104755
  model: options.model,
104398
104756
  timeoutMs: options.timeoutMs,
104399
104757
  tools: ["Bash"],
104400
- toolUseId: `task-search-${randomUUID7()}`,
104758
+ toolUseId: `task-search-${randomUUID8()}`,
104401
104759
  warm: true,
104402
104760
  onBlock
104403
104761
  });
@@ -105660,10 +106018,11 @@ async function buildProvidersPayload() {
105660
106018
  providers[name] = {
105661
106019
  api: prov.api,
105662
106020
  base_url: prov.base_url,
105663
- status: caps.subscriptionReady ? "ready" : "no_key",
106021
+ status: caps.installed ? "ready" : "no_key",
105664
106022
  auto_detected: !explicitNames.has(name),
105665
106023
  models: getModelsForProvider(name, prov.models),
105666
- credential_source: caps.subscriptionReady ? "subscription" : caps.installed ? "cli_no_subscription" : "cli_not_installed"
106024
+ credential_source: caps.installed ? `cli_${caps.auth.mode}` : "cli_not_installed",
106025
+ credential_detail: caps.installed ? caps.auth.label : void 0
105667
106026
  };
105668
106027
  continue;
105669
106028
  }
@@ -105722,10 +106081,11 @@ async function buildProvidersPayload() {
105722
106081
  const caps = detectClaudeCli2();
105723
106082
  providers[name] = {
105724
106083
  api: template.api,
105725
- status: caps.subscriptionReady ? "ready" : "no_key",
106084
+ status: caps.installed ? "ready" : "no_key",
105726
106085
  auto_detected: false,
105727
106086
  models: getModelsForProvider(name),
105728
- credential_source: caps.subscriptionReady ? "subscription" : caps.installed ? "cli_no_subscription" : "cli_not_installed"
106087
+ credential_source: caps.installed ? `cli_${caps.auth.mode}` : "cli_not_installed",
106088
+ credential_detail: caps.installed ? caps.auth.label : void 0
105729
106089
  };
105730
106090
  continue;
105731
106091
  }
@@ -105881,7 +106241,7 @@ var init_config11 = __esm({
105881
106241
  const {
105882
106242
  traceCredentialResolution: traceCredentialResolution2,
105883
106243
  resolveCredentials: resolveCredentials2,
105884
- readClaudeSettingsEnv: readClaudeSettingsEnv2,
106244
+ readClaudeSettingsEnv: readClaudeSettingsEnv3,
105885
106245
  readClaudeCredentialExport: readClaudeCredentialExport2
105886
106246
  } = await Promise.resolve().then(() => (init_credential_resolver(), credential_resolver_exports));
105887
106247
  const fs121 = await import("fs");
@@ -105897,7 +106257,7 @@ var init_config11 = __esm({
105897
106257
  };
105898
106258
  const trace = traceCredentialResolution2({
105899
106259
  config,
105900
- claudeEnv: readClaudeSettingsEnv2(),
106260
+ claudeEnv: readClaudeSettingsEnv3(),
105901
106261
  processEnv: process.env,
105902
106262
  awsFiles: {
105903
106263
  credentials: exists3(path193.join(home, ".aws", "credentials")),
@@ -133332,7 +133692,7 @@ __export(session_requests_exports, {
133332
133692
  settleNotified: () => settleNotified,
133333
133693
  settleReplied: () => settleReplied
133334
133694
  });
133335
- import { createHash as createHash18, randomUUID as randomUUID8 } from "crypto";
133695
+ import { createHash as createHash18, randomUUID as randomUUID9 } from "crypto";
133336
133696
  import path137 from "path";
133337
133697
  function oneLine(text, max = 160) {
133338
133698
  const flat = text.replace(/\s+/g, " ").trim();
@@ -133356,7 +133716,7 @@ function clampReplyTimeoutSecs(secs) {
133356
133716
  }
133357
133717
  async function createSessionRequest(input) {
133358
133718
  const request3 = {
133359
- id: `rq-${randomUUID8().replace(/-/g, "").slice(0, 12)}`,
133719
+ id: `rq-${randomUUID9().replace(/-/g, "").slice(0, 12)}`,
133360
133720
  fromSessionId: input.fromSessionId,
133361
133721
  ...input.toSessionId ? { toSessionId: input.toSessionId } : {},
133362
133722
  ...input.toTaskId ? { toTaskId: input.toTaskId } : {},
@@ -133789,7 +134149,7 @@ __export(task_start_exports, {
133789
134149
  SessionExistsError: () => SessionExistsError,
133790
134150
  startSessionForTask: () => startSessionForTask
133791
134151
  });
133792
- import { randomUUID as randomUUID9 } from "crypto";
134152
+ import { randomUUID as randomUUID10 } from "crypto";
133793
134153
  async function startSessionForTask(params) {
133794
134154
  const { taskIdPrefix, source } = params;
133795
134155
  let task;
@@ -133840,7 +134200,7 @@ async function startSessionForTask(params) {
133840
134200
  }
133841
134201
  let message = params.message?.trim() || `Working on task: ${task.title}`;
133842
134202
  const engine = normalizeEngine(params.engine);
133843
- const preassignedSessionId = engineCaps(engine).idProvisioning === "provider-issued" ? void 0 : randomUUID9();
134203
+ const preassignedSessionId = engineCaps(engine).idProvisioning === "provider-issued" ? void 0 : randomUUID10();
133844
134204
  let requestId;
133845
134205
  if (params.expectReply) {
133846
134206
  const { resolveCaller: resolveCaller2 } = await Promise.resolve().then(() => (init_session_send_core(), session_send_core_exports));
@@ -149338,7 +149698,7 @@ async function generateLedgerDesc(task, timeoutMs = 15e3) {
149338
149698
  try {
149339
149699
  const { getConfig: getConfig2 } = await Promise.resolve().then(() => (init_config_manager(), config_manager_exports));
149340
149700
  const config = await getConfig2();
149341
- const providerName = config.agent?.main_provider ?? "bedrock";
149701
+ const providerName = resolveMainProviderName(config);
149342
149702
  const model = cheapModelFor(providerName);
149343
149703
  const detail = (task.description ?? "").trim().slice(0, 800);
149344
149704
  const project = (task.project ?? "").trim();
@@ -149397,6 +149757,7 @@ var init_task_ledger_desc = __esm({
149397
149757
  "use strict";
149398
149758
  init_model();
149399
149759
  init_model_catalog();
149760
+ init_default_provider();
149400
149761
  init_logging();
149401
149762
  init_task_manager();
149402
149763
  MAX_DESC_LEN = 120;
@@ -149420,6 +149781,10 @@ function diffStreamedVsPersisted(streamedTextMsgIds, persistedMsgIds) {
149420
149781
  }
149421
149782
  function armStreamConvergenceCheck(sessionId, streamedTextMsgIds) {
149422
149783
  if (streamedTextMsgIds.length === 0) return;
149784
+ if (consumeWarmupTurn(sessionId)) {
149785
+ log.obs.debug("stream-convergence: skipped hidden warm-up turn", { sessionId });
149786
+ return;
149787
+ }
149423
149788
  const timer3 = setTimeout(() => {
149424
149789
  void runCheck(sessionId, streamedTextMsgIds).catch((err2) => {
149425
149790
  log.obs.warn("stream-convergence check failed", {
@@ -149485,6 +149850,7 @@ var init_stream_convergence = __esm({
149485
149850
  "src/core/observability/stream-convergence.ts"() {
149486
149851
  "use strict";
149487
149852
  init_logging();
149853
+ init_side_thread_warmup();
149488
149854
  CHECK_DELAY_MS = 15e3;
149489
149855
  INCIDENT_DEDUPE_MS = 30 * 6e4;
149490
149856
  lastIncidentAt = /* @__PURE__ */ new Map();
@@ -149735,12 +150101,17 @@ function getSystemHealth() {
149735
150101
  }
149736
150102
  async function refreshSystemHealth() {
149737
150103
  systemHealth.claudeCliAvailable = checkClaudeCliAvailable();
149738
- const cred = await resolveCredentialHealth();
150104
+ applyCredentialHealth(await resolveCredentialHealth());
150105
+ broadcastEvent("system:health", systemHealth);
150106
+ return systemHealth;
150107
+ }
150108
+ function applyCredentialHealth(cred) {
149739
150109
  systemHealth.hasReadyProvider = cred.hasReadyProvider;
149740
150110
  systemHealth.credentialSource = cred.source;
149741
150111
  systemHealth.credentialDetail = cred.detail;
149742
- broadcastEvent("system:health", systemHealth);
149743
- return systemHealth;
150112
+ systemHealth.mainProvider = cred.mainProvider;
150113
+ systemHealth.mainProviderImplicit = cred.mainProviderImplicit;
150114
+ systemHealth.claudeCliAuth = cred.claudeCliAuth;
149744
150115
  }
149745
150116
  function checkClaudeCliAvailable() {
149746
150117
  return isClaudeCliInstalled();
@@ -149750,27 +150121,36 @@ async function resolveCredentialHealth() {
149750
150121
  const { getConfig: getConfig2 } = await Promise.resolve().then(() => (init_config_manager(), config_manager_exports));
149751
150122
  const { resolveCredentials: resolveCredentials2 } = await Promise.resolve().then(() => (init_credential_resolver(), credential_resolver_exports));
149752
150123
  const { buildProviderMap: buildProviderMap2 } = await Promise.resolve().then(() => (init_registry2(), registry_exports));
150124
+ const { resolveMainProviderName: resolveMainProviderName2, mainProviderIsImplicit: mainProviderIsImplicit2 } = await Promise.resolve().then(() => (init_default_provider(), default_provider_exports));
150125
+ const { detectClaudeCli: detectClaudeCli2 } = await Promise.resolve().then(() => (init_claude_cli_detect(), claude_cli_detect_exports));
149753
150126
  const config = await getConfig2();
150127
+ const cli = detectClaudeCli2();
150128
+ const mainProvider = resolveMainProviderName2(config, cli.installed);
150129
+ const base = {
150130
+ mainProvider,
150131
+ mainProviderImplicit: mainProviderIsImplicit2(config),
150132
+ claudeCliAuth: cli.installed ? cli.auth.label : void 0
150133
+ };
150134
+ if (mainProvider === "claude_cli") {
150135
+ return cli.installed ? { ...base, hasReadyProvider: true, source: "config", detail: `claude-cli (${cli.auth.label})` } : { ...base, hasReadyProvider: false, source: "none" };
150136
+ }
149754
150137
  const cred = resolveCredentials2(config);
149755
150138
  if (cred.source !== "none") {
149756
- return { hasReadyProvider: true, source: cred.source, detail: cred.detail };
150139
+ return { ...base, hasReadyProvider: true, source: cred.source, detail: cred.detail };
149757
150140
  }
149758
150141
  const providers = buildProviderMap2(config.providers, config);
149759
150142
  for (const [, prov] of Object.entries(providers)) {
149760
150143
  if (prov.api === "bedrock" || prov.api === "ollama") continue;
149761
150144
  if (prov.api === "claude-cli") {
149762
- const { detectClaudeCli: detectClaudeCli2 } = await Promise.resolve().then(() => (init_claude_cli_detect(), claude_cli_detect_exports));
149763
- if (detectClaudeCli2().subscriptionReady) {
149764
- return { hasReadyProvider: true, source: "config", detail: "claude-cli (subscription)" };
149765
- }
150145
+ if (cli.installed) return { ...base, hasReadyProvider: true, source: "config", detail: `claude-cli (${cli.auth.label})` };
149766
150146
  continue;
149767
150147
  }
149768
150148
  const implemented = prov.api === "anthropic-messages" || prov.api === "openai-chat" || prov.api === "google-generative-ai";
149769
150149
  if (implemented && (prov.api_key || prov.bearer_token)) {
149770
- return { hasReadyProvider: true, source: "config", detail: prov.api };
150150
+ return { ...base, hasReadyProvider: true, source: "config", detail: prov.api };
149771
150151
  }
149772
150152
  }
149773
- return { hasReadyProvider: false, source: "none" };
150153
+ return { ...base, hasReadyProvider: false, source: "none" };
149774
150154
  } catch {
149775
150155
  return { hasReadyProvider: false, source: "none" };
149776
150156
  }
@@ -149809,11 +150189,11 @@ async function startServer(options = {}) {
149809
150189
  }
149810
150190
  {
149811
150191
  const cred = await resolveCredentialHealth();
149812
- systemHealth.hasReadyProvider = cred.hasReadyProvider;
149813
- systemHealth.credentialSource = cred.source;
149814
- systemHealth.credentialDetail = cred.detail;
150192
+ applyCredentialHealth(cred);
149815
150193
  if (!systemHealth.hasReadyProvider) {
149816
150194
  log.web.warn("No AI provider configured \u2014 configure one in Settings");
150195
+ } else if (cred.mainProvider === "claude_cli") {
150196
+ log.web.info("AI provider: Claude Code", { auth: cred.claudeCliAuth, implicit: cred.mainProviderImplicit });
149817
150197
  } else if (cred.source && cred.source !== "config") {
149818
150198
  log.web.info("Auto-detected Bedrock credential", { source: cred.source, detail: cred.detail });
149819
150199
  }
@@ -151093,10 +151473,9 @@ async function startServer(options = {}) {
151093
151473
  const prev = systemHealth.hasReadyProvider;
151094
151474
  const prevSource = systemHealth.credentialSource;
151095
151475
  const cred = await resolveCredentialHealth();
151096
- systemHealth.hasReadyProvider = cred.hasReadyProvider;
151097
- systemHealth.credentialSource = cred.source;
151098
- systemHealth.credentialDetail = cred.detail;
151099
- if (cred.hasReadyProvider !== prev || cred.source !== prevSource) {
151476
+ const prevProvider = systemHealth.mainProvider;
151477
+ applyCredentialHealth(cred);
151478
+ if (cred.hasReadyProvider !== prev || cred.source !== prevSource || cred.mainProvider !== prevProvider) {
151100
151479
  broadcastEvent("system:health", systemHealth);
151101
151480
  }
151102
151481
  } catch {
@@ -158523,11 +158902,12 @@ __export(session_controls_exports, {
158523
158902
  applySessionModelChange: () => applySessionModelChange,
158524
158903
  computeModelOptions: () => computeModelOptions,
158525
158904
  controlErrorKind: () => controlErrorKind,
158905
+ createForkSiblingTask: () => createForkSiblingTask,
158526
158906
  forkSessionToTask: () => forkSessionToTask,
158527
158907
  handleSessionControlRelay: () => handleSessionControlRelay,
158528
158908
  modelReadBackMatches: () => modelReadBackMatches
158529
158909
  });
158530
- import { randomUUID as randomUUID10 } from "crypto";
158910
+ import { randomUUID as randomUUID11 } from "crypto";
158531
158911
  function controlErrorKind(status2) {
158532
158912
  if (status2 === 404) return "not_found";
158533
158913
  if (status2 === 405) return "method_not_allowed";
@@ -158700,6 +159080,124 @@ async function computeModelOptions(sessionId) {
158700
159080
  currentEffort: liveEffort ?? record.effectiveEffort ?? record.effort ?? null
158701
159081
  };
158702
159082
  }
159083
+ async function createForkSiblingTask(sourceTaskId, opts) {
159084
+ const {
159085
+ getTask: getTask2,
159086
+ addTask: addTask2,
159087
+ updateTask: updateTask2,
159088
+ groupTasks: groupTasks2,
159089
+ addToGroup: addToGroup2,
159090
+ renameGroup: renameGroup2
159091
+ } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
159092
+ let sourceTask;
159093
+ try {
159094
+ sourceTask = await getTask2(sourceTaskId);
159095
+ } catch {
159096
+ throw new SessionControlError(`Source task "${sourceTaskId}" not found`, 404);
159097
+ }
159098
+ const sourceBaseTitle = sourceTask.title.replace(/^Fork of\s+/i, "").replace(/\s+-\s+fork of\s+.*$/i, "").trim() || sourceTask.title;
159099
+ const explicitTitle = opts.childTitle || (opts.titlePrefix ? `${opts.titlePrefix} - fork of ${sourceBaseTitle}` : void 0);
159100
+ const autoTitle = !explicitTitle;
159101
+ const newTitle = explicitTitle || `Fork of ${sourceBaseTitle}`;
159102
+ let inheritedTier;
159103
+ if (sourceTask.pinned && sourceTask.focus_tier) {
159104
+ const { getCustomTiers: getCustomTiers2 } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
159105
+ const known = ["focus", "backlog", "wait"].includes(sourceTask.focus_tier) || (await getCustomTiers2()).some((t3) => t3.id === sourceTask.focus_tier);
159106
+ if (known) inheritedTier = sourceTask.focus_tier;
159107
+ else {
159108
+ log.session.warn("fork: source tier is not registered, filing the fork in Satellite", {
159109
+ sourceTaskId: sourceTask.id,
159110
+ tier: sourceTask.focus_tier
159111
+ });
159112
+ }
159113
+ }
159114
+ const { task: newFork } = await addTask2({
159115
+ title: newTitle,
159116
+ project: sourceTask.project || "",
159117
+ source: sourceTask.source,
159118
+ ...opts.description ? { description: opts.description } : {},
159119
+ // A fork is a person splitting off live work, so it joins the board like
159120
+ // any other hand-made task (Satellite = pinned, no stored tier).
159121
+ pinned: true,
159122
+ ...inheritedTier ? { focus_tier: inheritedTier } : {},
159123
+ // Forking an Ask Walnut conversation yields another Ask Walnut
159124
+ // conversation: without the flag the fork loses the amber marker AND the
159125
+ // persona drift repair (both key on task.walnut_agent, never on project).
159126
+ ...sourceTask.walnut_agent ? { walnut_agent: true } : {}
159127
+ });
159128
+ let forkGroupId;
159129
+ try {
159130
+ if (sourceTask.group_id) {
159131
+ const r8 = await addToGroup2(sourceTask.group_id, [newFork.id]);
159132
+ forkGroupId = r8.group_id;
159133
+ } else {
159134
+ const r8 = await groupTasks2([sourceTask.id, newFork.id], sourceTask.title);
159135
+ forkGroupId = r8.group_id;
159136
+ }
159137
+ } catch (err2) {
159138
+ log.session.warn("fork: failed to group source + fork", {
159139
+ sourceTaskId: sourceTask.id,
159140
+ forkTaskId: newFork.id,
159141
+ error: err2 instanceof Error ? err2.message : String(err2)
159142
+ });
159143
+ }
159144
+ let task;
159145
+ try {
159146
+ task = await getTask2(newFork.id);
159147
+ } catch {
159148
+ task = newFork;
159149
+ }
159150
+ bus.emit(EventNames.TASK_CREATED, { task }, ["web-ui", "main-agent"], { source: opts.source });
159151
+ if (autoTitle && opts.prompt?.trim()) {
159152
+ const forkId = newFork.id;
159153
+ const sourceTitle = sourceBaseTitle;
159154
+ const placeholderTitle = newTitle;
159155
+ const prompt = opts.prompt;
159156
+ const titleSource = `${opts.source}-title`;
159157
+ void (async () => {
159158
+ try {
159159
+ const { summarizeForkPrompt: summarizeForkPrompt2 } = await Promise.resolve().then(() => (init_fork_title(), fork_title_exports));
159160
+ const label = await summarizeForkPrompt2(prompt);
159161
+ if (!label) return;
159162
+ const current5 = await getTask2(forkId);
159163
+ if (current5.title !== placeholderTitle) {
159164
+ log.session.info("fork title refine skipped \u2014 title changed since fork", { taskId: forkId });
159165
+ return;
159166
+ }
159167
+ const refinedTitle = `${label} - fork of ${sourceTitle}`;
159168
+ const { task: updated } = await updateTask2(forkId, { title: refinedTitle }, { source: titleSource });
159169
+ bus.emit(EventNames.TASK_UPDATED, { task: updated }, ["web-ui", "main-agent"], { source: titleSource });
159170
+ log.session.info("fork title refined", { taskId: forkId, title: refinedTitle });
159171
+ } catch (err2) {
159172
+ log.session.warn("fork title refine failed", {
159173
+ taskId: forkId,
159174
+ error: err2 instanceof Error ? err2.message : String(err2)
159175
+ });
159176
+ }
159177
+ })();
159178
+ }
159179
+ if (forkGroupId && !sourceTask.group_id) {
159180
+ const gid = forkGroupId;
159181
+ const seedTitles = [sourceTask.title, newTitle];
159182
+ const groupSource = opts.source;
159183
+ void (async () => {
159184
+ try {
159185
+ const { summarizeGroupLabel: summarizeGroupLabel2 } = await Promise.resolve().then(() => (init_fork_title(), fork_title_exports));
159186
+ const label = await summarizeGroupLabel2(seedTitles);
159187
+ if (!label) return;
159188
+ await renameGroup2(gid, label);
159189
+ bus.emit(EventNames.TASK_GROUPS_CHANGED, { group_id: gid, label }, ["web-ui", "main-agent"], { source: groupSource });
159190
+ log.session.info("fork group label refined", { groupId: gid, label });
159191
+ } catch (err2) {
159192
+ log.session.warn("fork group label refine failed", {
159193
+ groupId: gid,
159194
+ error: err2 instanceof Error ? err2.message : String(err2)
159195
+ });
159196
+ }
159197
+ })();
159198
+ }
159199
+ return { task, ...forkGroupId ? { groupId: forkGroupId } : {} };
159200
+ }
158703
159201
  async function forkSessionToTask(sourceSessionId, input, source = "web-api") {
158704
159202
  const { task_id, create_child_task, child_title, message, title, model, imageContext } = input;
158705
159203
  if (!task_id && !create_child_task) {
@@ -158709,14 +159207,7 @@ async function forkSessionToTask(sourceSessionId, input, source = "web-api") {
158709
159207
  throw new SessionControlError("task_id and create_child_task are mutually exclusive", 400);
158710
159208
  }
158711
159209
  const { getSessionByClaudeId: getSessionByClaudeId2, getSessionsForTask: getSessionsForTask2, createSessionRecord: createSessionRecord2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
158712
- const {
158713
- getTask: getTask2,
158714
- addTask: addTask2,
158715
- updateTask: updateTask2,
158716
- groupTasks: groupTasks2,
158717
- addToGroup: addToGroup2,
158718
- renameGroup: renameGroup2
158719
- } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
159210
+ const { getTask: getTask2 } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
158720
159211
  const sourceRecord = await getSessionByClaudeId2(sourceSessionId);
158721
159212
  if (!sourceRecord) throw new SessionControlError("Source session not found", 404);
158722
159213
  if (!engineCaps(sourceRecord.engine).fork) {
@@ -158733,108 +159224,13 @@ async function forkSessionToTask(sourceSessionId, input, source = "web-api") {
158733
159224
  if (!sourceRecord.taskId) {
158734
159225
  throw new SessionControlError("Source session has no task \u2014 cannot create fork task", 400);
158735
159226
  }
158736
- let sourceTask;
158737
- try {
158738
- sourceTask = await getTask2(sourceRecord.taskId);
158739
- } catch {
158740
- throw new SessionControlError(`Source task "${sourceRecord.taskId}" not found`, 404);
158741
- }
158742
- const sourceBaseTitle = sourceTask.title.replace(/^Fork of\s+/i, "").replace(/\s+-\s+fork of\s+.*$/i, "").trim() || sourceTask.title;
158743
- const autoTitle = !child_title;
158744
- const newTitle = child_title || `Fork of ${sourceBaseTitle}`;
158745
- let inheritedTier;
158746
- if (sourceTask.pinned && sourceTask.focus_tier) {
158747
- const { getCustomTiers: getCustomTiers2 } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
158748
- const known = ["focus", "backlog", "wait"].includes(sourceTask.focus_tier) || (await getCustomTiers2()).some((t3) => t3.id === sourceTask.focus_tier);
158749
- if (known) inheritedTier = sourceTask.focus_tier;
158750
- else {
158751
- log.session.warn("fork: source tier is not registered, filing the fork in Satellite", {
158752
- sourceTaskId: sourceTask.id,
158753
- tier: sourceTask.focus_tier
158754
- });
158755
- }
158756
- }
158757
- const { task: newFork } = await addTask2({
158758
- title: newTitle,
158759
- project: sourceTask.project || "",
158760
- source: sourceTask.source,
158761
- // A fork is a person splitting off live work, so it joins the board like
158762
- // any other hand-made task (Satellite = pinned, no stored tier).
158763
- pinned: true,
158764
- ...inheritedTier ? { focus_tier: inheritedTier } : {},
158765
- // Forking an Ask Walnut conversation yields another Ask Walnut
158766
- // conversation: without the flag the fork loses the amber marker AND the
158767
- // persona drift repair (both key on task.walnut_agent, never on project).
158768
- ...sourceTask.walnut_agent ? { walnut_agent: true } : {}
158769
- });
158770
- let forkGroupId;
158771
- try {
158772
- if (sourceTask.group_id) {
158773
- const r8 = await addToGroup2(sourceTask.group_id, [newFork.id]);
158774
- forkGroupId = r8.group_id;
158775
- } else {
158776
- const r8 = await groupTasks2([sourceTask.id, newFork.id], sourceTask.title);
158777
- forkGroupId = r8.group_id;
158778
- }
158779
- } catch (err2) {
158780
- log.session.warn("fork: failed to group source + fork", {
158781
- sourceTaskId: sourceTask.id,
158782
- forkTaskId: newFork.id,
158783
- error: err2 instanceof Error ? err2.message : String(err2)
158784
- });
158785
- }
158786
- try {
158787
- task = await getTask2(newFork.id);
158788
- } catch {
158789
- task = newFork;
158790
- }
158791
- bus.emit(EventNames.TASK_CREATED, { task }, ["web-ui", "main-agent"], { source: "fork" });
159227
+ const created = await createForkSiblingTask(sourceRecord.taskId, {
159228
+ childTitle: child_title,
159229
+ prompt: message,
159230
+ source: "fork"
159231
+ });
159232
+ task = created.task;
158792
159233
  childTaskCreated = true;
158793
- if (autoTitle && message?.trim()) {
158794
- const forkId = newFork.id;
158795
- const sourceTitle = sourceBaseTitle;
158796
- const placeholderTitle = newTitle;
158797
- void (async () => {
158798
- try {
158799
- const { summarizeForkPrompt: summarizeForkPrompt2 } = await Promise.resolve().then(() => (init_fork_title(), fork_title_exports));
158800
- const label = await summarizeForkPrompt2(message);
158801
- if (!label) return;
158802
- const current5 = await getTask2(forkId);
158803
- if (current5.title !== placeholderTitle) {
158804
- log.session.info("fork title refine skipped \u2014 title changed since fork", { taskId: forkId });
158805
- return;
158806
- }
158807
- const refinedTitle = `${label} - fork of ${sourceTitle}`;
158808
- const { task: updated } = await updateTask2(forkId, { title: refinedTitle }, { source: "fork-title" });
158809
- bus.emit(EventNames.TASK_UPDATED, { task: updated }, ["web-ui", "main-agent"], { source: "fork-title" });
158810
- log.session.info("fork title refined", { taskId: forkId, title: refinedTitle });
158811
- } catch (err2) {
158812
- log.session.warn("fork title refine failed", {
158813
- taskId: forkId,
158814
- error: err2 instanceof Error ? err2.message : String(err2)
158815
- });
158816
- }
158817
- })();
158818
- }
158819
- if (forkGroupId && !sourceTask.group_id) {
158820
- const gid = forkGroupId;
158821
- const seedTitles = [sourceTask.title, newTitle];
158822
- void (async () => {
158823
- try {
158824
- const { summarizeGroupLabel: summarizeGroupLabel2 } = await Promise.resolve().then(() => (init_fork_title(), fork_title_exports));
158825
- const label = await summarizeGroupLabel2(seedTitles);
158826
- if (!label) return;
158827
- await renameGroup2(gid, label);
158828
- bus.emit(EventNames.TASK_GROUPS_CHANGED, { group_id: gid, label }, ["web-ui", "main-agent"], { source: "fork" });
158829
- log.session.info("fork group label refined", { groupId: gid, label });
158830
- } catch (err2) {
158831
- log.session.warn("fork group label refine failed", {
158832
- groupId: gid,
158833
- error: err2 instanceof Error ? err2.message : String(err2)
158834
- });
158835
- }
158836
- })();
158837
- }
158838
159234
  } else {
158839
159235
  try {
158840
159236
  task = await getTask2(task_id);
@@ -158856,7 +159252,7 @@ async function forkSessionToTask(sourceSessionId, input, source = "web-api") {
158856
159252
  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";
158857
159253
  const forkMessage = `${FORK_FOCUS_PREFIX}${imageContext ?? ""}New request:
158858
159254
  ${userRequest}`;
158859
- const forkSessionId = randomUUID10();
159255
+ const forkSessionId = randomUUID11();
158860
159256
  const forkTitle = title ?? `Fork of ${sourceRecord.title ?? sourceSessionId.slice(0, 16)}`;
158861
159257
  try {
158862
159258
  await createSessionRecord2(forkSessionId, task.id, task.project ?? "", sourceRecord.cwd, {
@@ -159579,7 +159975,7 @@ __export(session_rewind_exports, {
159579
159975
  probeRewindViaDaemon: () => probeRewindViaDaemon,
159580
159976
  rewindSessionToMessage: () => rewindSessionToMessage
159581
159977
  });
159582
- import { randomUUID as randomUUID11 } from "crypto";
159978
+ import { randomUUID as randomUUID12 } from "crypto";
159583
159979
  function cutAncestorHistoryAtRewindPoint(ancestorMessages, rewoundAtMessageUuid) {
159584
159980
  if (!rewoundAtMessageUuid) {
159585
159981
  return { messages: ancestorMessages, found: false, dropped: 0 };
@@ -159780,7 +160176,7 @@ async function rewindSessionToMessage(sessionId, input, source = "web-api") {
159780
160176
  error: err2 instanceof Error ? err2.message : String(err2)
159781
160177
  });
159782
160178
  }
159783
- const rewoundId = randomUUID11();
160179
+ const rewoundId = randomUUID12();
159784
160180
  const baseTitle = (record.title ?? sessionId.slice(0, 16)).replace(/\s*\(rewound\)$/i, "");
159785
160181
  const title = `${baseTitle} (rewound)`.slice(0, 200);
159786
160182
  const { createSessionRecord: createSessionRecord2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
@@ -160500,6 +160896,7 @@ function parseSessionMessages(content, opts) {
160500
160896
  }
160501
160897
  }
160502
160898
  let lastPlanContent = null;
160899
+ let dropWarmupReply = false;
160503
160900
  const resultParentIds = [];
160504
160901
  const result = [];
160505
160902
  for (const [mapMsgId, msg] of messageMap) {
@@ -160577,10 +160974,18 @@ ${imgPath}` : imgPath;
160577
160974
  let text = textParts.join("\n").trim();
160578
160975
  if (!text && tools2.length === 0 && !thinking) continue;
160579
160976
  if (msg.role === "user" && text.startsWith("<")) {
160977
+ if (isCacheWarmupText(text)) {
160978
+ dropWarmupReply = true;
160979
+ continue;
160980
+ }
160580
160981
  const transformed = transformInjectedUserText(text);
160581
160982
  if (transformed === null) continue;
160582
160983
  if (transformed !== void 0) text = transformed;
160583
160984
  }
160985
+ if (msg.role === "assistant" && dropWarmupReply) {
160986
+ dropWarmupReply = false;
160987
+ if (tools2.length === 0) continue;
160988
+ }
160584
160989
  if (msg.role === "user") text = stripOutputModeWrappers(text);
160585
160990
  if (msg.role === "assistant" && !text && tools2.length === 0) continue;
160586
160991
  result.push({
@@ -161531,6 +161936,7 @@ var init_session_history = __esm({
161531
161936
  init_session_io();
161532
161937
  init_constants();
161533
161938
  init_remote_image_mirror();
161939
+ init_side_thread_warmup();
161534
161940
  init_session_file_reader();
161535
161941
  LOCAL_HOME = os36.homedir();
161536
161942
  TAIL_SEGMENT_ROLL_BYTES = 4 * 1024 * 1024;
@@ -161678,7 +162084,7 @@ var init_context_window = __esm({
161678
162084
  });
161679
162085
 
161680
162086
  // src/providers/turn-ledger.ts
161681
- import { randomUUID as randomUUID12 } from "crypto";
162087
+ import { randomUUID as randomUUID13 } from "crypto";
161682
162088
  function openTurn(sessionId) {
161683
162089
  const existing = openTurns.get(sessionId);
161684
162090
  if (existing && !existing.settled) {
@@ -161688,7 +162094,7 @@ function openTurn(sessionId) {
161688
162094
  });
161689
162095
  return { turnId: existing.turnId, promise: existing.promise };
161690
162096
  }
161691
- const turnId = randomUUID12();
162097
+ const turnId = randomUUID13();
161692
162098
  let resolve;
161693
162099
  let reject;
161694
162100
  const promise = new Promise((res, rej) => {
@@ -168436,11 +168842,39 @@ var init_claude_code_session = __esm({
168436
168842
  session2.pendingDescription = message.slice(0, 500);
168437
168843
  let appendSystemPrompt;
168438
168844
  const isFork = !!data.forkedFromSessionId;
168439
- if (data.appendSystemPrompt && !isFork) {
168440
- appendSystemPrompt = data.appendSystemPrompt;
168441
- log.session.info("using caller-provided system prompt", { taskId: taskId2, promptLength: data.appendSystemPrompt.length });
168442
- }
168443
- {
168845
+ const { parseSideLaneKey: parseSideLaneKey2 } = await Promise.resolve().then(() => (init_side_thread_fork(), side_thread_fork_exports));
168846
+ const sideIds = parseSideLaneKey2(data.lane);
168847
+ if (sideIds) {
168848
+ let exact = data.appendSystemPromptExact;
168849
+ let source = "live-or-record";
168850
+ if (exact === void 0) {
168851
+ try {
168852
+ const { getSessionByClaudeId: getSessionByClaudeId2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
168853
+ const { spawnPrefixFromRecord: spawnPrefixFromRecord2 } = await Promise.resolve().then(() => (init_spawn_prefix(), spawn_prefix_exports));
168854
+ const parentRecord = await getSessionByClaudeId2(sideIds.parentSid);
168855
+ const prefix = parentRecord ? spawnPrefixFromRecord2(parentRecord) : null;
168856
+ exact = prefix?.appendSystemPrompt ?? null;
168857
+ source = prefix?.source ?? "no-parent-record";
168858
+ } catch (err2) {
168859
+ exact = null;
168860
+ source = "lookup-failed";
168861
+ log.session.warn("side thread: parent prompt lookup failed \u2014 spawning without an append prompt", {
168862
+ parentSid: sideIds.parentSid,
168863
+ error: err2 instanceof Error ? err2.message : String(err2)
168864
+ });
168865
+ }
168866
+ }
168867
+ appendSystemPrompt = exact ?? void 0;
168868
+ log.session.info("side thread: prefix prompt resolved", {
168869
+ parentSid: sideIds.parentSid,
168870
+ source,
168871
+ promptLength: appendSystemPrompt?.length ?? 0
168872
+ });
168873
+ } else {
168874
+ if (data.appendSystemPrompt && !isFork) {
168875
+ appendSystemPrompt = data.appendSystemPrompt;
168876
+ log.session.info("using caller-provided system prompt", { taskId: taskId2, promptLength: data.appendSystemPrompt.length });
168877
+ }
168444
168878
  try {
168445
168879
  const { buildSessionContext: buildSessionContext2 } = await Promise.resolve().then(() => (init_session_context(), session_context_exports));
168446
168880
  const ctx = await buildSessionContext2(taskId2 ?? "", cwd, data.host);
@@ -168458,32 +168892,6 @@ ${ctx.systemPrompt}` : ctx.systemPrompt;
168458
168892
  log.session.warn("failed to build session context", { taskId: taskId2, error: err2 instanceof Error ? err2.message : String(err2) });
168459
168893
  }
168460
168894
  }
168461
- {
168462
- const { parseSideLaneKey: parseSideLaneKey2 } = await Promise.resolve().then(() => (init_side_thread_fork(), side_thread_fork_exports));
168463
- const sideIds = parseSideLaneKey2(data.lane);
168464
- if (sideIds) {
168465
- try {
168466
- const { getSessionByClaudeId: getSessionByClaudeId2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
168467
- const parentRecord = await getSessionByClaudeId2(sideIds.parentSid);
168468
- const inherited = parentRecord?.appliedAppendSystemPrompt;
168469
- if (inherited && Buffer.byteLength(inherited, "utf8") <= 65536) {
168470
- appendSystemPrompt = inherited;
168471
- log.session.info("side thread: inherited parent system prompt verbatim", {
168472
- parentSid: sideIds.parentSid,
168473
- promptLength: appendSystemPrompt.length
168474
- });
168475
- } else {
168476
- log.session.info("side thread: parent has no stored system prompt \u2014 fresh build (cache prefix will diverge)", {
168477
- parentSid: sideIds.parentSid
168478
- });
168479
- }
168480
- } catch (err2) {
168481
- log.session.warn("side thread: parent prompt lookup failed \u2014 fresh build", {
168482
- error: err2 instanceof Error ? err2.message : String(err2)
168483
- });
168484
- }
168485
- }
168486
- }
168487
168895
  if (data.preassignedSessionId && appendSystemPrompt && Buffer.byteLength(appendSystemPrompt, "utf8") <= 65536) {
168488
168896
  const promptToStore = appendSystemPrompt;
168489
168897
  const sidToStore = data.preassignedSessionId;
@@ -177482,7 +177890,7 @@ __export(mobile_launch_exports, {
177482
177890
  performMobileLaunch: () => performMobileLaunch,
177483
177891
  validateMobileLaunchBody: () => validateMobileLaunchBody
177484
177892
  });
177485
- import { randomUUID as randomUUID13 } from "crypto";
177893
+ import { randomUUID as randomUUID14 } from "crypto";
177486
177894
  function launchErrorCode(status2) {
177487
177895
  if (status2 === 404) return "not_found";
177488
177896
  if (status2 === 409) return "conflict";
@@ -177604,7 +178012,7 @@ async function performMobileLaunch(input, source) {
177604
178012
  );
177605
178013
  }
177606
178014
  }
177607
- const preassignedSessionId = engineCaps(input.engine).idProvisioning === "provider-issued" ? void 0 : randomUUID13();
178015
+ const preassignedSessionId = engineCaps(input.engine).idProvisioning === "provider-issued" ? void 0 : randomUUID14();
177608
178016
  const task = await quickStartSession({
177609
178017
  message: input.message,
177610
178018
  cwd: input.cwd,
@@ -178031,6 +178439,7 @@ __export(daemon_connection_exports, {
178031
178439
  isDaemonConnected: () => isDaemonConnected,
178032
178440
  isPooledConnection: () => isPooledConnection,
178033
178441
  probeDaemonSession: () => probeDaemonSession,
178442
+ probeDaemonSessionArgs: () => probeDaemonSessionArgs,
178034
178443
  pushDaemonHooksToAllHosts: () => pushDaemonHooksToAllHosts,
178035
178444
  setOnDaemonHostConnected: () => setOnDaemonHostConnected,
178036
178445
  setOnDaemonStatusChange: () => setOnDaemonStatusChange,
@@ -178207,6 +178616,28 @@ async function probeDaemonSession(hostKey2, sessionId) {
178207
178616
  return null;
178208
178617
  }
178209
178618
  }
178619
+ async function probeDaemonSessionArgs(hostKey2, sessionId, deadlineMs = 2e3) {
178620
+ const conn = getConnectedDaemonConnection(hostKey2);
178621
+ if (!conn) return null;
178622
+ try {
178623
+ const result = await Promise.race([
178624
+ conn.send("status", { sid: sessionId, includeArgs: true }),
178625
+ new Promise((_, reject) => setTimeout(
178626
+ () => reject(new Error(`status probe exceeded ${deadlineMs}ms`)),
178627
+ deadlineMs
178628
+ ).unref?.())
178629
+ ]);
178630
+ if (!result.ok || !result.alive || !Array.isArray(result.args)) return null;
178631
+ return result.args.filter((a8) => typeof a8 === "string");
178632
+ } catch (err2) {
178633
+ log.session.debug("probeDaemonSessionArgs: status probe failed", {
178634
+ hostKey: hostKey2,
178635
+ sessionId,
178636
+ error: err2 instanceof Error ? err2.message : String(err2)
178637
+ });
178638
+ return null;
178639
+ }
178640
+ }
178210
178641
  var execFileAsync23, MOBILE_ENQUEUE_LEDGER_MAX, recentMobileEnqueues, DaemonConnection, onPoolStatusChange, onHostConnected, hostConnectedListeners, connectionPool, connectingPromises, failureCache, FAILURE_CACHE_TTL_MS;
178211
178642
  var init_daemon_connection = __esm({
178212
178643
  "src/providers/daemon-connection.ts"() {
@@ -182774,6 +183205,7 @@ async function createSessionRecord(claudeSessionId, taskId2, project, cwd, extra
182774
183205
  ...extra?.effort ? { effort: extra.effort } : {},
182775
183206
  ...extra?.profile ? { profile: extra.profile } : {},
182776
183207
  ...extra?.lane ? { lane: extra.lane } : {},
183208
+ ...extra?.appliedAppendSystemPrompt !== void 0 ? { appliedAppendSystemPrompt: extra.appliedAppendSystemPrompt } : {},
182777
183209
  ...extra?.engine ? { engine: extra.engine } : {},
182778
183210
  ...extra?.acpRuntimeId ? { acpRuntimeId: extra.acpRuntimeId } : {},
182779
183211
  ...extra?.acpJournalPath ? { acpJournalPath: extra.acpJournalPath } : {},
@@ -182928,7 +183360,7 @@ function applyUpdateToSession(session2, updates, logLabel) {
182928
183360
  };
182929
183361
  if (count2 === 1) log.session.info("legacy status write suppressed", payload);
182930
183362
  else log.session.debug("legacy status write suppressed", payload);
182931
- if (updates.process_status === "error") {
183363
+ if (updates.process_status === "error" || updates.process_status === "stopped") {
182932
183364
  noteSuppressedErrorReason(session2.claudeSessionId, {
182933
183365
  reason: typeof reason === "string" ? reason : "unknown",
182934
183366
  message: typeof updates.errorMessage === "string" ? updates.errorMessage : void 0,