@schoolai/shipyard 3.11.0 → 3.11.1

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 (60) hide show
  1. package/dist/{auth-GGM253LQ.js → auth-AUY74PMB.js} +3 -3
  2. package/dist/capability-detector-worker.js +8 -8
  3. package/dist/{chunk-R3XQ6W7L.js → chunk-4SYLDZTY.js} +4 -4
  4. package/dist/{chunk-C6QOTETH.js → chunk-5LWD5W7O.js} +24 -10
  5. package/dist/chunk-5LWD5W7O.js.map +1 -0
  6. package/dist/{chunk-IJHF4OM4.js → chunk-5W5N5U2S.js} +2 -2
  7. package/dist/{chunk-L7ELOV3S.js → chunk-FVZ5BDZS.js} +4 -4
  8. package/dist/chunk-FVZ5BDZS.js.map +1 -0
  9. package/dist/{chunk-RW2OTTUA.js → chunk-KYLYGFMH.js} +4 -4
  10. package/dist/{chunk-QJP7JCIS.js → chunk-LRNGLC4V.js} +41 -3
  11. package/dist/chunk-LRNGLC4V.js.map +1 -0
  12. package/dist/{chunk-A2UK6TW2.js → chunk-LZSMNUAI.js} +18 -1
  13. package/dist/{chunk-A2UK6TW2.js.map → chunk-LZSMNUAI.js.map} +1 -1
  14. package/dist/{chunk-Z37T5W6S.js → chunk-P2HZDIN7.js} +12 -7
  15. package/dist/chunk-P2HZDIN7.js.map +1 -0
  16. package/dist/{chunk-ZRJTZLRF.js → chunk-QKJNVVQ3.js} +4 -4
  17. package/dist/{chunk-2EQOL57Z.js → chunk-TFRYQDDG.js} +2 -2
  18. package/dist/{chunk-YXPPZQBJ.js → chunk-VL5RUCRF.js} +164 -37
  19. package/dist/chunk-VL5RUCRF.js.map +1 -0
  20. package/dist/{chunk-3WEEGJJN.js → chunk-X5KCX6ZS.js} +2 -2
  21. package/dist/{chunk-GM6MH4CD.js → chunk-XIEOWUPV.js} +2 -2
  22. package/dist/{chunk-6LINHACK.js → chunk-Y5UWRARP.js} +47 -24
  23. package/dist/chunk-Y5UWRARP.js.map +1 -0
  24. package/dist/cursor-runner.js +88 -62
  25. package/dist/cursor-runner.js.map +1 -1
  26. package/dist/electron-utility.js +5 -5
  27. package/dist/{git-repo-QNGPCJLI.js → git-repo-CTZJS3ER.js} +6 -4
  28. package/dist/index.js +8 -8
  29. package/dist/{logger-2F3CBS3V.js → logger-AN7EUK2B.js} +7 -5
  30. package/dist/{login-U256OVOJ.js → login-YB34LF4L.js} +6 -6
  31. package/dist/{logout-HY3MPOY5.js → logout-GUXVSWLZ.js} +5 -5
  32. package/dist/{mcp-servers-ICHOWXZB.js → mcp-servers-OAPQNDA7.js} +4 -4
  33. package/dist/{roi-YM5OOWHG.js → roi-NXJHL5X2.js} +3 -3
  34. package/dist/{serve-D5GKV2RU.js → serve-P3U2C5YH.js} +1175 -739
  35. package/dist/{serve-D5GKV2RU.js.map → serve-P3U2C5YH.js.map} +1 -1
  36. package/dist/{skills-W2Y6TWHA.js → skills-2UBVHFQ5.js} +2 -2
  37. package/dist/{start-JY26XC5R.js → start-Y34X3WVF.js} +10 -10
  38. package/package.json +1 -1
  39. package/dist/chunk-6LINHACK.js.map +0 -1
  40. package/dist/chunk-C6QOTETH.js.map +0 -1
  41. package/dist/chunk-L7ELOV3S.js.map +0 -1
  42. package/dist/chunk-QJP7JCIS.js.map +0 -1
  43. package/dist/chunk-YXPPZQBJ.js.map +0 -1
  44. package/dist/chunk-Z37T5W6S.js.map +0 -1
  45. /package/dist/{auth-GGM253LQ.js.map → auth-AUY74PMB.js.map} +0 -0
  46. /package/dist/{chunk-R3XQ6W7L.js.map → chunk-4SYLDZTY.js.map} +0 -0
  47. /package/dist/{chunk-IJHF4OM4.js.map → chunk-5W5N5U2S.js.map} +0 -0
  48. /package/dist/{chunk-RW2OTTUA.js.map → chunk-KYLYGFMH.js.map} +0 -0
  49. /package/dist/{chunk-ZRJTZLRF.js.map → chunk-QKJNVVQ3.js.map} +0 -0
  50. /package/dist/{chunk-2EQOL57Z.js.map → chunk-TFRYQDDG.js.map} +0 -0
  51. /package/dist/{chunk-3WEEGJJN.js.map → chunk-X5KCX6ZS.js.map} +0 -0
  52. /package/dist/{chunk-GM6MH4CD.js.map → chunk-XIEOWUPV.js.map} +0 -0
  53. /package/dist/{git-repo-QNGPCJLI.js.map → git-repo-CTZJS3ER.js.map} +0 -0
  54. /package/dist/{logger-2F3CBS3V.js.map → logger-AN7EUK2B.js.map} +0 -0
  55. /package/dist/{login-U256OVOJ.js.map → login-YB34LF4L.js.map} +0 -0
  56. /package/dist/{logout-HY3MPOY5.js.map → logout-GUXVSWLZ.js.map} +0 -0
  57. /package/dist/{mcp-servers-ICHOWXZB.js.map → mcp-servers-OAPQNDA7.js.map} +0 -0
  58. /package/dist/{roi-YM5OOWHG.js.map → roi-NXJHL5X2.js.map} +0 -0
  59. /package/dist/{skills-W2Y6TWHA.js.map → skills-2UBVHFQ5.js.map} +0 -0
  60. /package/dist/{start-JY26XC5R.js.map → start-Y34X3WVF.js.map} +0 -0
@@ -12,7 +12,7 @@ import {
12
12
  guardedSubscribe,
13
13
  makeInitialAttemptState,
14
14
  shutdownFileWatcherGuard
15
- } from "./chunk-L7ELOV3S.js";
15
+ } from "./chunk-FVZ5BDZS.js";
16
16
  import {
17
17
  TRAILER_SCHEMA_VERSION,
18
18
  appendTrailerToMessage,
@@ -25,10 +25,10 @@ import {
25
25
  } from "./chunk-WN5Q6WBU.js";
26
26
  import {
27
27
  loadAuthToken
28
- } from "./chunk-3WEEGJJN.js";
28
+ } from "./chunk-X5KCX6ZS.js";
29
29
  import {
30
30
  getDaemonVersion
31
- } from "./chunk-ZRJTZLRF.js";
31
+ } from "./chunk-QKJNVVQ3.js";
32
32
  import {
33
33
  getRecentWasmPanicMessages
34
34
  } from "./chunk-7H34LI75.js";
@@ -146,12 +146,12 @@ import {
146
146
  translateWarningNotification,
147
147
  unresolveReviewThread,
148
148
  writeCursorApiKeyToVault
149
- } from "./chunk-C6QOTETH.js";
149
+ } from "./chunk-5LWD5W7O.js";
150
150
  import {
151
151
  buildNodeSpawnEnv,
152
152
  nodeSpawnEnvOverlay,
153
153
  rewriteElectronNodeScriptSpawn
154
- } from "./chunk-GM6MH4CD.js";
154
+ } from "./chunk-XIEOWUPV.js";
155
155
  import {
156
156
  TIMEOUT_MS,
157
157
  addWorktreeToCache,
@@ -164,7 +164,7 @@ import {
164
164
  removeWorktreeFromCache,
165
165
  run,
166
166
  runWithTimeout
167
- } from "./chunk-6LINHACK.js";
167
+ } from "./chunk-Y5UWRARP.js";
168
168
  import {
169
169
  gitExecSafe
170
170
  } from "./chunk-4T2OQAVL.js";
@@ -182,7 +182,7 @@ import {
182
182
  redactEnv,
183
183
  resolveEnabledMcpServers,
184
184
  resolveStdioEnv
185
- } from "./chunk-RW2OTTUA.js";
185
+ } from "./chunk-KYLYGFMH.js";
186
186
  import {
187
187
  atomicWriteFile,
188
188
  findProjectRoot,
@@ -193,13 +193,13 @@ import {
193
193
  } from "./chunk-RR6V6SNM.js";
194
194
  import {
195
195
  classifySkill
196
- } from "./chunk-2EQOL57Z.js";
196
+ } from "./chunk-TFRYQDDG.js";
197
197
  import {
198
198
  createOneShotPool
199
199
  } from "./chunk-ZFKJAYAN.js";
200
200
  import {
201
201
  parseRunnerToDaemon
202
- } from "./chunk-A2UK6TW2.js";
202
+ } from "./chunk-LZSMNUAI.js";
203
203
  import {
204
204
  AGENT_IDS,
205
205
  AGENT_SYSTEM_CLAUDE_CODE,
@@ -229,7 +229,7 @@ import {
229
229
  COMMENT_ADD_TOOL,
230
230
  COMMENT_LIST_TOOL,
231
231
  COMMENT_READ_THREAD_TOOL,
232
- COMPACT_TIMEOUT_MS,
232
+ COMPACTION_SILENCE_TIMEOUT_MS,
233
233
  CREATE_TASK_TOOL,
234
234
  CREDENTIAL_STORE_VERSION,
235
235
  CanvasRepository,
@@ -270,6 +270,7 @@ import {
270
270
  LSP_FIND_REFERENCES_TOOL,
271
271
  LSP_GO_TO_DEFINITION_TOOL,
272
272
  LSP_RENAME_SYMBOL_TOOL,
273
+ MERMAID_SECURITY_LEVEL,
273
274
  Mark,
274
275
  MarkType,
275
276
  MarkdownParser,
@@ -399,7 +400,7 @@ import {
399
400
  unsafeAssertSourceId,
400
401
  vizFileExtension,
401
402
  withPreferredRuntimeAuthMethod
402
- } from "./chunk-YXPPZQBJ.js";
403
+ } from "./chunk-VL5RUCRF.js";
403
404
  import {
404
405
  AuthGitHubCallbackRequestSchema,
405
406
  AuthGitHubCallbackResponseSchema,
@@ -455,7 +456,7 @@ import {
455
456
  createChildLogger,
456
457
  flushLogger,
457
458
  logger
458
- } from "./chunk-QJP7JCIS.js";
459
+ } from "./chunk-LRNGLC4V.js";
459
460
  import {
460
461
  getShipyardHome,
461
462
  getStallProfilerConfig,
@@ -463,7 +464,7 @@ import {
463
464
  isVanillaAgentMode,
464
465
  resolveNodeExecPath,
465
466
  validateEnv
466
- } from "./chunk-Z37T5W6S.js";
467
+ } from "./chunk-P2HZDIN7.js";
467
468
  import {
468
469
  external_exports
469
470
  } from "./chunk-CNR7O5YH.js";
@@ -472,7 +473,7 @@ import "./chunk-2H7UOFLK.js";
472
473
  // src/services/serve.ts
473
474
  import { mkdir as mkdir44, realpath as realpath3 } from "fs/promises";
474
475
  import { homedir as homedir14 } from "os";
475
- import { join as join86 } from "path";
476
+ import { join as join87 } from "path";
476
477
  import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
477
478
 
478
479
  // ../../node_modules/.pnpm/@logtape+logtape@1.3.7/node_modules/@logtape/logtape/dist/level.js
@@ -10130,11 +10131,11 @@ async function runCapabilityDetectorWithFallback(opts) {
10130
10131
  );
10131
10132
  }
10132
10133
  const cleanupGraceMs = opts.cleanupGraceMs ?? CAPABILITY_DETECTOR_WORKER_CLEANUP_GRACE_MS;
10133
- const spawn15 = opts.spawn ?? defaultSpawn;
10134
+ const spawn16 = opts.spawn ?? defaultSpawn;
10134
10135
  const log = opts.log ?? (() => {
10135
10136
  });
10136
10137
  const startedAt = Date.now();
10137
- const worker = spawn15();
10138
+ const worker = spawn16();
10138
10139
  worker.unref?.();
10139
10140
  return new Promise((resolve10) => {
10140
10141
  let resultSettled = false;
@@ -13134,7 +13135,7 @@ function nanoid(size2 = 21) {
13134
13135
  }
13135
13136
 
13136
13137
  // src/services/bootstrap/signaling.ts
13137
- var DAEMON_NPM_VERSION = true ? "3.11.0" : "unknown";
13138
+ var DAEMON_NPM_VERSION = true ? "3.11.1" : "unknown";
13138
13139
  function createDaemonSignaling(config) {
13139
13140
  const agentId = config.agentId ?? nanoid();
13140
13141
  function send(msg) {
@@ -14307,7 +14308,7 @@ function runTaskSearch(args) {
14307
14308
  }
14308
14309
 
14309
14310
  // src/services/channels/control-channel-wiring.ts
14310
- import { join as join34 } from "path";
14311
+ import { join as join35 } from "path";
14311
14312
 
14312
14313
  // src/shared/type-utils.ts
14313
14314
  function narrow(value) {
@@ -16561,10 +16562,10 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
16561
16562
  respondError2(requestId, formatError(err3));
16562
16563
  }
16563
16564
  }
16564
- const execFileAsync5 = promisify4(execFile4);
16565
+ const execFileAsync4 = promisify4(execFile4);
16565
16566
  async function handleGitStatus(requestId) {
16566
16567
  try {
16567
- const { stdout } = await execFileAsync5("git", ["status", "--porcelain=v1", "-z", "-u"], {
16568
+ const { stdout } = await execFileAsync4("git", ["status", "--porcelain=v1", "-z", "-u"], {
16568
16569
  cwd,
16569
16570
  maxBuffer: 5 * 1024 * 1024
16570
16571
  });
@@ -16646,7 +16647,7 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
16646
16647
  });
16647
16648
  return;
16648
16649
  }
16649
- const { stdout } = await execFileAsync5(
16650
+ const { stdout } = await execFileAsync4(
16650
16651
  "git",
16651
16652
  ["diff", "--name-status", `${mergeBase}..HEAD`],
16652
16653
  { cwd, maxBuffer: 10 * 1024 * 1024 }
@@ -16736,7 +16737,7 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
16736
16737
  }
16737
16738
  async function handleGitStageFile(requestId, safeRelPath) {
16738
16739
  try {
16739
- await execFileAsync5("git", ["add", "--", safeRelPath], { cwd });
16740
+ await execFileAsync4("git", ["add", "--", safeRelPath], { cwd });
16740
16741
  respond({ type: "git_stage_result", requestId });
16741
16742
  } catch (err3) {
16742
16743
  respondError2(requestId, formatError(err3));
@@ -16744,7 +16745,7 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
16744
16745
  }
16745
16746
  async function handleGitUnstageFile(requestId, safeRelPath) {
16746
16747
  try {
16747
- await execFileAsync5("git", ["restore", "--staged", "--", safeRelPath], { cwd });
16748
+ await execFileAsync4("git", ["restore", "--staged", "--", safeRelPath], { cwd });
16748
16749
  respond({ type: "git_unstage_result", requestId });
16749
16750
  } catch (err3) {
16750
16751
  respondError2(requestId, formatError(err3));
@@ -16754,7 +16755,7 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
16754
16755
  try {
16755
16756
  const indexContent = await readGitObject(`:${safeRelPath}`);
16756
16757
  if (indexContent === null) {
16757
- await execFileAsync5("git", ["add", "--", safeRelPath], { cwd });
16758
+ await execFileAsync4("git", ["add", "--", safeRelPath], { cwd });
16758
16759
  respond({ type: "git_stage_result", requestId });
16759
16760
  return;
16760
16761
  }
@@ -16789,12 +16790,12 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
16789
16790
  const relPath = relative(cwd, absPath);
16790
16791
  if (!await validateMutationTarget(requestId, relPath, absPath)) return;
16791
16792
  try {
16792
- await execFileAsync5("git", ["checkout", "--", relPath], { cwd });
16793
+ await execFileAsync4("git", ["checkout", "--", relPath], { cwd });
16793
16794
  respond({ type: "git_discard_file_result", requestId });
16794
16795
  } catch {
16795
16796
  try {
16796
16797
  await unlink5(absPath);
16797
- await execFileAsync5("git", ["reset", "HEAD", "--", relPath], { cwd }).catch(
16798
+ await execFileAsync4("git", ["reset", "HEAD", "--", relPath], { cwd }).catch(
16798
16799
  (resetErr) => {
16799
16800
  log({ event: "git_reset_head_failed", path: relPath, error: formatError(resetErr) });
16800
16801
  }
@@ -16807,7 +16808,7 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
16807
16808
  }
16808
16809
  async function readGitObject(ref) {
16809
16810
  try {
16810
- const { stdout } = await execFileAsync5("git", ["show", ref], {
16811
+ const { stdout } = await execFileAsync4("git", ["show", ref], {
16811
16812
  cwd,
16812
16813
  maxBuffer: 10 * 1024 * 1024
16813
16814
  });
@@ -16818,7 +16819,7 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
16818
16819
  }
16819
16820
  async function getFileMode(safeRelPath) {
16820
16821
  try {
16821
- const { stdout } = await execFileAsync5("git", ["ls-files", "-s", "--", safeRelPath], { cwd });
16822
+ const { stdout } = await execFileAsync4("git", ["ls-files", "-s", "--", safeRelPath], { cwd });
16822
16823
  const match = /^(\d+)\s/.exec(stdout);
16823
16824
  if (match?.[1]) return match[1];
16824
16825
  } catch {
@@ -16830,7 +16831,7 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
16830
16831
  const mode = isNew ? "100644" : await getFileMode(safeRelPath);
16831
16832
  const args = ["update-index", "--cacheinfo", `${mode},${blobHash},${safeRelPath}`];
16832
16833
  if (isNew) args.splice(1, 0, "--add");
16833
- await execFileAsync5("git", args, { cwd });
16834
+ await execFileAsync4("git", args, { cwd });
16834
16835
  }
16835
16836
  function gitHashObject(content, pathHint) {
16836
16837
  return new Promise((resolve10, reject) => {
@@ -17960,7 +17961,7 @@ body { min-height: 100vh; display: flex; align-items: center; justify-content: c
17960
17961
  <script type="module">
17961
17962
  import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
17962
17963
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
17963
- mermaid.initialize({ startOnLoad: true, theme: prefersDark ? 'dark' : 'default', securityLevel: 'strict' });
17964
+ mermaid.initialize({ startOnLoad: true, theme: prefersDark ? 'dark' : 'default', securityLevel: '${MERMAID_SECURITY_LEVEL}' });
17964
17965
  </script>
17965
17966
  </body>
17966
17967
  </html>
@@ -20399,6 +20400,13 @@ var TELEMETERED_EVENT_NAMES = [
20399
20400
  "harness_mcp_registration_failed",
20400
20401
  "codex_auth_divergence_detected",
20401
20402
  "cursor_auth_divergence_detected",
20403
+ /**
20404
+ * A Codex remote-compact cycle failed with a 401 "Missing bearer" error —
20405
+ * the app-server lost its in-memory token during refresh-token rotation.
20406
+ * Emitted once per compact attempt (not per retry) so fleet dashboards can
20407
+ * surface the storm rate without log amplification.
20408
+ */
20409
+ "codex_compact_auth_failure",
20402
20410
  "event_loop_stall",
20403
20411
  "machine_sleep_artifact",
20404
20412
  /**
@@ -20410,7 +20418,16 @@ var TELEMETERED_EVENT_NAMES = [
20410
20418
  */
20411
20419
  "electron_app_metrics",
20412
20420
  /** Electron main cleared a wedged Squirrel.Mac staged install on launch. */
20413
- "squirrel_install_stuck_recovered"
20421
+ "squirrel_install_stuck_recovered",
20422
+ /**
20423
+ * An ALO control-channel entry exhausted its maxDeliver retry budget
20424
+ * without receiving an ack. Payload: { attempts: number, channel: 'control' }.
20425
+ * Emitted via the `onDeadLetter` callback in control-channel-wiring.ts.
20426
+ * Complements the `alo_resend_exhausted` ERROR log event emitted by the
20427
+ * shell itself; this telemetry path routes it to D1 for fleet-wide
20428
+ * observability so silent resend exhaustion is detectable in aggregate.
20429
+ */
20430
+ "alo_resend_exhausted"
20414
20431
  ];
20415
20432
  var TELEMETERED_EVENTS = new Set(TELEMETERED_EVENT_NAMES);
20416
20433
  var sink = null;
@@ -22428,8 +22445,54 @@ function withListWorktreeContext(context) {
22428
22445
  ...context?.reason !== void 0 && { reason: context.reason }
22429
22446
  };
22430
22447
  }
22448
+ function logWorktreePollDegraded(logAdapter, repoPath, err3, requestContext) {
22449
+ const repoExists = existsSync2(repoPath);
22450
+ if (err3 instanceof GitExecError) {
22451
+ const { code, signal, killed, stderr, durationMs } = err3.info;
22452
+ logAdapter({
22453
+ event: "worktree_poll_degraded",
22454
+ repoPath,
22455
+ code,
22456
+ signal,
22457
+ killed,
22458
+ stderr,
22459
+ durationMs,
22460
+ repoExists,
22461
+ permanent: !repoExists,
22462
+ ...requestContext
22463
+ });
22464
+ return;
22465
+ }
22466
+ const error = err3 instanceof Error ? err3.message : String(err3);
22467
+ logAdapter({
22468
+ event: "worktree_poll_degraded",
22469
+ repoPath,
22470
+ error,
22471
+ repoExists,
22472
+ permanent: !repoExists,
22473
+ ...requestContext
22474
+ });
22475
+ }
22476
+ function handleWorktreeListError(err3, repoPath, degradedRepos, logAdapter, requestContext) {
22477
+ if (err3 instanceof CircuitOpenError) {
22478
+ if (!degradedRepos.has(repoPath)) {
22479
+ degradedRepos.add(repoPath);
22480
+ logAdapter({ event: "worktree_poll_degraded", repoPath, reason: "circuit_open" });
22481
+ }
22482
+ return true;
22483
+ }
22484
+ if (err3 instanceof BackoffWaitError) {
22485
+ return true;
22486
+ }
22487
+ if (!degradedRepos.has(repoPath)) {
22488
+ degradedRepos.add(repoPath);
22489
+ logWorktreePollDegraded(logAdapter, repoPath, err3, requestContext);
22490
+ }
22491
+ return false;
22492
+ }
22431
22493
  function buildWorktreeHandlers(deps) {
22432
22494
  const { daemon, logAdapter, worktreeRunner } = deps;
22495
+ const degradedRepos = /* @__PURE__ */ new Set();
22433
22496
  return {
22434
22497
  onCreateWorktree: (requestId, sourceRepoPath, branchName, baseRef, setupScript, mode) => {
22435
22498
  logAdapter({ event: "worktree_create_requested", requestId, sourceRepoPath, branchName });
@@ -22458,54 +22521,26 @@ function buildWorktreeHandlers(deps) {
22458
22521
  const requestContext = withListWorktreeContext(context);
22459
22522
  logAdapter({ event: "worktree_list_requested", repoPath, ...requestContext });
22460
22523
  listWorktrees2(repoPath).then((worktrees) => {
22524
+ if (degradedRepos.has(repoPath)) {
22525
+ degradedRepos.delete(repoPath);
22526
+ logAdapter({ event: "worktree_poll_recovered", repoPath });
22527
+ }
22461
22528
  deps.controlHandler().sendControl({ type: "worktree_list", repoPath, worktrees });
22462
22529
  }).catch((err3) => {
22463
- if (err3 instanceof CircuitOpenError) {
22464
- logAdapter({
22465
- event: "worktree_list_skipped_circuit_open",
22466
- repoPath,
22467
- ...requestContext
22468
- });
22469
- return;
22470
- }
22471
- if (err3 instanceof BackoffWaitError) {
22472
- logAdapter({
22473
- event: "worktree_list_skipped_backoff",
22474
- repoPath,
22475
- waitMs: err3.waitMs,
22476
- ...requestContext
22477
- });
22478
- return;
22479
- }
22480
- const msg = err3 instanceof Error ? err3.message : String(err3);
22481
- const repoExists = existsSync2(repoPath);
22482
- if (err3 instanceof GitExecError) {
22483
- const { code, signal, killed, stderr, durationMs } = err3.info;
22484
- logAdapter({
22485
- event: "worktree_list_failed",
22486
- repoPath,
22487
- error: msg,
22488
- code,
22489
- signal,
22490
- killed,
22491
- stderr,
22492
- durationMs,
22493
- repoExists,
22494
- ...requestContext
22495
- });
22496
- } else {
22497
- logAdapter({
22498
- event: "worktree_list_failed",
22499
- repoPath,
22500
- error: msg,
22501
- repoExists,
22502
- ...requestContext
22530
+ const suppress = handleWorktreeListError(
22531
+ err3,
22532
+ repoPath,
22533
+ degradedRepos,
22534
+ logAdapter,
22535
+ requestContext
22536
+ );
22537
+ if (!suppress) {
22538
+ const msg = err3 instanceof Error ? err3.message : String(err3);
22539
+ deps.controlHandler().sendControl({
22540
+ type: "error",
22541
+ error: `Failed to list worktrees: ${msg}`
22503
22542
  });
22504
22543
  }
22505
- deps.controlHandler().sendControl({
22506
- type: "error",
22507
- error: `Failed to list worktrees: ${msg}`
22508
- });
22509
22544
  });
22510
22545
  },
22511
22546
  onRemoveWorktree: (worktreePath) => {
@@ -23188,7 +23223,7 @@ async function refreshPluginCapabilities(daemon, tokenStore) {
23188
23223
  const userSettings = await daemon.userSettingsStore.getSettings();
23189
23224
  const [updatedMarketplace, updatedMcp, updatedSkills] = await Promise.all([
23190
23225
  detectMarketplacePlugins(daemon.capabilities.marketplacePlugins),
23191
- import("./mcp-servers-ICHOWXZB.js").then(
23226
+ import("./mcp-servers-OAPQNDA7.js").then(
23192
23227
  (m2) => m2.detectMCPServers(
23193
23228
  daemon.capabilities.environments,
23194
23229
  tokenStore,
@@ -23197,7 +23232,7 @@ async function refreshPluginCapabilities(daemon, tokenStore) {
23197
23232
  readPreferredClaudeAuthMethod(userSettings)
23198
23233
  )
23199
23234
  ),
23200
- import("./skills-W2Y6TWHA.js").then(
23235
+ import("./skills-2UBVHFQ5.js").then(
23201
23236
  (m2) => m2.detectSkills(daemon.capabilities.environments)
23202
23237
  )
23203
23238
  ]);
@@ -25666,7 +25701,7 @@ function spawnCodexLogout(callbacks) {
25666
25701
 
25667
25702
  // src/services/serve-factory-helpers.ts
25668
25703
  import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
25669
- import { join as join28 } from "path";
25704
+ import { join as join29 } from "path";
25670
25705
 
25671
25706
  // src/shared/capabilities/runtime/rate-limit-account-key.ts
25672
25707
  function providerForProfile(profile) {
@@ -27337,6 +27372,19 @@ function matchCollabQueueCorrelationIds(messages, queueEntries) {
27337
27372
  return claimedIds;
27338
27373
  }
27339
27374
 
27375
+ // src/services/session/codex-home-isolation.ts
27376
+ import { join as join27 } from "path";
27377
+ var ISOLATED_CODEX_HOME_DIR = "codex-isolated";
27378
+ function getIsolatedCodexHome() {
27379
+ return join27(getShipyardHome(), ISOLATED_CODEX_HOME_DIR);
27380
+ }
27381
+ function buildAgentSubprocessCodexHomeEnv() {
27382
+ return { CODEX_HOME: getIsolatedCodexHome() };
27383
+ }
27384
+ function buildCodexShellCodexHomeConfigOverride() {
27385
+ return { "shell_environment_policy.set.CODEX_HOME": getIsolatedCodexHome() };
27386
+ }
27387
+
27340
27388
  // src/services/session/mcp-settle.ts
27341
27389
  var STATUSES_STILL_STARTING = /* @__PURE__ */ new Set(["starting", "pending", "connecting"]);
27342
27390
  async function waitForMcpServersToSettle(opts) {
@@ -27687,6 +27735,13 @@ var AnthropicAgentSubprocess = class _AnthropicAgentSubprocess {
27687
27735
  #onEvent;
27688
27736
  #spawnMcpServers;
27689
27737
  #harnessTaskIdSetter = null;
27738
+ /**
27739
+ * The epoch bound to this subprocess's in-process harness server at spawn
27740
+ * time (installed by serve-factory immediately after `setHarnessEpoch`).
27741
+ * Surfaced via the `harnessEpoch` getter so the pre-warm adoption handshake
27742
+ * writes the SAME epoch to the registry that the harness fence checks.
27743
+ */
27744
+ #harnessEpoch = null;
27690
27745
  /**
27691
27746
  * Mutable slot for the CwdChanged handler. The SDK hook is registered
27692
27747
  * unconditionally at spawn time and reads through this slot on each
@@ -27745,6 +27800,18 @@ var AnthropicAgentSubprocess = class _AnthropicAgentSubprocess {
27745
27800
  installHarnessTaskIdSetter(setter) {
27746
27801
  this.#harnessTaskIdSetter = setter;
27747
27802
  }
27803
+ /**
27804
+ * Record the epoch serve-factory minted and bound to this subprocess's
27805
+ * in-process harness server (called right after `setHarnessEpoch`). The
27806
+ * pre-warm manager reads it back via `harnessEpoch` so adoption registers
27807
+ * the same epoch the fence will check — never a second, divergent UUID.
27808
+ */
27809
+ installHarnessEpoch(epoch) {
27810
+ this.#harnessEpoch = epoch;
27811
+ }
27812
+ get harnessEpoch() {
27813
+ return this.#harnessEpoch;
27814
+ }
27748
27815
  setHarnessTaskId(taskId) {
27749
27816
  if (!this.#harnessTaskIdSetter) {
27750
27817
  throw new Error("setHarnessTaskId called before installHarnessTaskIdSetter");
@@ -27903,7 +27970,8 @@ var AnthropicAgentSubprocess = class _AnthropicAgentSubprocess {
27903
27970
  ...process.env,
27904
27971
  ...options.env,
27905
27972
  DISABLE_AUTO_COMPACT: "1",
27906
- CLAUDE_CODE_ENTRYPOINT: "shipyard"
27973
+ CLAUDE_CODE_ENTRYPOINT: "shipyard",
27974
+ ...buildAgentSubprocessCodexHomeEnv()
27907
27975
  },
27908
27976
  disallowedTools: options.disallowedTools,
27909
27977
  skills: options.skills,
@@ -28389,7 +28457,7 @@ import { homedir as homedir4 } from "os";
28389
28457
 
28390
28458
  // src/shared/capabilities/capabilities-cache.ts
28391
28459
  import { mkdir as mkdir13, readFile as readFile18, rename as rename11, unlink as unlink8, writeFile as writeFile14 } from "fs/promises";
28392
- import { dirname as dirname15, join as join27 } from "path";
28460
+ import { dirname as dirname15, join as join28 } from "path";
28393
28461
  var CAPABILITIES_CACHE_VERSION = 1;
28394
28462
  var GitRepoInfoCacheSchema = external_exports.object({
28395
28463
  path: external_exports.string(),
@@ -28423,7 +28491,7 @@ var CacheFileSchema = external_exports.object({
28423
28491
  mcpServers: external_exports.array(MCPServerInfoCacheSchema)
28424
28492
  });
28425
28493
  function cacheFilePath() {
28426
- return join27(getShipyardHome(), "data", "capabilities-cache.json");
28494
+ return join28(getShipyardHome(), "data", "capabilities-cache.json");
28427
28495
  }
28428
28496
  var EMPTY_ENVIRONMENTS = [];
28429
28497
  var EMPTY_MCP_SERVERS = [];
@@ -28689,7 +28757,7 @@ function appendDynamicPrompt(base2, appendix) {
28689
28757
  ${appendix}` : base2;
28690
28758
  }
28691
28759
  function rehydrateVizRegistry(registry, vizWatcher, resolvedTaskId, vizDir, log) {
28692
- const registryPath = join28(vizDir, resolvedTaskId, "registry.json");
28760
+ const registryPath = join29(vizDir, resolvedTaskId, "registry.json");
28693
28761
  if (!existsSync3(registryPath)) return Promise.resolve();
28694
28762
  try {
28695
28763
  const raw = readFileSync4(registryPath, "utf-8");
@@ -28926,7 +28994,7 @@ async function initializeVaultSync(vaultKey, deps, credentialsVaultStore, oauthS
28926
28994
  });
28927
28995
  }
28928
28996
  function buildDefaultRepo(dataDir, identity) {
28929
- const storage = new FileStorageAdapter(join28(dataDir, "loro"));
28997
+ const storage = new FileStorageAdapter(join29(dataDir, "loro"));
28930
28998
  return {
28931
28999
  repo: new Repo({
28932
29000
  identity,
@@ -29168,6 +29236,9 @@ function buildRejectionStubSubprocess(input) {
29168
29236
  stopBackgroundTask: noop3,
29169
29237
  setHarnessTaskId() {
29170
29238
  },
29239
+ get harnessEpoch() {
29240
+ return null;
29241
+ },
29171
29242
  setOnCwdChanged() {
29172
29243
  },
29173
29244
  async rollback(_numTurns) {
@@ -29712,12 +29783,12 @@ function createBestEffortSender(deps) {
29712
29783
 
29713
29784
  // src/services/comments/github-ingest-pipeline.ts
29714
29785
  import { readFile as readFile20 } from "fs/promises";
29715
- import { join as join30 } from "path";
29786
+ import { join as join31 } from "path";
29716
29787
 
29717
29788
  // src/services/harness/comment-server.ts
29718
29789
  import { randomUUID as randomUUID9 } from "crypto";
29719
29790
  import { access as access4, readFile as readFile19 } from "fs/promises";
29720
- import { join as join29, relative as relative5 } from "path";
29791
+ import { join as join30, relative as relative5 } from "path";
29721
29792
 
29722
29793
  // src/shared/xml-utils.ts
29723
29794
  function escapeXmlAttr(text) {
@@ -30606,7 +30677,7 @@ async function resolveDiffScopeForFile(cwd, normalizedPath) {
30606
30677
  }
30607
30678
  async function handleDiffComment(ctx, filePath, lineNumber, comment) {
30608
30679
  const normalizedPath = normalizeToWorkspaceRelative(filePath, ctx.environmentKey);
30609
- const absolutePath = join29(ctx.environmentKey, normalizedPath);
30680
+ const absolutePath = join30(ctx.environmentKey, normalizedPath);
30610
30681
  const fileExists2 = await access4(absolutePath).then(
30611
30682
  () => true,
30612
30683
  () => false
@@ -30893,7 +30964,7 @@ function prCommentToAnnotation(prComment, ctx) {
30893
30964
  // src/services/comments/github-ingest-pipeline.ts
30894
30965
  async function readLocalFileContent(cwd, path5) {
30895
30966
  try {
30896
- return await readFile20(join30(cwd, path5), "utf-8");
30967
+ return await readFile20(join31(cwd, path5), "utf-8");
30897
30968
  } catch {
30898
30969
  return getFileAtRef(cwd, path5, "HEAD");
30899
30970
  }
@@ -31701,12 +31772,12 @@ function createPRPoller(callbacks, log, resolveTopLevel = getGitTopLevel) {
31701
31772
 
31702
31773
  // src/services/roi/commit-sweep.ts
31703
31774
  import { stat as stat11 } from "fs/promises";
31704
- import { join as join32 } from "path";
31775
+ import { join as join33 } from "path";
31705
31776
 
31706
31777
  // src/services/git-checkpoint.ts
31707
31778
  import { execFile as execFileCb2 } from "child_process";
31708
31779
  import { readFile as readFile21, stat as stat10, unlink as unlink9, writeFile as writeFile15 } from "fs/promises";
31709
- import { join as join31 } from "path";
31780
+ import { join as join32 } from "path";
31710
31781
  import { promisify as promisify7 } from "util";
31711
31782
  var execFile9 = promisify7(execFileCb2);
31712
31783
  var NOT_A_GIT_REPO = "Not a git repository";
@@ -31718,10 +31789,10 @@ async function resolveGitPaths(repoDir, taskId) {
31718
31789
  try {
31719
31790
  const { stdout } = await execFile9("git", ["rev-parse", "--git-dir"], { cwd: repoDir });
31720
31791
  const gitDir = stdout.trim();
31721
- const absoluteGitDir = gitDir.startsWith("/") ? gitDir : join31(repoDir, gitDir);
31792
+ const absoluteGitDir = gitDir.startsWith("/") ? gitDir : join32(repoDir, gitDir);
31722
31793
  return {
31723
31794
  gitDir: absoluteGitDir,
31724
- shadowIndex: join31(absoluteGitDir, `shipyard-index-${taskId}`)
31795
+ shadowIndex: join32(absoluteGitDir, `shipyard-index-${taskId}`)
31725
31796
  };
31726
31797
  } catch {
31727
31798
  return null;
@@ -31816,7 +31887,7 @@ async function rewindToCheckpointImpl(repoDir, taskId, turnNo) {
31816
31887
  const untrackedFiles = untrackedRaw.trim().split("\n").filter((f2) => f2.length > 0);
31817
31888
  for (const file of untrackedFiles) {
31818
31889
  try {
31819
- const fullPath = join31(repoDir, file);
31890
+ const fullPath = join32(repoDir, file);
31820
31891
  const s2 = await stat10(fullPath);
31821
31892
  if (s2.isFile()) {
31822
31893
  await unlink9(fullPath);
@@ -32083,7 +32154,7 @@ async function revertSingleFile(repoDir, file, restoreCommit, mode) {
32083
32154
  const isDeletedByAgent = file.status === "deleted";
32084
32155
  const shouldDelete = mode === "revert" && isAddedByAgent || mode === "unrevert" && isDeletedByAgent;
32085
32156
  if (shouldDelete) {
32086
- await unlink9(join31(repoDir, file.path)).catch((err3) => {
32157
+ await unlink9(join32(repoDir, file.path)).catch((err3) => {
32087
32158
  if (!isEnoent(err3)) throw err3;
32088
32159
  });
32089
32160
  await execFile9("git", ["reset", "HEAD", "--", file.path], { cwd: repoDir }).catch(() => {
@@ -32095,7 +32166,7 @@ async function revertSingleFile(repoDir, file, restoreCommit, mode) {
32095
32166
  maxBuffer: 10 * 1024 * 1024,
32096
32167
  encoding: "buffer"
32097
32168
  });
32098
- await writeFile15(join31(repoDir, file.path), stdout);
32169
+ await writeFile15(join32(repoDir, file.path), stdout);
32099
32170
  await execFile9("git", ["reset", "HEAD", "--", file.path], { cwd: repoDir }).catch(() => {
32100
32171
  });
32101
32172
  return true;
@@ -32188,7 +32259,7 @@ async function diffCheckpointToWorkingTreeImpl(repoDir, taskId, fromTurnNo, scop
32188
32259
  if (!line || statusMap.has(line)) continue;
32189
32260
  let ins = 0;
32190
32261
  try {
32191
- ins = countLines(await readFile21(join31(repoDir, line), "utf-8"));
32262
+ ins = countLines(await readFile21(join32(repoDir, line), "utf-8"));
32192
32263
  } catch {
32193
32264
  }
32194
32265
  entries.push({ path: line, status: "added", insertions: ins, deletions: 0 });
@@ -32208,7 +32279,7 @@ async function getFileVsWorkingTreeImpl(repoDir, taskId, fromTurnNo, filePath) {
32208
32279
  if (from2 === "error") return null;
32209
32280
  let modifiedContent;
32210
32281
  try {
32211
- modifiedContent = await readFile21(join31(repoDir, filePath), "utf-8");
32282
+ modifiedContent = await readFile21(join32(repoDir, filePath), "utf-8");
32212
32283
  } catch {
32213
32284
  modifiedContent = "";
32214
32285
  }
@@ -32489,7 +32560,7 @@ async function isGitBinaryMissing() {
32489
32560
  }
32490
32561
  async function isGitWorkingTree(cwd) {
32491
32562
  try {
32492
- await stat11(join32(cwd, ".git"));
32563
+ await stat11(join33(cwd, ".git"));
32493
32564
  return true;
32494
32565
  } catch {
32495
32566
  return false;
@@ -33453,7 +33524,7 @@ async function sweepOrphanedNotifications(taskStateStore, log) {
33453
33524
 
33454
33525
  // src/services/loro-recovery.ts
33455
33526
  import { access as access5, mkdir as mkdir14, rename as rename12, rm as rm6 } from "fs/promises";
33456
- import { join as join33 } from "path";
33527
+ import { join as join34 } from "path";
33457
33528
  var DEFAULT_RECOVERY_TTL_MS = 6e4;
33458
33529
  function createLoroRecoveryService(deps) {
33459
33530
  const disabled = validateEnv().SHIPYARD_DISABLE_LORO_RECOVERY;
@@ -33504,9 +33575,9 @@ function createLoroRecoveryService(deps) {
33504
33575
  }
33505
33576
  async function quarantineAndSweep(docId, isoTs) {
33506
33577
  const encDocId = encodeURIComponent(docId);
33507
- const sourcePath = join33(deps.dataDir, "loro", encDocId);
33508
- const quarantineRoot = join33(deps.dataDir, "loro-quarantine", isoTs);
33509
- const quarantinePath = join33(quarantineRoot, encDocId);
33578
+ const sourcePath = join34(deps.dataDir, "loro", encDocId);
33579
+ const quarantineRoot = join34(deps.dataDir, "loro-quarantine", isoTs);
33580
+ const quarantinePath = join34(quarantineRoot, encDocId);
33510
33581
  const sourceExists = await pathExists3(sourcePath);
33511
33582
  if (!sourceExists) return "";
33512
33583
  await mkdir14(quarantineRoot, { recursive: true, mode: 448 });
@@ -33515,7 +33586,7 @@ function createLoroRecoveryService(deps) {
33515
33586
  }
33516
33587
  async function postDeleteSweep(docId) {
33517
33588
  const encDocId = encodeURIComponent(docId);
33518
- const sourcePath = join33(deps.dataDir, "loro", encDocId);
33589
+ const sourcePath = join34(deps.dataDir, "loro", encDocId);
33519
33590
  if (await pathExists3(sourcePath)) {
33520
33591
  try {
33521
33592
  await rm6(sourcePath, { recursive: true, force: true });
@@ -35342,7 +35413,7 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
35342
35413
  const aloStreamId = controlChannelStreamId(deps.peerSourceId);
35343
35414
  const aloOutboxChannelName = controlChannelOutboxChannel("daemon", deps.peerSourceId);
35344
35415
  const channelEpoch = registerControlChannelEpoch(controlPeerId);
35345
- const dataDir = join34(deps?.shipyardHome ?? getShipyardHome(), "data");
35416
+ const dataDir = join35(deps?.shipyardHome ?? getShipyardHome(), "data");
35346
35417
  const aloOutbox = new JsonDocOutbox({
35347
35418
  dataDir,
35348
35419
  channel: aloOutboxChannelName,
@@ -35467,6 +35538,9 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
35467
35538
  onOrphanReaped: () => {
35468
35539
  handleControlChannelTerminal("orphan_reaped");
35469
35540
  },
35541
+ onDeadLetter: (_entry, attempts) => {
35542
+ telemeter("alo_resend_exhausted", { attempts, channel: "control" });
35543
+ },
35470
35544
  log: logAdapter
35471
35545
  });
35472
35546
  const aloReady = aloShell.initialize().catch((err3) => {
@@ -35766,7 +35840,7 @@ function createPeerRoleRegistry() {
35766
35840
 
35767
35841
  // src/services/epoch-pruning.ts
35768
35842
  import { readdir as readdir14, rm as rm7 } from "fs/promises";
35769
- import { join as join35 } from "path";
35843
+ import { join as join36 } from "path";
35770
35844
  var LEGACY_PREFIXES = [
35771
35845
  "task-meta",
35772
35846
  "task-conv",
@@ -35806,7 +35880,7 @@ async function pruneOldEpochData(dataDir, currentEpoch, log) {
35806
35880
  }
35807
35881
  if (!shouldPrune(decoded, currentEpoch)) continue;
35808
35882
  removals.push(
35809
- rm7(join35(dataDir, entry.name), { recursive: true }).then(() => {
35883
+ rm7(join36(dataDir, entry.name), { recursive: true }).then(() => {
35810
35884
  pruned++;
35811
35885
  }).catch((err3) => {
35812
35886
  log({
@@ -38031,7 +38105,7 @@ function onConnection(ws, deps, peers) {
38031
38105
 
38032
38106
  // src/services/local-direct/local-direct-token.ts
38033
38107
  import { chmod as chmod2, mkdir as mkdir15, rename as rename13, rm as rm8, writeFile as writeFile16 } from "fs/promises";
38034
- import { dirname as dirname17, join as join36 } from "path";
38108
+ import { dirname as dirname17, join as join37 } from "path";
38035
38109
  var ADVERTISEMENT_FILE = "local-direct.json";
38036
38110
  var ADVERTISEMENT_MODE = 384;
38037
38111
  var ADVERTISEMENT_DIR_MODE = 448;
@@ -38040,7 +38114,7 @@ function generateLocalDirectToken() {
38040
38114
  return nanoid(TOKEN_LENGTH);
38041
38115
  }
38042
38116
  function advertisementPath(shipyardHome) {
38043
- return join36(shipyardHome, "data", ADVERTISEMENT_FILE);
38117
+ return join37(shipyardHome, "data", ADVERTISEMENT_FILE);
38044
38118
  }
38045
38119
  async function writeAdvertisement(shipyardHome, ad) {
38046
38120
  const target = advertisementPath(shipyardHome);
@@ -38307,7 +38381,7 @@ async function setupPluginEventWiring(deps) {
38307
38381
  import { exec as execCb2 } from "child_process";
38308
38382
  import { existsSync as existsSync5 } from "fs";
38309
38383
  import { readdir as readdir15, readFile as readFile22, stat as stat12 } from "fs/promises";
38310
- import { basename as basename7, dirname as dirname18, join as join37 } from "path";
38384
+ import { basename as basename7, dirname as dirname18, join as join38 } from "path";
38311
38385
  import { pathToFileURL as pathToFileURL2 } from "url";
38312
38386
  import { promisify as promisify8 } from "util";
38313
38387
 
@@ -38442,7 +38516,7 @@ var PluginFileWatcher = class {
38442
38516
  }
38443
38517
  const loaded = [];
38444
38518
  for (const entry of entries) {
38445
- const pluginDir = join37(dir, entry);
38519
+ const pluginDir = join38(dir, entry);
38446
38520
  let stats;
38447
38521
  try {
38448
38522
  stats = await stat12(pluginDir);
@@ -38457,7 +38531,7 @@ var PluginFileWatcher = class {
38457
38531
  this.#reconcile(loaded);
38458
38532
  }
38459
38533
  async #loadPlugin(id, pluginDir) {
38460
- const manifestPath = join37(pluginDir, "plugin.json");
38534
+ const manifestPath = join38(pluginDir, "plugin.json");
38461
38535
  let manifestRaw;
38462
38536
  try {
38463
38537
  manifestRaw = await readFile22(manifestPath, "utf-8");
@@ -38494,7 +38568,7 @@ var PluginFileWatcher = class {
38494
38568
  return null;
38495
38569
  }
38496
38570
  let template = "";
38497
- const templatePath = join37(pluginDir, "template.html");
38571
+ const templatePath = join38(pluginDir, "template.html");
38498
38572
  try {
38499
38573
  template = await readFile22(templatePath, "utf-8");
38500
38574
  } catch {
@@ -38509,7 +38583,7 @@ var PluginFileWatcher = class {
38509
38583
  };
38510
38584
  }
38511
38585
  async #loadAndRegisterHandler(id, pluginDir, title, manifest) {
38512
- const handlerPath = join37(pluginDir, "handler.mjs");
38586
+ const handlerPath = join38(pluginDir, "handler.mjs");
38513
38587
  const loaded = await this.#resolveHandler(id, handlerPath);
38514
38588
  const { handlerFn, provideResourcesFn } = loaded;
38515
38589
  if (manifest.provideResources && provideResourcesFn && this.#config.resourceResolver) {
@@ -38636,14 +38710,20 @@ var PluginFileWatcher = class {
38636
38710
  get plugins() {
38637
38711
  return this.#currentPlugins;
38638
38712
  }
38639
- dispose() {
38713
+ async dispose() {
38640
38714
  this.#disposed = true;
38641
38715
  if (this.#debounceTimer) {
38642
38716
  clearTimeout(this.#debounceTimer);
38643
38717
  this.#debounceTimer = null;
38644
38718
  }
38645
- void this.#dirWatcher?.unsubscribe();
38719
+ const sub = this.#dirWatcher;
38646
38720
  this.#dirWatcher = null;
38721
+ await sub?.unsubscribe().catch((err3) => {
38722
+ this.#config.log({
38723
+ event: "plugin_watcher_dispose_unsubscribe_failed",
38724
+ error: err3 instanceof Error ? err3.message : String(err3)
38725
+ });
38726
+ });
38647
38727
  for (const id of this.#registeredIds) {
38648
38728
  unregisterPlugin(id);
38649
38729
  this.#config.eventBus?.unregisterAll(id);
@@ -38667,7 +38747,7 @@ async function findNativeDependencyMarker(pluginDir) {
38667
38747
  }
38668
38748
  async function scanNativeDependencyDir(dir, stack) {
38669
38749
  for (const entry of await readNativeScanEntries(dir)) {
38670
- const fullPath = join37(dir, entry.name);
38750
+ const fullPath = join38(dir, entry.name);
38671
38751
  if (isNativeDependencyMarker(entry, dir)) return fullPath;
38672
38752
  if (shouldDescendForNativeScan(entry)) stack.push(fullPath);
38673
38753
  }
@@ -38692,7 +38772,7 @@ function isNodeBuildReleaseDir(dir) {
38692
38772
 
38693
38773
  // src/services/port-detection.ts
38694
38774
  import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
38695
- import { dirname as dirname19, join as join38 } from "path";
38775
+ import { dirname as dirname19, join as join39 } from "path";
38696
38776
  var LSOF_TIMEOUT_MS = 8e3;
38697
38777
  var LSOF_TTL_MS = 5e3;
38698
38778
  var DEFAULT_POLL_INTERVAL_MS = 5e3;
@@ -38875,7 +38955,7 @@ function hasStringName(val) {
38875
38955
  function resolveProjectRoot(cwd) {
38876
38956
  let dir = cwd;
38877
38957
  while (true) {
38878
- const candidate = join38(dir, "package.json");
38958
+ const candidate = join39(dir, "package.json");
38879
38959
  if (existsSync6(candidate)) {
38880
38960
  let packageName;
38881
38961
  try {
@@ -38999,7 +39079,7 @@ import {
38999
39079
  unlinkSync as unlinkSync2,
39000
39080
  writeFileSync as writeFileSync2
39001
39081
  } from "fs";
39002
- import { join as join39 } from "path";
39082
+ import { join as join40 } from "path";
39003
39083
  function isEnoent6(err3) {
39004
39084
  return err3 instanceof Error && Reflect.get(err3, "code") === "ENOENT";
39005
39085
  }
@@ -39009,7 +39089,7 @@ function atomicWriteSync(filePath, content) {
39009
39089
  renameSync(tmpPath, filePath);
39010
39090
  }
39011
39091
  function recordFilePath2(rootDir, taskId, elementId) {
39012
- return join39(rootDir, taskId, `${elementId}.json`);
39092
+ return join40(rootDir, taskId, `${elementId}.json`);
39013
39093
  }
39014
39094
  function parseRecord2(filePath, logger2) {
39015
39095
  let raw;
@@ -39049,7 +39129,7 @@ function createPreviewStateStore(opts) {
39049
39129
  return taskMap;
39050
39130
  }
39051
39131
  function scanTaskDir(taskId) {
39052
- const taskPath = join39(rootDir, taskId);
39132
+ const taskPath = join40(rootDir, taskId);
39053
39133
  let files;
39054
39134
  try {
39055
39135
  files = readdirSync4(taskPath);
@@ -39058,7 +39138,7 @@ function createPreviewStateStore(opts) {
39058
39138
  }
39059
39139
  for (const file of files) {
39060
39140
  if (!file.endsWith(".json")) continue;
39061
- const record = parseRecord2(join39(taskPath, file), logger2);
39141
+ const record = parseRecord2(join40(taskPath, file), logger2);
39062
39142
  if (record) getTaskMap(taskId).set(record.elementId, record);
39063
39143
  }
39064
39144
  }
@@ -39084,7 +39164,7 @@ function createPreviewStateStore(opts) {
39084
39164
  function writeToDisk(taskId, state) {
39085
39165
  const filePath = recordFilePath2(rootDir, taskId, state.elementId);
39086
39166
  try {
39087
- mkdirSync3(join39(rootDir, taskId), { recursive: true });
39167
+ mkdirSync3(join40(rootDir, taskId), { recursive: true });
39088
39168
  atomicWriteSync(filePath, JSON.stringify(state));
39089
39169
  } catch (err3) {
39090
39170
  logger2.warn(
@@ -41425,7 +41505,7 @@ function wireThreadErrorFallback(daemon, dc, taskId, threadId, channelId, log) {
41425
41505
  }
41426
41506
 
41427
41507
  // src/services/terminal-handler.ts
41428
- import { join as join40 } from "path";
41508
+ import { join as join41 } from "path";
41429
41509
 
41430
41510
  // src/shared/pty-manager.ts
41431
41511
  import { accessSync, chmodSync, constants as constants2, createWriteStream } from "fs";
@@ -41514,7 +41594,7 @@ function createPtyManager() {
41514
41594
  if (exitSink) exitSink(exitCode3, signal);
41515
41595
  for (const cb of exitCallbacks) cb(exitCode3, signal);
41516
41596
  }
41517
- function spawn15(options) {
41597
+ function spawn16(options) {
41518
41598
  if (isAlive) {
41519
41599
  throw new Error("PTY already spawned. Call kill() or dispose() first.");
41520
41600
  }
@@ -41690,7 +41770,7 @@ function createPtyManager() {
41690
41770
  get originTaskId() {
41691
41771
  return originTaskId;
41692
41772
  },
41693
- spawn: spawn15,
41773
+ spawn: spawn16,
41694
41774
  write,
41695
41775
  resize,
41696
41776
  onData,
@@ -41706,7 +41786,7 @@ function createPtyManager() {
41706
41786
  // src/services/terminal-handler.ts
41707
41787
  var MAX_PTYS_DEFAULT = 20;
41708
41788
  function terminalLogPathFor(dir, terminalId) {
41709
- return join40(dir, `${terminalId}.log`);
41789
+ return join41(dir, `${terminalId}.log`);
41710
41790
  }
41711
41791
  function handleTerminalChannel(taskId, terminalId, cwd, send, log, deps) {
41712
41792
  const maxPtys = deps.maxPtys ?? MAX_PTYS_DEFAULT;
@@ -42316,7 +42396,7 @@ function buildCollabRoomManager(deps) {
42316
42396
  // src/services/serve-factory.ts
42317
42397
  import { randomUUID as randomUUID25 } from "crypto";
42318
42398
  import { mkdir as mkdir36 } from "fs/promises";
42319
- import { join as join78 } from "path";
42399
+ import { join as join79 } from "path";
42320
42400
 
42321
42401
  // src/shared/capabilities/cursor-boot-auth.ts
42322
42402
  async function mergeCursorAuthFromVault(caps, detect, log) {
@@ -42357,12 +42437,12 @@ async function mergeCursorAuthFromVault(caps, detect, log) {
42357
42437
 
42358
42438
  // src/shared/capabilities/cursor-hook-shim-path.ts
42359
42439
  import { statSync as statSync3 } from "fs";
42360
- import { join as join41 } from "path";
42440
+ import { join as join42 } from "path";
42361
42441
  import { fileURLToPath as fileURLToPath2 } from "url";
42362
42442
  function getCursorHookShimPath() {
42363
42443
  const resourcesPath = Reflect.get(process, "resourcesPath");
42364
42444
  if (typeof resourcesPath === "string" && resourcesPath.length > 0) {
42365
- return join41(resourcesPath, "daemon", "cursor-hook-shim.js");
42445
+ return join42(resourcesPath, "daemon", "cursor-hook-shim.js");
42366
42446
  }
42367
42447
  return fileURLToPath2(new URL("./cursor-hook-shim.js", import.meta.url));
42368
42448
  }
@@ -42597,7 +42677,7 @@ function resolveRuntimeAuthIdentity(runtimeAuth, runtimeId) {
42597
42677
  // src/shared/capabilities/spinner-verbs-reader.ts
42598
42678
  import { readFile as readFile23 } from "fs/promises";
42599
42679
  import { homedir as homedir5 } from "os";
42600
- import { join as join42 } from "path";
42680
+ import { join as join43 } from "path";
42601
42681
  var ClaudeSpinnerVerbsSchema = external_exports.object({
42602
42682
  mode: external_exports.enum(["append", "replace"]),
42603
42683
  verbs: external_exports.array(external_exports.string())
@@ -42622,8 +42702,8 @@ function toPersistedVerbs(resolved, defaults2) {
42622
42702
  return resolved.every((v2, i) => v2 === defaults2[i]) ? [] : [...resolved];
42623
42703
  }
42624
42704
  async function readSpinnerVerbs() {
42625
- const claudeDir = join42(homedir5(), ".claude");
42626
- const paths = [join42(claudeDir, "settings.json"), join42(claudeDir, "settings.local.json")];
42705
+ const claudeDir = join43(homedir5(), ".claude");
42706
+ const paths = [join43(claudeDir, "settings.json"), join43(claudeDir, "settings.local.json")];
42627
42707
  const parsed = await Promise.all(
42628
42708
  paths.map(async (p2) => {
42629
42709
  try {
@@ -42665,7 +42745,7 @@ async function reimportSpinnerVerbs(store, log) {
42665
42745
 
42666
42746
  // src/shared/mcp/oauth-state-store.ts
42667
42747
  import { readFile as readFile24 } from "fs/promises";
42668
- import { join as join43 } from "path";
42748
+ import { join as join44 } from "path";
42669
42749
  import {
42670
42750
  OAuthClientInformationFullSchema as OAuthClientInformationFullSchema2,
42671
42751
  OAuthClientInformationSchema as OAuthClientInformationSchema2,
@@ -42711,7 +42791,7 @@ var MCPOAuthStateStore = class {
42711
42791
  this.#shipyardHome = shipyardHome;
42712
42792
  }
42713
42793
  #filePath() {
42714
- return join43(this.#shipyardHome, STATE_FILE);
42794
+ return join44(this.#shipyardHome, STATE_FILE);
42715
42795
  }
42716
42796
  #lockPath() {
42717
42797
  return `${this.#filePath()}.lock`;
@@ -42810,11 +42890,11 @@ function getShipyardSubagentCatalog() {
42810
42890
 
42811
42891
  // src/services/bootstrap/rehydrate.ts
42812
42892
  import { readFile as readFile26, rename as rename16, writeFile as writeFile18 } from "fs/promises";
42813
- import { join as join45 } from "path";
42893
+ import { join as join46 } from "path";
42814
42894
 
42815
42895
  // src/services/collab/collab-queue-persistence.ts
42816
42896
  import { appendFile as appendFile2, mkdir as mkdir16, readdir as readdir16, readFile as readFile25, rename as rename15, rm as rm9, writeFile as writeFile17 } from "fs/promises";
42817
- import { join as join44 } from "path";
42897
+ import { join as join45 } from "path";
42818
42898
  var PersistedQueueEntrySchema = external_exports.object({
42819
42899
  content: external_exports.array(ContentBlockSchema),
42820
42900
  settings: DaemonSettingsSchema.optional(),
@@ -42838,9 +42918,9 @@ function parsePersistedLine(line) {
42838
42918
  };
42839
42919
  }
42840
42920
  function buildCollabQueuePersistence(dataDir) {
42841
- const queuesDir = join44(dataDir, "collab-queues");
42921
+ const queuesDir = join45(dataDir, "collab-queues");
42842
42922
  function queuePath(queueKey) {
42843
- return join44(queuesDir, `${queueKey}.jsonl`);
42923
+ return join45(queuesDir, `${queueKey}.jsonl`);
42844
42924
  }
42845
42925
  async function ensureDir() {
42846
42926
  await mkdir16(queuesDir, { recursive: true });
@@ -42933,7 +43013,7 @@ function buildCollabQueuePersistence(dataDir) {
42933
43013
  }
42934
43014
  for (const name of names) {
42935
43015
  if (!name.includes(".tmp-")) continue;
42936
- await rm9(join44(queuesDir, name), { force: true }).catch(() => {
43016
+ await rm9(join45(queuesDir, name), { force: true }).catch(() => {
42937
43017
  });
42938
43018
  }
42939
43019
  }
@@ -43252,7 +43332,7 @@ async function stripPinnedFieldFromTasksFile(tasksPath, envelope, records) {
43252
43332
  }
43253
43333
  }
43254
43334
  async function migratePinnedToFavoriteTasks(dataDir, userSettingsStore, log) {
43255
- const tasksPath = join45(dataDir, "tasks.json");
43335
+ const tasksPath = join46(dataDir, "tasks.json");
43256
43336
  const parsed = await readRawTasksEnvelope(tasksPath);
43257
43337
  if (!parsed) return;
43258
43338
  const { records, envelope, fromMigrated } = parsed;
@@ -43368,7 +43448,7 @@ async function sweepStaleTasks(taskStateStore, taskManager, log) {
43368
43448
 
43369
43449
  // src/services/bootstrap/update-status.ts
43370
43450
  import { readFile as readFile27, unlink as unlink11 } from "fs/promises";
43371
- import { join as join46 } from "path";
43451
+ import { join as join47 } from "path";
43372
43452
  var UPDATE_STATUS_FILENAME = "update-status.json";
43373
43453
  var RAW_LOG_MAX_CHARS = 500;
43374
43454
  var UpdateStatusSchema = external_exports.object({
@@ -43381,7 +43461,7 @@ var UpdateStatusSchema = external_exports.object({
43381
43461
  rolledBack: external_exports.boolean()
43382
43462
  });
43383
43463
  async function readAndClearUpdateStatus(shipyardHome, log) {
43384
- const path5 = join46(shipyardHome, UPDATE_STATUS_FILENAME);
43464
+ const path5 = join47(shipyardHome, UPDATE_STATUS_FILENAME);
43385
43465
  let raw;
43386
43466
  try {
43387
43467
  raw = await readFile27(path5, "utf-8");
@@ -43468,11 +43548,11 @@ function wireUpdateStatusForwarding(report, sink2, onMessage, log) {
43468
43548
 
43469
43549
  // src/services/bootstrap/updates-cleanup.ts
43470
43550
  import { readdir as readdir17, rm as rm10, stat as stat14, unlink as unlink13 } from "fs/promises";
43471
- import { join as join48 } from "path";
43551
+ import { join as join49 } from "path";
43472
43552
 
43473
43553
  // src/services/bootstrap/self-update-lock.ts
43474
43554
  import { mkdir as mkdir17, readFile as readFile28, stat as stat13, unlink as unlink12, writeFile as writeFile19 } from "fs/promises";
43475
- import { dirname as dirname21, join as join47 } from "path";
43555
+ import { dirname as dirname21, join as join48 } from "path";
43476
43556
  var LOCK_FILENAME = ".lock";
43477
43557
  var STALE_LOCK_MS = 10 * 60 * 1e3;
43478
43558
  var LockFileSchema = external_exports.object({
@@ -43480,7 +43560,7 @@ var LockFileSchema = external_exports.object({
43480
43560
  startedAt: external_exports.number()
43481
43561
  });
43482
43562
  function lockPath(shipyardHome) {
43483
- return join47(shipyardHome, "updates", LOCK_FILENAME);
43563
+ return join48(shipyardHome, "updates", LOCK_FILENAME);
43484
43564
  }
43485
43565
  function isStaleLock(lock, now, isProcessAlive2) {
43486
43566
  const age = now - lock.startedAt;
@@ -43603,7 +43683,7 @@ async function cleanupUpdatesDir(shipyardHome, deps = {}) {
43603
43683
  const now = deps.now ?? Date.now;
43604
43684
  const isProcessAlive2 = deps.isProcessAlive ?? defaultIsProcessAlive;
43605
43685
  const log = deps.log;
43606
- const updatesDir = join48(shipyardHome, "updates");
43686
+ const updatesDir = join49(shipyardHome, "updates");
43607
43687
  const empty2 = {
43608
43688
  tarballsKept: [],
43609
43689
  tarballsDeleted: [],
@@ -43727,7 +43807,7 @@ async function sortByMtimeDesc(baseDir, names, log) {
43727
43807
  const rows = [];
43728
43808
  for (const name of names) {
43729
43809
  try {
43730
- const s2 = await stat14(join48(baseDir, name));
43810
+ const s2 = await stat14(join49(baseDir, name));
43731
43811
  rows.push({ name, mtimeMs: s2.mtimeMs });
43732
43812
  } catch (err3) {
43733
43813
  log?.info({ err: err3, entry: name }, "updates cleanup: stat failed, skipping entry");
@@ -43740,7 +43820,7 @@ async function filterOlderThan(baseDir, names, nowMs, thresholdMs, log) {
43740
43820
  const out = [];
43741
43821
  for (const name of names) {
43742
43822
  try {
43743
- const s2 = await stat14(join48(baseDir, name));
43823
+ const s2 = await stat14(join49(baseDir, name));
43744
43824
  if (nowMs - s2.mtimeMs > thresholdMs) {
43745
43825
  out.push(name);
43746
43826
  }
@@ -43754,7 +43834,7 @@ async function deleteFiles(baseDir, names, log) {
43754
43834
  const deleted = [];
43755
43835
  for (const name of names) {
43756
43836
  try {
43757
- await unlink13(join48(baseDir, name));
43837
+ await unlink13(join49(baseDir, name));
43758
43838
  deleted.push(name);
43759
43839
  } catch (err3) {
43760
43840
  if (isEnoent(err3)) {
@@ -43770,7 +43850,7 @@ async function deleteDirs(baseDir, names, log) {
43770
43850
  const deleted = [];
43771
43851
  for (const name of names) {
43772
43852
  try {
43773
- await rm10(join48(baseDir, name), { recursive: true, force: true });
43853
+ await rm10(join49(baseDir, name), { recursive: true, force: true });
43774
43854
  deleted.push(name);
43775
43855
  } catch (err3) {
43776
43856
  log?.info({ err: err3, entry: name }, "updates cleanup: rm -rf failed");
@@ -43779,7 +43859,7 @@ async function deleteDirs(baseDir, names, log) {
43779
43859
  return deleted;
43780
43860
  }
43781
43861
  async function maybeClearLock(shipyardHome, nowMs, isProcessAlive2, log) {
43782
- const lockFilePath = join48(shipyardHome, "updates", LOCK_FILENAME);
43862
+ const lockFilePath = join49(shipyardHome, "updates", LOCK_FILENAME);
43783
43863
  const outcome = await readLockFileWithOutcome(shipyardHome);
43784
43864
  switch (outcome.kind) {
43785
43865
  case "absent":
@@ -45080,7 +45160,7 @@ function wireDevServersAndTerminalsResolvers(deps) {
45080
45160
 
45081
45161
  // src/services/file-resource-resolver.ts
45082
45162
  import { readFile as readFile29, stat as stat15 } from "fs/promises";
45083
- import { basename as basename8, join as join49, normalize as normalize4, sep as sep3 } from "path";
45163
+ import { basename as basename8, join as join50, normalize as normalize4, sep as sep3 } from "path";
45084
45164
  var MAX_FILE_SIZE = 10 * 1024 * 1024;
45085
45165
  var MIME_BY_EXT2 = {
45086
45166
  ".ts": "text/typescript",
@@ -45149,7 +45229,7 @@ function createFileResourceResolver(deps) {
45149
45229
  const { taskId, relativePath } = parseFileUri2(uri);
45150
45230
  const cwd = deps.getTaskCwd(taskId) ?? deps.workspaceRoot;
45151
45231
  const normalizedCwd = normalize4(cwd);
45152
- const absolutePath = normalize4(join49(cwd, relativePath));
45232
+ const absolutePath = normalize4(join50(cwd, relativePath));
45153
45233
  const cwdPrefix = normalizedCwd.endsWith(sep3) ? normalizedCwd : `${normalizedCwd}${sep3}`;
45154
45234
  if (!absolutePath.startsWith(cwdPrefix) && absolutePath !== normalizedCwd) {
45155
45235
  throw new Error(`Path traversal rejected: ${relativePath}`);
@@ -45217,7 +45297,7 @@ function unregisterSubprocessEpoch(taskId, registry = subprocessEpochs) {
45217
45297
  // src/services/harness/deliverable-server.ts
45218
45298
  import { randomUUID as randomUUID12 } from "crypto";
45219
45299
  import { copyFile, rm as fsRm, stat as fsStat3, mkdir as mkdir18 } from "fs/promises";
45220
- import { basename as basename9, extname as extname2, join as join50, resolve as resolve8, sep as sep4 } from "path";
45300
+ import { basename as basename9, extname as extname2, join as join51, resolve as resolve8, sep as sep4 } from "path";
45221
45301
  var TOOL_DESCRIPTION2 = [
45222
45302
  "Register a proof-of-work deliverable for this task. Call this after producing an artifact (screenshot, video, log, markdown document, AX tree, DOM snapshot) so reviewers can see verifiable evidence of what the agent produced.",
45223
45303
  "",
@@ -45277,7 +45357,7 @@ var RegisterDeliverableInput = {
45277
45357
  };
45278
45358
  var COPY_MAX_BYTES = 50 * 1024 * 1024;
45279
45359
  function isInternedFile(dataDir, filePath) {
45280
- const internBase = resolve8(join50(dataDir, "deliverable-files")) + sep4;
45360
+ const internBase = resolve8(join51(dataDir, "deliverable-files")) + sep4;
45281
45361
  return resolve8(filePath).startsWith(internBase);
45282
45362
  }
45283
45363
  async function internDeliverableFile(deliverableId, taskId, filePath, storedSizeBytes, dataDir, prevInternedPath) {
@@ -45301,8 +45381,8 @@ async function internDeliverableFile(deliverableId, taskId, filePath, storedSize
45301
45381
  };
45302
45382
  }
45303
45383
  const ext = extname2(filePath);
45304
- const destDir = join50(dataDir, "deliverable-files", taskId);
45305
- const destPath = join50(destDir, `${deliverableId}${ext}`);
45384
+ const destDir = join51(dataDir, "deliverable-files", taskId);
45385
+ const destPath = join51(destDir, `${deliverableId}${ext}`);
45306
45386
  try {
45307
45387
  await mkdir18(destDir, { recursive: true });
45308
45388
  await copyFile(filePath, destPath);
@@ -45728,7 +45808,7 @@ function createDeliverableTools(ctx) {
45728
45808
 
45729
45809
  // src/services/harness/plugin-server.ts
45730
45810
  import { mkdir as mkdir19, writeFile as writeFile20 } from "fs/promises";
45731
- import { join as join51 } from "path";
45811
+ import { join as join52 } from "path";
45732
45812
 
45733
45813
  // src/services/harness/sandbox-docs.ts
45734
45814
  var SANDBOX_HTML_RULES = `## HTML Rules
@@ -46346,14 +46426,14 @@ ${addendum}`;
46346
46426
  events: input.events,
46347
46427
  provideResources: input.provideResources
46348
46428
  });
46349
- const pluginDir = join51(ctx.pluginsDir, input.pluginId);
46429
+ const pluginDir = join52(ctx.pluginsDir, input.pluginId);
46350
46430
  await mkdir19(pluginDir, { recursive: true });
46351
- await writeFile20(join51(pluginDir, "template.html"), input.template, "utf-8");
46431
+ await writeFile20(join52(pluginDir, "template.html"), input.template, "utf-8");
46352
46432
  if (input.handler) {
46353
- await writeFile20(join51(pluginDir, "handler.mjs"), input.handler, "utf-8");
46433
+ await writeFile20(join52(pluginDir, "handler.mjs"), input.handler, "utf-8");
46354
46434
  }
46355
46435
  await writeFile20(
46356
- join51(pluginDir, "plugin.json"),
46436
+ join52(pluginDir, "plugin.json"),
46357
46437
  JSON.stringify(manifest, null, 2),
46358
46438
  "utf-8"
46359
46439
  );
@@ -55026,124 +55106,6 @@ function validateHtml(html) {
55026
55106
 
55027
55107
  // src/services/harness/mermaid-validator.ts
55028
55108
  import { Window } from "happy-dom";
55029
-
55030
- // src/services/harness/mermaid-label-length.ts
55031
- var MERMAID_MAX_LABEL_SEGMENT_CHARS = 40;
55032
- var BR_TAG_SPLIT = /<br\s*\/?>/gi;
55033
- var QUOTED_LABEL_PATTERNS = [
55034
- /\[\(\s*(["'])((?:\\.|(?!\1).)*)\1\s*\)\]/g,
55035
- /\(\[\s*(["'])((?:\\.|(?!\1).)*)\1\s*\]\)/g,
55036
- /\[\[\s*(["'])((?:\\.|(?!\1).)*)\1\s*\]\]/g,
55037
- /\{\{\s*(["'])((?:\\.|(?!\1).)*)\1\s*\}\}/g,
55038
- /\[\s*(["'])((?:\\.|(?!\1).)*)\1\s*\]/g,
55039
- /\(\(\s*(["'])((?:\\.|(?!\1).)*)\1\s*\)\)/g,
55040
- /\(\s*(["'])((?:\\.|(?!\1).)*)\1\s*\)/g,
55041
- /\{\s*(["'])((?:\\.|(?!\1).)*)\1\s*\}/g
55042
- ];
55043
- var UNQUOTED_LABEL_PATTERNS = [
55044
- /\[\(\s*([^)"'\n]+?)\s*\)\]/g,
55045
- /\(\[\s*([^)"'\n]+?)\s*\]\)/g,
55046
- /\[\[\s*([^\]"\n]+?)\s*\]\]/g,
55047
- /\[\s*([^\]"(<\n]+?)\s*\]/g,
55048
- /\(\(\s*([^)"'\n]+?)\s*\)\)/g,
55049
- /\(\s*([^)"'\n]+?)\s*\)/g,
55050
- /\{\s*([^}"'\n]+?)\s*\}/g
55051
- ];
55052
- function stripMermaidFrontmatter(code) {
55053
- return code.replace(/^%%\{[\s\S]*?\}%%\s*/m, "");
55054
- }
55055
- function consumeQuotedSegment(line, start, quote) {
55056
- let text = "";
55057
- for (let i = start; i < line.length; i++) {
55058
- const ch = line[i];
55059
- if (ch === "\\" && i + 1 < line.length) {
55060
- text += ch + line[i + 1];
55061
- i++;
55062
- continue;
55063
- }
55064
- text += ch;
55065
- if (i > start && ch === quote) return { text, nextIndex: i };
55066
- }
55067
- return { text, nextIndex: line.length - 1 };
55068
- }
55069
- function stripMermaidCommentFromLine(line) {
55070
- let result = "";
55071
- for (let i = 0; i < line.length; i++) {
55072
- const ch = line[i];
55073
- if (ch === '"' || ch === "'") {
55074
- const consumed = consumeQuotedSegment(line, i, ch);
55075
- result += consumed.text;
55076
- i = consumed.nextIndex;
55077
- continue;
55078
- }
55079
- if (ch === "%" && line[i + 1] === "%") break;
55080
- result += ch;
55081
- }
55082
- return result;
55083
- }
55084
- function stripMermaidComments(code) {
55085
- return code.split("\n").map(stripMermaidCommentFromLine).join("\n");
55086
- }
55087
- function extractMermaidNodeLabels(code) {
55088
- const stripped = stripMermaidComments(stripMermaidFrontmatter(code));
55089
- const labels = [];
55090
- const matchedRanges = [];
55091
- function overlaps(start, end) {
55092
- return matchedRanges.some(([s2, e]) => start < e && end > s2);
55093
- }
55094
- function record(start, end, text) {
55095
- if (!overlaps(start, end)) {
55096
- matchedRanges.push([start, end]);
55097
- labels.push(text);
55098
- }
55099
- }
55100
- for (const pattern of QUOTED_LABEL_PATTERNS) {
55101
- pattern.lastIndex = 0;
55102
- let match = pattern.exec(stripped);
55103
- while (match !== null) {
55104
- const text = match[2];
55105
- if (text !== void 0) {
55106
- record(match.index, match.index + match[0].length, text);
55107
- }
55108
- match = pattern.exec(stripped);
55109
- }
55110
- }
55111
- for (const pattern of UNQUOTED_LABEL_PATTERNS) {
55112
- pattern.lastIndex = 0;
55113
- let match = pattern.exec(stripped);
55114
- while (match !== null) {
55115
- const text = match[1]?.trim();
55116
- if (text) {
55117
- record(match.index, match.index + match[0].length, text);
55118
- }
55119
- match = pattern.exec(stripped);
55120
- }
55121
- }
55122
- return labels;
55123
- }
55124
- function checkMermaidLabelSegmentLengths(code, maxChars = MERMAID_MAX_LABEL_SEGMENT_CHARS) {
55125
- const labels = extractMermaidNodeLabels(code);
55126
- const warnings = [];
55127
- const seen = /* @__PURE__ */ new Set();
55128
- for (const label of labels) {
55129
- const segments = label.split(BR_TAG_SPLIT);
55130
- for (const segment of segments) {
55131
- const trimmed = segment.trim();
55132
- if (trimmed.length > maxChars) {
55133
- const key = `${trimmed}:${trimmed.length}`;
55134
- if (!seen.has(key)) {
55135
- seen.add(key);
55136
- warnings.push(
55137
- `Label too long (${trimmed.length} > ${maxChars} chars): "${trimmed}" \u2014 abbreviate or split with <br/>.`
55138
- );
55139
- }
55140
- }
55141
- }
55142
- }
55143
- return warnings;
55144
- }
55145
-
55146
- // src/services/harness/mermaid-validator.ts
55147
55109
  var domInstalled = false;
55148
55110
  function installDom() {
55149
55111
  if (domInstalled) return;
@@ -55185,7 +55147,7 @@ function getMermaid() {
55185
55147
  mermaidPromise = (async () => {
55186
55148
  installDom();
55187
55149
  const mod = await import("mermaid");
55188
- mod.default.initialize({ startOnLoad: false, securityLevel: "antiscript" });
55150
+ mod.default.initialize({ startOnLoad: false, securityLevel: MERMAID_SECURITY_LEVEL });
55189
55151
  return mod;
55190
55152
  })().catch((err3) => {
55191
55153
  mermaidPromise = null;
@@ -55201,8 +55163,7 @@ async function validateMermaid(code) {
55201
55163
  try {
55202
55164
  const { default: mermaid } = await getMermaid();
55203
55165
  const result = await mermaid.parse(code);
55204
- const warnings = checkMermaidLabelSegmentLengths(code);
55205
- return { valid: true, diagramType: result?.diagramType ?? "unknown", warnings };
55166
+ return { valid: true, diagramType: result?.diagramType ?? "unknown" };
55206
55167
  } catch (e) {
55207
55168
  const message = e instanceof Error ? e.message : String(e);
55208
55169
  return { valid: false, error: message };
@@ -55940,7 +55901,7 @@ async function validateContent(type, content) {
55940
55901
  switch (type) {
55941
55902
  case "mermaid": {
55942
55903
  const result = await validateMermaid(content);
55943
- if (result.valid) return { ok: true, warnings: result.warnings };
55904
+ if (result.valid) return { ok: true, warnings: [] };
55944
55905
  return { ok: false, error: result.error };
55945
55906
  }
55946
55907
  case "html": {
@@ -56544,7 +56505,7 @@ import { pathToFileURL as pathToFileURL3 } from "url";
56544
56505
  // src/services/lsp/language-server-registry.ts
56545
56506
  import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
56546
56507
  import { createRequire as createRequire3 } from "module";
56547
- import { dirname as dirname22, isAbsolute as isAbsolute4, join as join52 } from "path";
56508
+ import { dirname as dirname22, isAbsolute as isAbsolute4, join as join53 } from "path";
56548
56509
  function decideTypescriptServer(d) {
56549
56510
  if (d.hasAngular || d.hasNest || d.hasTsserverPlugins) return "vtsls";
56550
56511
  return "tsgo";
@@ -56634,11 +56595,11 @@ function isRecord7(value) {
56634
56595
  return typeof value === "object" && value !== null;
56635
56596
  }
56636
56597
  function gatherTsDetection(cwd, deps) {
56637
- const pkg = deps.readJsonFile(join52(cwd, "package.json"));
56598
+ const pkg = deps.readJsonFile(join53(cwd, "package.json"));
56638
56599
  const depNames = isRecord7(pkg) ? [...keysOf(pkg.dependencies), ...keysOf(pkg.devDependencies)] : [];
56639
- const hasAngular = depNames.includes("@angular/core") || deps.fileExists(join52(cwd, "angular.json"));
56640
- const hasNest = depNames.includes("@nestjs/core") || deps.fileExists(join52(cwd, "nest-cli.json"));
56641
- const tsconfig = deps.readJsonFile(join52(cwd, "tsconfig.json"));
56600
+ const hasAngular = depNames.includes("@angular/core") || deps.fileExists(join53(cwd, "angular.json"));
56601
+ const hasNest = depNames.includes("@nestjs/core") || deps.fileExists(join53(cwd, "nest-cli.json"));
56602
+ const tsconfig = deps.readJsonFile(join53(cwd, "tsconfig.json"));
56642
56603
  const compilerOptions = isRecord7(tsconfig) ? tsconfig.compilerOptions : void 0;
56643
56604
  const plugins2 = isRecord7(compilerOptions) ? compilerOptions.plugins : void 0;
56644
56605
  const hasTsserverPlugins = Array.isArray(plugins2) && plugins2.length > 0;
@@ -56653,11 +56614,11 @@ function detectPythonVenv(cwd, deps) {
56653
56614
  if (envVenv && isAbsolute4(envVenv) && deps.fileExists(envVenv)) {
56654
56615
  candidates.push(envVenv);
56655
56616
  }
56656
- candidates.push(join52(cwd, ".venv"), join52(cwd, "venv"));
56617
+ candidates.push(join53(cwd, ".venv"), join53(cwd, "venv"));
56657
56618
  const isWin = deps.platform === "win32";
56658
56619
  for (const root of candidates) {
56659
- const python = isWin ? join52(root, "Scripts", "python.exe") : join52(root, "bin", "python");
56660
- const python3 = isWin ? python : join52(root, "bin", "python3");
56620
+ const python = isWin ? join53(root, "Scripts", "python.exe") : join53(root, "bin", "python");
56621
+ const python3 = isWin ? python : join53(root, "bin", "python3");
56661
56622
  if (deps.fileExists(python) || deps.fileExists(python3)) return root;
56662
56623
  }
56663
56624
  return null;
@@ -56690,8 +56651,8 @@ function resolveTsgoBinaryPath(req) {
56690
56651
  `Unable to resolve ${platformPkg}: ${err3 instanceof Error ? err3.message : String(err3)}`
56691
56652
  );
56692
56653
  }
56693
- const exeDir = join52(dirname22(pkgJsonPath), "lib");
56694
- const exe = process.platform === "win32" ? join52(exeDir, "tsgo.exe") : join52(exeDir, "tsgo");
56654
+ const exeDir = join53(dirname22(pkgJsonPath), "lib");
56655
+ const exe = process.platform === "win32" ? join53(exeDir, "tsgo.exe") : join53(exeDir, "tsgo");
56695
56656
  if (!existsSync7(exe)) {
56696
56657
  throw new Error(`tsgo native binary not found: ${exe}`);
56697
56658
  }
@@ -56704,7 +56665,7 @@ function resolveBinViaPackageJson(pkg, binName, req) {
56704
56665
  const named = isRecord7(binField) ? binField[binName] : void 0;
56705
56666
  const bin = typeof binField === "string" ? binField : typeof named === "string" ? named : void 0;
56706
56667
  if (!bin) throw new Error(`package '${pkg}' has no bin entry '${binName}'`);
56707
- return join52(dirname22(pkgJsonPath), bin);
56668
+ return join53(dirname22(pkgJsonPath), bin);
56708
56669
  }
56709
56670
  function errorMessage3(err3) {
56710
56671
  return err3 instanceof Error ? err3.message : String(err3);
@@ -57393,7 +57354,7 @@ function getPeerSubscriptionSynchronizer(repo) {
57393
57354
  }
57394
57355
  function repoHasPeerSubscription(repo, docId) {
57395
57356
  const synchronizer = getPeerSubscriptionSynchronizer(repo);
57396
- if (!synchronizer) return false;
57357
+ if (!synchronizer) return true;
57397
57358
  return synchronizer.getPeers().some((peer) => peer.subscriptions.has(docId));
57398
57359
  }
57399
57360
 
@@ -59567,7 +59528,7 @@ import { execFile as execFileCb3 } from "child_process";
59567
59528
  import { createHash as createHash3 } from "crypto";
59568
59529
  import { readFile as readFile31 } from "fs/promises";
59569
59530
  import { homedir as homedir6 } from "os";
59570
- import { join as join53 } from "path";
59531
+ import { join as join54 } from "path";
59571
59532
  import { promisify as promisify9 } from "util";
59572
59533
  var execFile10 = promisify9(execFileCb3);
59573
59534
  var CodexCredentialsEntrySchema = external_exports.object({
@@ -59628,10 +59589,10 @@ async function readKeychainStore() {
59628
59589
  return null;
59629
59590
  }
59630
59591
  }
59631
- async function readCodexMcpCredentials(serverName, serverUrl, codexHome = join53(homedir6(), ".codex")) {
59592
+ async function readCodexMcpCredentials(serverName, serverUrl, codexHome = join54(homedir6(), ".codex")) {
59632
59593
  if (!serverUrl) return null;
59633
59594
  try {
59634
- const raw = await readFile31(join53(codexHome, FALLBACK_FILENAME), "utf-8");
59595
+ const raw = await readFile31(join54(codexHome, FALLBACK_FILENAME), "utf-8");
59635
59596
  const fileResult = CodexCredentialsFileSchema.safeParse(JSON.parse(raw));
59636
59597
  if (fileResult.success) {
59637
59598
  const entry = findEntry2(fileResult.data, serverName, serverUrl);
@@ -61828,7 +61789,7 @@ var HealthMetrics = class {
61828
61789
  // src/services/metrics/stall-profiler.ts
61829
61790
  import { mkdir as mkdir20, readdir as readdir18, unlink as unlink14, writeFile as writeFile21 } from "fs/promises";
61830
61791
  import { Session as Session2 } from "inspector/promises";
61831
- import { join as join54 } from "path";
61792
+ import { join as join55 } from "path";
61832
61793
  import { monitorEventLoopDelay as monitorEventLoopDelay3 } from "perf_hooks";
61833
61794
  function hasProfile2(value) {
61834
61795
  return typeof value === "object" && value !== null && "profile" in value;
@@ -61938,7 +61899,7 @@ var StallProfiler = class {
61938
61899
  const isoTs = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
61939
61900
  const stallRounded = Math.round(stallMs);
61940
61901
  const filename = `stall-${isoTs}-${stallRounded}ms.cpuprofile`;
61941
- const profilePath = join54(this.#outDir, filename);
61902
+ const profilePath = join55(this.#outDir, filename);
61942
61903
  await mkdir20(this.#outDir, { recursive: true });
61943
61904
  await writeFile21(profilePath, JSON.stringify(stopResult.profile));
61944
61905
  this.#log({
@@ -61972,7 +61933,7 @@ var StallProfiler = class {
61972
61933
  const entries = await readdir18(this.#outDir);
61973
61934
  const toDelete = planProfileEviction(entries, this.#maxProfiles, STALL_PROFILE_PREFIX);
61974
61935
  if (toDelete.length === 0) return;
61975
- const results = await Promise.allSettled(toDelete.map((f2) => unlink14(join54(this.#outDir, f2))));
61936
+ const results = await Promise.allSettled(toDelete.map((f2) => unlink14(join55(this.#outDir, f2))));
61976
61937
  const failures = results.filter((r) => r.status === "rejected").length;
61977
61938
  if (failures > 0) {
61978
61939
  this.#log({
@@ -63031,7 +62992,7 @@ function joinPoint(doc2, pos, dir = -1) {
63031
62992
  pos = dir < 0 ? $pos.before(d) : $pos.after(d);
63032
62993
  }
63033
62994
  }
63034
- function join55(tr2, pos, depth) {
62995
+ function join56(tr2, pos, depth) {
63035
62996
  let convertNewlines = null;
63036
62997
  let { linebreakReplacement } = tr2.doc.type.schema;
63037
62998
  let $before = tr2.doc.resolve(pos - depth), beforeType = $before.node().type;
@@ -63717,7 +63678,7 @@ var Transform = class {
63717
63678
  last and first siblings are also joined, and so on.
63718
63679
  */
63719
63680
  join(pos, depth = 1) {
63720
- join55(this, pos, depth);
63681
+ join56(this, pos, depth);
63721
63682
  return this;
63722
63683
  }
63723
63684
  /**
@@ -84131,10 +84092,11 @@ var PreWarmManager = class {
84131
84092
  #cwd = null;
84132
84093
  #generation = 0;
84133
84094
  /**
84134
- * Per-spawn epoch UUID minted alongside each `#generation` increment in
84135
- * `#spawn()`. Surfaced on `PreWarmClaim` so the adopting Thread can register
84136
- * it with the subprocess-epoch registry (AGENTS.md Invariant #2b). Reset to
84137
- * `null` on `#teardown()` / `dispose()`.
84095
+ * The harness-bound epoch captured from `subprocess.harnessEpoch` after the
84096
+ * spawn dispatcher minted+bound it (NOT minted here). Surfaced on
84097
+ * `PreWarmClaim` so the adopting Thread registers the SAME epoch the harness
84098
+ * fence checks (AGENTS.md Invariant #2b). Reset to `null` on
84099
+ * `#teardown()` / `dispose()`.
84138
84100
  */
84139
84101
  #epoch = null;
84140
84102
  #disposed = false;
@@ -84358,7 +84320,7 @@ var PreWarmManager = class {
84358
84320
  this.#canUseToolSlot = null;
84359
84321
  if (this.#epoch === null) {
84360
84322
  throw new Error(
84361
- `PreWarmManager.claim() invariant violated: warm subprocess has no #epoch (status=${this.#status}, cwd=${cwd})`
84323
+ `PreWarmManager.claim() invariant violated: warm subprocess has no harness epoch (status=${this.#status}, cwd=${cwd})`
84362
84324
  );
84363
84325
  }
84364
84326
  const epoch = this.#epoch;
@@ -84402,7 +84364,7 @@ var PreWarmManager = class {
84402
84364
  return;
84403
84365
  }
84404
84366
  const generation = ++this.#generation;
84405
- this.#epoch = crypto.randomUUID();
84367
+ this.#epoch = null;
84406
84368
  this.#cwd = cwd;
84407
84369
  this.#status = "spawning";
84408
84370
  const slot = createEventSlot();
@@ -84440,6 +84402,7 @@ var PreWarmManager = class {
84440
84402
  settings,
84441
84403
  cwd
84442
84404
  );
84405
+ this.#epoch = this.#subprocess.harnessEpoch;
84443
84406
  emitStep("spawn_call", performance.now() - tSpawnCall);
84444
84407
  this.#startWarmTimer(generation);
84445
84408
  }
@@ -90542,6 +90505,7 @@ function buildCodexThreadConfigOverrides(writableRoots = []) {
90542
90505
  const sandboxWorkspaceWrite = writableRoots.length > 0 ? { network_access: true, writable_roots: [...writableRoots] } : { network_access: true };
90543
90506
  return {
90544
90507
  ...CODEX_THREAD_CONFIG_OVERRIDES,
90508
+ ...buildCodexShellCodexHomeConfigOverride(),
90545
90509
  sandbox_workspace_write: sandboxWorkspaceWrite
90546
90510
  };
90547
90511
  }
@@ -91907,6 +91871,10 @@ var CodexAgentSubprocess = class _CodexAgentSubprocess {
91907
91871
  reason: "Codex prewarm not supported; taskId is set at thread/start only."
91908
91872
  });
91909
91873
  }
91874
+ /** Codex fences its harness MCP via the HTTP `x-shipyard-generation` header / per-runner bearer, not an in-process epoch. */
91875
+ get harnessEpoch() {
91876
+ return null;
91877
+ }
91910
91878
  setOnCwdChanged(handler) {
91911
91879
  this.#cwdHandler = handler;
91912
91880
  }
@@ -92424,6 +92392,10 @@ function createCodexSubprocessFacade(options, log) {
92424
92392
  }
92425
92393
  pending.push({ kind: "setHarnessTaskId", taskId });
92426
92394
  },
92395
+ /** Codex fences via HTTP header / per-runner bearer — always null (delegated for forward-compat). */
92396
+ get harnessEpoch() {
92397
+ return inner ? inner.harnessEpoch : null;
92398
+ },
92427
92399
  setOnCwdChanged(handler) {
92428
92400
  cwdHandler = handler;
92429
92401
  if (inner) {
@@ -92502,6 +92474,10 @@ function createCodexFacadeApiBuilder(d) {
92502
92474
  reconnectMcpServer: (serverName) => codexFacadeReconnectMcpServer(state, serverName),
92503
92475
  stopBackgroundTask: (taskId) => codexFacadeStopBackgroundTask(state, taskId),
92504
92476
  setHarnessTaskId: (taskId) => codexFacadeSetHarnessTaskId(state, taskId),
92477
+ /** Codex fences its harness via HTTP header / per-runner bearer — delegate (always null today). */
92478
+ get harnessEpoch() {
92479
+ return state.inner?.harnessEpoch ?? null;
92480
+ },
92505
92481
  setOnCwdChanged: (handler) => codexFacadeSetOnCwdChanged(state, handler),
92506
92482
  getContextUsage: () => state.inner?.getContextUsage() ?? Promise.resolve(null),
92507
92483
  restartWithExtendedSandbox: (grant) => codexFacadeRestartWithExtendedSandbox(state, grant),
@@ -92962,15 +92938,15 @@ function bindToolToMcpServer(server, tool) {
92962
92938
 
92963
92939
  // src/services/serve-factory/pure-helpers.ts
92964
92940
  function buildCodexSpawnArgs(args) {
92965
- const spawn15 = rewriteElectronNodeScriptSpawn({
92941
+ const spawn16 = rewriteElectronNodeScriptSpawn({
92966
92942
  binary: args.binary,
92967
92943
  argv: args.argv,
92968
92944
  env: args.env
92969
92945
  });
92970
92946
  return {
92971
- binary: spawn15.binary,
92972
- argv: spawn15.argv,
92973
- ...spawn15.env !== void 0 ? { env: spawn15.env } : {},
92947
+ binary: spawn16.binary,
92948
+ argv: spawn16.argv,
92949
+ ...spawn16.env !== void 0 ? { env: spawn16.env } : {},
92974
92950
  stripEnv: args.stripEnv
92975
92951
  };
92976
92952
  }
@@ -93107,14 +93083,14 @@ function createCodexFacadeSubsystem(d) {
93107
93083
  const engine = new CodexEngineSingleton({
93108
93084
  resolveSpawnConfig: async () => {
93109
93085
  const binaryPath = await codexProfile.spawn.binaryResolver() ?? "codex";
93110
- const spawn15 = rewriteElectronNodeScriptSpawn({
93086
+ const spawn16 = rewriteElectronNodeScriptSpawn({
93111
93087
  binary: binaryPath,
93112
93088
  argv: ["app-server"]
93113
93089
  });
93114
93090
  return {
93115
- binaryPath: spawn15.binary,
93116
- args: spawn15.argv,
93117
- ...spawn15.env !== void 0 ? { env: spawn15.env } : {},
93091
+ binaryPath: spawn16.binary,
93092
+ args: spawn16.argv,
93093
+ ...spawn16.env !== void 0 ? { env: spawn16.env } : {},
93118
93094
  stripEnv: codexProfile.spawn.envStrip
93119
93095
  };
93120
93096
  },
@@ -93616,12 +93592,12 @@ function handleTaskStoreBroadcast(deps, event) {
93616
93592
  }
93617
93593
 
93618
93594
  // src/services/serve-factory/viz-preview.ts
93619
- import { join as join58 } from "path";
93595
+ import { join as join59 } from "path";
93620
93596
 
93621
93597
  // src/services/harness/visualization-file-watcher.ts
93622
93598
  import { randomUUID as randomUUID16 } from "crypto";
93623
93599
  import { mkdir as mkdir21, readFile as readFile32, rename as rename17, writeFile as writeFile22 } from "fs/promises";
93624
- import { basename as basename10, dirname as dirname23, join as join56 } from "path";
93600
+ import { basename as basename10, dirname as dirname23, join as join57 } from "path";
93625
93601
  var PREVIEW_DEFAULT_W = 1200;
93626
93602
  var PREVIEW_DEFAULT_H = 800;
93627
93603
  function previewDataToLoroValue(data) {
@@ -93832,7 +93808,7 @@ var VisualizationFileWatcher = class {
93832
93808
  }
93833
93809
  case "persist_registry":
93834
93810
  await atomicWrite2(
93835
- join56(this.#deps.vizDir, effect.taskId, "registry.json"),
93811
+ join57(this.#deps.vizDir, effect.taskId, "registry.json"),
93836
93812
  JSON.stringify(effect.data, null, 2)
93837
93813
  );
93838
93814
  break;
@@ -94100,7 +94076,7 @@ var VisualizationFileWatcher = class {
94100
94076
 
94101
94077
  // src/services/harness/visualization-registry.ts
94102
94078
  import { createHash as createHash5 } from "crypto";
94103
- import { join as join57 } from "path";
94079
+ import { join as join58 } from "path";
94104
94080
  function hashContent(content) {
94105
94081
  return createHash5("sha256").update(content).digest("hex");
94106
94082
  }
@@ -94114,7 +94090,7 @@ var VisualizationRegistry = class {
94114
94090
  return null;
94115
94091
  }
94116
94092
  const ext = vizFileExtension(vizType);
94117
- const filePath = join57(vizDir, taskId, `${slug}${ext}`);
94093
+ const filePath = join58(vizDir, taskId, `${slug}${ext}`);
94118
94094
  const contentHash3 = hashContent(content);
94119
94095
  const viz = {
94120
94096
  slug,
@@ -94265,7 +94241,7 @@ var VisualizationRegistry = class {
94265
94241
  // src/services/serve-factory/viz-preview.ts
94266
94242
  function createVizPreviewRegistry(deps) {
94267
94243
  const { canvasRepo } = deps;
94268
- const vizDir = join58(deps.dataDir, "visualizations");
94244
+ const vizDir = join59(deps.dataDir, "visualizations");
94269
94245
  const vizWatchers = /* @__PURE__ */ new Map();
94270
94246
  function getOrCreateVizWatcher(taskId) {
94271
94247
  const existing = vizWatchers.get(taskId);
@@ -95196,6 +95172,9 @@ import { fork } from "child_process";
95196
95172
  import { randomUUID as cryptoRandomUUID } from "crypto";
95197
95173
  import { fileURLToPath as fileURLToPath3 } from "url";
95198
95174
 
95175
+ // src/services/session/agent-subprocess.ts
95176
+ var RUN_STATUS_ERROR_SUBTYPE = "run_status_error";
95177
+
95199
95178
  // src/services/session/cursor-event-translator.ts
95200
95179
  function createCursorTranslatorCtx(generation) {
95201
95180
  return {
@@ -95445,7 +95424,7 @@ function classifyStatusErrorText(text) {
95445
95424
  return {
95446
95425
  type: "sdk_error",
95447
95426
  error: "Cursor run error (status: ERROR)",
95448
- errorSubtype: "run_status_error"
95427
+ errorSubtype: RUN_STATUS_ERROR_SUBTYPE
95449
95428
  };
95450
95429
  }
95451
95430
  const lower = text.toLowerCase();
@@ -95458,7 +95437,7 @@ function classifyStatusErrorText(text) {
95458
95437
  if (lower.includes("payment") || lower.includes("billing") || lower.includes("insufficient credits") || lower.includes("subscription")) {
95459
95438
  return { type: "billing_error" };
95460
95439
  }
95461
- return { type: "sdk_error", error: text, errorSubtype: "run_status_error" };
95440
+ return { type: "sdk_error", error: text, errorSubtype: RUN_STATUS_ERROR_SUBTYPE };
95462
95441
  }
95463
95442
  function translateStatusMessage(msg) {
95464
95443
  if (msg.status !== "ERROR") return [];
@@ -95611,6 +95590,9 @@ function buildCursorCompactionCompleted(preTokens) {
95611
95590
  ...preTokens !== void 0 ? { preTokens } : {}
95612
95591
  };
95613
95592
  }
95593
+ function toolExecutionSubprocessEvent(phase) {
95594
+ return { type: phase === "started" ? "tool_execution_started" : "tool_execution_settled" };
95595
+ }
95614
95596
 
95615
95597
  // src/services/session/cursor-run-result.ts
95616
95598
  var RunGitBranchInfoSchema = external_exports.object({
@@ -96475,6 +96457,10 @@ var CursorAgentSubprocess = class {
96475
96457
  token: this.#harnessToken
96476
96458
  });
96477
96459
  }
96460
+ /** Cursor fences its harness MCP via the HTTP `x-shipyard-generation` header / per-runner bearer, not an in-process epoch. */
96461
+ get harnessEpoch() {
96462
+ return null;
96463
+ }
96478
96464
  setOnCwdChanged(_handler) {
96479
96465
  }
96480
96466
  async rollback(_numTurns) {
@@ -96528,7 +96514,7 @@ var CursorAgentSubprocess = class {
96528
96514
  * (connectrpc streaming requires HTTP/2), so the agent reaches the model
96529
96515
  * and then errors mid-run. Default HTTP/2 is correct; the run completes.
96530
96516
  */
96531
- env: buildNodeSpawnEnv(process.env)
96517
+ env: { ...buildNodeSpawnEnv(process.env), ...buildAgentSubprocessCodexHomeEnv() }
96532
96518
  });
96533
96519
  this.#child = child;
96534
96520
  const tracker = this.#pidTracker;
@@ -96916,6 +96902,9 @@ var CursorAgentSubprocess = class {
96916
96902
  case "stream_activity":
96917
96903
  this.#onEvent({ type: msg.kind });
96918
96904
  return;
96905
+ case "tool_execution":
96906
+ this.#onEvent(toolExecutionSubprocessEvent(msg.phase));
96907
+ return;
96919
96908
  case "user_message_persisted": {
96920
96909
  logger.info({
96921
96910
  event: "cursor_user_message_persisted",
@@ -97742,7 +97731,7 @@ function createShipyardResolver() {
97742
97731
 
97743
97732
  // src/services/stack-detection.ts
97744
97733
  import { access as access6 } from "fs/promises";
97745
- import { join as join60 } from "path";
97734
+ import { join as join61 } from "path";
97746
97735
  var STACK_TIMEOUT_MS = TIMEOUT_MS;
97747
97736
  async function defaultIsCommandAvailable(command2) {
97748
97737
  try {
@@ -97784,7 +97773,7 @@ async function detectStack(cwd, currentBranch, deps = {}) {
97784
97773
  async function detectGraphite(cwd, currentBranch, isCommandAvailable, fileExists2, exec, getDefaultBranch2) {
97785
97774
  const gtAvailable = await isCommandAvailable("gt");
97786
97775
  if (!gtAvailable) return null;
97787
- const markerPresent = await fileExists2(join60(cwd, ".git", ".graphite_repo_config"));
97776
+ const markerPresent = await fileExists2(join61(cwd, ".git", ".graphite_repo_config"));
97788
97777
  if (!markerPresent) return null;
97789
97778
  const ancestor = await exec("gt", ["parent"], cwd).then((s2) => normalizeBranchName(s2)).catch(() => null);
97790
97779
  const childrenRaw = await exec("gt", ["children"], cwd).then((s2) => s2).catch(() => "");
@@ -97797,7 +97786,7 @@ async function detectGraphite(cwd, currentBranch, isCommandAvailable, fileExists
97797
97786
  return { ancestor: resolvedAncestor, descendants };
97798
97787
  }
97799
97788
  async function detectJujutsu(cwd, fileExists2, exec) {
97800
- const jjPresent = await fileExists2(join60(cwd, ".jj"));
97789
+ const jjPresent = await fileExists2(join61(cwd, ".jj"));
97801
97790
  if (!jjPresent) return null;
97802
97791
  const ancestorRaw = await exec(
97803
97792
  "jj",
@@ -97846,7 +97835,7 @@ function parseJjLines(raw) {
97846
97835
 
97847
97836
  // src/services/storage/annotation-store.ts
97848
97837
  import { mkdir as mkdir22, readFile as readFile34 } from "fs/promises";
97849
- import { dirname as dirname25, join as join61 } from "path";
97838
+ import { dirname as dirname25, join as join62 } from "path";
97850
97839
  var LegacyBaseFields = external_exports.object({
97851
97840
  commentId: external_exports.string(),
97852
97841
  body: external_exports.string(),
@@ -97873,7 +97862,7 @@ var LegacyPlanStoreSchema = external_exports.object({
97873
97862
  versions: external_exports.array(PlanVersionZodSchema)
97874
97863
  });
97875
97864
  function buildAnnotationStore(dataDir) {
97876
- const baseDir = join61(dataDir, "annotations");
97865
+ const baseDir = join62(dataDir, "annotations");
97877
97866
  const cache2 = /* @__PURE__ */ new Map();
97878
97867
  const listeners = /* @__PURE__ */ new Set();
97879
97868
  const writeQueues = /* @__PURE__ */ new Map();
@@ -97886,7 +97875,7 @@ function buildAnnotationStore(dataDir) {
97886
97875
  }
97887
97876
  }
97888
97877
  function filePath(taskId) {
97889
- return join61(baseDir, `${taskId}.json`);
97878
+ return join62(baseDir, `${taskId}.json`);
97890
97879
  }
97891
97880
  function emptyData() {
97892
97881
  return {
@@ -97951,9 +97940,9 @@ function buildAnnotationStore(dataDir) {
97951
97940
  }
97952
97941
  async function migrateLegacyFiles(taskId) {
97953
97942
  const [diffRaw, previewRaw, planRaw] = await Promise.all([
97954
- readLegacyFile(join61(dataDir, "diff-review", `${taskId}.json`)),
97955
- readLegacyFile(join61(dataDir, "preview-annotations", `${taskId}.json`)),
97956
- readLegacyFile(join61(dataDir, "plan-comments", `${taskId}.json`))
97943
+ readLegacyFile(join62(dataDir, "diff-review", `${taskId}.json`)),
97944
+ readLegacyFile(join62(dataDir, "preview-annotations", `${taskId}.json`)),
97945
+ readLegacyFile(join62(dataDir, "plan-comments", `${taskId}.json`))
97957
97946
  ]);
97958
97947
  if (diffRaw === null && previewRaw === null && planRaw === null) {
97959
97948
  return null;
@@ -98156,7 +98145,7 @@ function buildAnnotationStore(dataDir) {
98156
98145
 
98157
98146
  // src/services/storage/asset-store.ts
98158
98147
  import { mkdir as mkdir23, readFile as readFile35, rename as rename18, writeFile as writeFile23 } from "fs/promises";
98159
- import { join as join62 } from "path";
98148
+ import { join as join63 } from "path";
98160
98149
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
98161
98150
  function isValidAssetId(id) {
98162
98151
  return UUID_RE.test(id);
@@ -98182,10 +98171,10 @@ function buildAssetStore(assetsDir) {
98182
98171
  }
98183
98172
  }
98184
98173
  function binaryPath(assetId) {
98185
- return join62(assetsDir, `${assetId}.bin`);
98174
+ return join63(assetsDir, `${assetId}.bin`);
98186
98175
  }
98187
98176
  function metaPath(assetId) {
98188
- return join62(assetsDir, `${assetId}.meta.json`);
98177
+ return join63(assetsDir, `${assetId}.meta.json`);
98189
98178
  }
98190
98179
  function parseMeta(raw) {
98191
98180
  return AssetMetadataSchema.parse(JSON.parse(raw));
@@ -98209,8 +98198,8 @@ function buildAssetStore(assetsDir) {
98209
98198
  size: data.byteLength,
98210
98199
  createdAt: Date.now()
98211
98200
  };
98212
- const tmpBin = join62(assetsDir, `${assetId}.bin.tmp`);
98213
- const tmpMeta = join62(assetsDir, `${assetId}.meta.tmp`);
98201
+ const tmpBin = join63(assetsDir, `${assetId}.bin.tmp`);
98202
+ const tmpMeta = join63(assetsDir, `${assetId}.meta.tmp`);
98214
98203
  await writeFile23(tmpBin, data);
98215
98204
  await writeFile23(tmpMeta, JSON.stringify(metadata), "utf-8");
98216
98205
  await rename18(tmpBin, binaryPath(assetId));
@@ -98435,7 +98424,7 @@ function buildCredentialsVaultStore(filePath, corruptionLogger) {
98435
98424
 
98436
98425
  // src/services/storage/deliverable-store.ts
98437
98426
  import { mkdir as mkdir25, readFile as readFile37 } from "fs/promises";
98438
- import { dirname as dirname27, join as join63 } from "path";
98427
+ import { dirname as dirname27, join as join64 } from "path";
98439
98428
  var DELIVERABLE_STORE_VERSION = 1;
98440
98429
  var DeliverableStoreSchema = external_exports.object({
98441
98430
  schemaVersion: external_exports.number(),
@@ -98449,7 +98438,7 @@ function migrateDeliverableStore(raw) {
98449
98438
  return { schemaVersion: DELIVERABLE_STORE_VERSION, records: [] };
98450
98439
  }
98451
98440
  function buildDeliverableStore(dataDir) {
98452
- const baseDir = join63(dataDir, "deliverables");
98441
+ const baseDir = join64(dataDir, "deliverables");
98453
98442
  const cache2 = /* @__PURE__ */ new Map();
98454
98443
  const listeners = /* @__PURE__ */ new Set();
98455
98444
  const writeQueues = /* @__PURE__ */ new Map();
@@ -98462,7 +98451,7 @@ function buildDeliverableStore(dataDir) {
98462
98451
  }
98463
98452
  }
98464
98453
  function filePath(taskId) {
98465
- return join63(baseDir, `${taskId}.json`);
98454
+ return join64(baseDir, `${taskId}.json`);
98466
98455
  }
98467
98456
  function emptyData() {
98468
98457
  return { schemaVersion: DELIVERABLE_STORE_VERSION, records: [] };
@@ -98602,7 +98591,7 @@ function buildDeliverableStore(dataDir) {
98602
98591
 
98603
98592
  // src/services/storage/jsonl-conversation-store.ts
98604
98593
  import { appendFile as appendFile3, mkdir as mkdir26, open, readFile as readFile38, stat as stat16 } from "fs/promises";
98605
- import { join as join64 } from "path";
98594
+ import { join as join65 } from "path";
98606
98595
  var StoredMessageSchema = MessageSchema.omit({ seqNo: true, channelId: true });
98607
98596
  function logPerf(entry) {
98608
98597
  try {
@@ -98612,12 +98601,12 @@ function logPerf(entry) {
98612
98601
  }
98613
98602
  }
98614
98603
  function buildJsonlConversationStore(dataDir, opts = {}) {
98615
- const channelsDir = join64(dataDir, "channels");
98604
+ const channelsDir = join65(dataDir, "channels");
98616
98605
  const seqCounters = /* @__PURE__ */ new Map();
98617
98606
  const channelQueues = /* @__PURE__ */ new Map();
98618
98607
  const readCache = /* @__PURE__ */ new Map();
98619
98608
  function channelPath(channelId) {
98620
- return join64(channelsDir, `${channelId}.jsonl`);
98609
+ return join65(channelsDir, `${channelId}.jsonl`);
98621
98610
  }
98622
98611
  async function ensureDir() {
98623
98612
  await mkdir26(channelsDir, { recursive: true });
@@ -99389,7 +99378,7 @@ function buildProjectsStore(filePath) {
99389
99378
 
99390
99379
  // src/services/storage/rate-limit-store.ts
99391
99380
  import { mkdir as mkdir28, readFile as readFile40 } from "fs/promises";
99392
- import { dirname as dirname29, join as join65 } from "path";
99381
+ import { dirname as dirname29, join as join66 } from "path";
99393
99382
  var RATE_LIMIT_STORE_VERSION = 4;
99394
99383
  var RateLimitRecordSchema = external_exports.object({
99395
99384
  info: RateLimitInfoSchema,
@@ -99428,7 +99417,7 @@ function isWindowKey(x2) {
99428
99417
  return x2 !== void 0 && WINDOW_KEYS.has(x2);
99429
99418
  }
99430
99419
  async function buildRateLimitStore(dataDir, opts) {
99431
- const filePath = join65(dataDir, "rate-limits.json");
99420
+ const filePath = join66(dataDir, "rate-limits.json");
99432
99421
  const lockPath2 = `${filePath}.lock`;
99433
99422
  const initial = await loadStoreFile(filePath, opts.logger);
99434
99423
  const records = { ...initial.records };
@@ -99681,7 +99670,7 @@ async function atomicWrite4(filePath, data) {
99681
99670
  }
99682
99671
 
99683
99672
  // src/services/storage/schedule-store.ts
99684
- import { join as join66 } from "path";
99673
+ import { join as join67 } from "path";
99685
99674
 
99686
99675
  // src/services/storage/json-document-store.ts
99687
99676
  import { mkdir as mkdir29, readFile as readFile41 } from "fs/promises";
@@ -99872,7 +99861,7 @@ function buildScheduleStore(dataDir) {
99872
99861
  const store = buildJsonDocumentStore({
99873
99862
  storeName: "schedules",
99874
99863
  docType: "schedule",
99875
- filePath: join66(dataDir, "schedules.json"),
99864
+ filePath: join67(dataDir, "schedules.json"),
99876
99865
  recordSchema: ScheduleRecordSchema,
99877
99866
  currentVersion: SCHEDULE_STORE_VERSION,
99878
99867
  migrate(raw) {
@@ -99911,7 +99900,7 @@ function buildScheduleStore(dataDir) {
99911
99900
 
99912
99901
  // src/services/storage/session-persistence.ts
99913
99902
  import { mkdir as mkdir30, readdir as readdir19, readFile as readFile42, rm as rm12 } from "fs/promises";
99914
- import { join as join67 } from "path";
99903
+ import { join as join68 } from "path";
99915
99904
  var PersistedSessionSchema = external_exports.object({
99916
99905
  sessionId: external_exports.string(),
99917
99906
  channelId: external_exports.string(),
@@ -99968,9 +99957,9 @@ async function loadOneSessionFile(originalPath, corruptionLogger) {
99968
99957
  }
99969
99958
  }
99970
99959
  function buildSessionPersistence(dataDir, corruptionLogger) {
99971
- const channelsDir = join67(dataDir, "channels");
99960
+ const channelsDir = join68(dataDir, "channels");
99972
99961
  function sessionPath(channelId) {
99973
- return join67(channelsDir, `${channelId}.session.json`);
99962
+ return join68(channelsDir, `${channelId}.session.json`);
99974
99963
  }
99975
99964
  async function ensureDir() {
99976
99965
  await mkdir30(channelsDir, { recursive: true });
@@ -100028,7 +100017,7 @@ function buildSessionPersistence(dataDir, corruptionLogger) {
100028
100017
  if (i >= sessionEntries.length) return;
100029
100018
  const entry = sessionEntries[i];
100030
100019
  if (!entry) return;
100031
- results[i] = await loadOneSessionFile(join67(channelsDir, entry), corruptionLogger);
100020
+ results[i] = await loadOneSessionFile(join68(channelsDir, entry), corruptionLogger);
100032
100021
  }
100033
100022
  }
100034
100023
  const workers = Array.from(
@@ -100042,12 +100031,12 @@ function buildSessionPersistence(dataDir, corruptionLogger) {
100042
100031
  }
100043
100032
 
100044
100033
  // src/services/storage/template-store.ts
100045
- import { join as join68 } from "path";
100034
+ import { join as join69 } from "path";
100046
100035
  function buildTemplateStore(dataDir) {
100047
100036
  const store = buildJsonDocumentStore({
100048
100037
  storeName: "templates",
100049
100038
  docType: "template",
100050
- filePath: join68(dataDir, "templates.json"),
100039
+ filePath: join69(dataDir, "templates.json"),
100051
100040
  recordSchema: TaskTemplateRecordSchema,
100052
100041
  currentVersion: TEMPLATE_STORE_VERSION,
100053
100042
  migrate(raw) {
@@ -102743,6 +102732,20 @@ function emitSessionDeath(b2, event, _origin) {
102743
102732
  function isCleanExit(event) {
102744
102733
  return event.exitCode === 0 && (event.signal === null || event.signal === void 0);
102745
102734
  }
102735
+ function stallHeartbeatEffect(event) {
102736
+ switch (event.type) {
102737
+ case "step_progress":
102738
+ return { type: "reset_stall_timer", firstStep: true, toolExecution: "none" };
102739
+ case "stream_activity":
102740
+ return { type: "reset_stall_timer", firstStep: false, toolExecution: "none" };
102741
+ case "tool_execution_started":
102742
+ return { type: "reset_stall_timer", firstStep: false, toolExecution: "latch" };
102743
+ case "tool_execution_settled":
102744
+ return { type: "reset_stall_timer", firstStep: false, toolExecution: "unlatch" };
102745
+ default:
102746
+ return null;
102747
+ }
102748
+ }
102746
102749
  function handleSpawning(snapshot, event) {
102747
102750
  const b2 = createEffectBuilder();
102748
102751
  if (event.type === "init_received") {
@@ -102833,8 +102836,9 @@ function handleSpawning(snapshot, event) {
102833
102836
  effects: b2.effects
102834
102837
  };
102835
102838
  }
102836
- if (event.type === "step_progress" || event.type === "stream_activity") {
102837
- b2.effects.push({ type: "reset_stall_timer", firstStep: event.type === "step_progress" });
102839
+ const heartbeat = stallHeartbeatEffect(event);
102840
+ if (heartbeat) {
102841
+ b2.effects.push(heartbeat);
102838
102842
  return {
102839
102843
  state: "spawning",
102840
102844
  sessionId: snapshot.sessionId,
@@ -102862,6 +102866,28 @@ function handleSpawning(snapshot, event) {
102862
102866
  }
102863
102867
  return noop2(snapshot);
102864
102868
  }
102869
+ function runningStallTimeout(sessionId, event) {
102870
+ const b2 = createEffectBuilder();
102871
+ b2.log("running", "resumable_idle", event.type);
102872
+ b2.effects.push({ type: "clear_pending_inputs" });
102873
+ b2.effects.push({ type: "clear_queue" });
102874
+ if (!event.recoveryArmed) {
102875
+ b2.taskStatus("input_required");
102876
+ b2.emitError(
102877
+ "Cursor stopped responding mid-run. Send a message to try again.",
102878
+ "sdk_error",
102879
+ "run_stall_timeout"
102880
+ );
102881
+ }
102882
+ b2.effects.push({ type: "kill_runner" });
102883
+ return {
102884
+ state: "resumable_idle",
102885
+ sessionId,
102886
+ rewindAtMessageId: null,
102887
+ awaitingSetup: false,
102888
+ effects: b2.effects
102889
+ };
102890
+ }
102865
102891
  function handleRunning(snapshot, event) {
102866
102892
  const { sessionId } = snapshot;
102867
102893
  const b2 = createEffectBuilder();
@@ -102987,8 +103013,9 @@ function handleRunning(snapshot, event) {
102987
103013
  effects: b2.effects
102988
103014
  };
102989
103015
  }
102990
- if (event.type === "step_progress" || event.type === "stream_activity") {
102991
- b2.effects.push({ type: "reset_stall_timer", firstStep: event.type === "step_progress" });
103016
+ const heartbeat = stallHeartbeatEffect(event);
103017
+ if (heartbeat) {
103018
+ b2.effects.push(heartbeat);
102992
103019
  return {
102993
103020
  state: "running",
102994
103021
  sessionId,
@@ -102998,23 +103025,7 @@ function handleRunning(snapshot, event) {
102998
103025
  };
102999
103026
  }
103000
103027
  if (event.type === "run_stall_timeout") {
103001
- b2.log("running", "resumable_idle", event.type);
103002
- b2.effects.push({ type: "clear_pending_inputs" });
103003
- b2.effects.push({ type: "clear_queue" });
103004
- b2.taskStatus("input_required");
103005
- b2.emitError(
103006
- "Cursor stopped responding mid-run. Send a message to try again.",
103007
- "sdk_error",
103008
- "run_stall_timeout"
103009
- );
103010
- b2.effects.push({ type: "kill_runner" });
103011
- return {
103012
- state: "resumable_idle",
103013
- sessionId,
103014
- rewindAtMessageId: null,
103015
- awaitingSetup: false,
103016
- effects: b2.effects
103017
- };
103028
+ return runningStallTimeout(sessionId, event);
103018
103029
  }
103019
103030
  return noop2(snapshot);
103020
103031
  }
@@ -103175,6 +103186,21 @@ var AgentSessionManager = class {
103175
103186
  * reset can only make the watchdog fire sooner, never mask a hang.
103176
103187
  */
103177
103188
  #lastStreamActivityAt = null;
103189
+ /**
103190
+ * True while a Cursor tool call is in flight (between `tool_execution_started`
103191
+ * and `tool_execution_settled` FSM events). When true, `#chooseStallTimeoutMs`
103192
+ * returns `toolExecutionStallTimeoutMs` (if non-null) instead of the shorter
103193
+ * mid-run window. Cleared explicitly in three places: the `spawn` effect
103194
+ * case (a fresh subprocess generation starts unlatched), the `kill_runner`
103195
+ * effect case (a stall death must not leave stale latch state), and
103196
+ * `#manageTimers`'s leaving-active branch (a turn whose final tool never
103197
+ * settled must not leak the long window into a warm-resume turn).
103198
+ *
103199
+ * IMPORTANT: do NOT clear inside `#clearStallTimer` / `#restartStallTimer` —
103200
+ * `#restartStallTimer` calls `#clearStallTimer` before re-arming, so clearing
103201
+ * the latch there would wipe it on every timer reset.
103202
+ */
103203
+ #toolExecutionInFlight = false;
103178
103204
  constructor(config, initialSnapshot2) {
103179
103205
  this.#config = config;
103180
103206
  if (initialSnapshot2) {
@@ -103325,6 +103351,21 @@ var AgentSessionManager = class {
103325
103351
  notifyStepProgress() {
103326
103352
  this.#dispatch({ type: "step_progress" });
103327
103353
  }
103354
+ /**
103355
+ * Notify the session manager of a tool-execution liveness edge. Phase
103356
+ * `'started'` arms the extended per-tool stall window (0→1 transition);
103357
+ * `'settled'` reverts to the shorter mid-run window (1→0 transition).
103358
+ * No-op when the watchdog is disabled (Claude/Codex set
103359
+ * `toolExecutionStallTimeoutMs: null`).
103360
+ */
103361
+ notifyToolExecution(phase) {
103362
+ if (this.#config.toolExecutionStallTimeoutMs === null) {
103363
+ return;
103364
+ }
103365
+ this.#dispatch(
103366
+ phase === "started" ? { type: "tool_execution_started" } : { type: "tool_execution_settled" }
103367
+ );
103368
+ }
103328
103369
  /**
103329
103370
  * Heartbeat for streamed output (assistant/thinking/tool deltas) that proves
103330
103371
  * the run is alive mid-step. Resets the stall watchdog WITHOUT retiring the
@@ -103398,6 +103439,7 @@ var AgentSessionManager = class {
103398
103439
  this.#lastSpawnAtMs = Date.now();
103399
103440
  this.#firstStepReceived = false;
103400
103441
  this.#lastStreamActivityAt = null;
103442
+ this.#toolExecutionInFlight = false;
103401
103443
  this.#config.onSpawn(effect.reason, initial?.content ?? []);
103402
103444
  break;
103403
103445
  }
@@ -103447,12 +103489,10 @@ var AgentSessionManager = class {
103447
103489
  }
103448
103490
  break;
103449
103491
  case "reset_stall_timer":
103450
- if (effect.firstStep) {
103451
- this.#firstStepReceived = true;
103452
- }
103453
- this.#restartStallTimer();
103492
+ this.#applyResetStallTimer(effect);
103454
103493
  break;
103455
103494
  case "kill_runner":
103495
+ this.#toolExecutionInFlight = false;
103456
103496
  this.#clearStallTimer();
103457
103497
  this.#config.onKillRunner();
103458
103498
  break;
@@ -103463,13 +103503,45 @@ var AgentSessionManager = class {
103463
103503
  }
103464
103504
  }
103465
103505
  }
103506
+ /**
103507
+ * `firstStep` (a completed step boundary from `step_progress`) means the
103508
+ * cold start succeeded and we're in the mid-run cadence from now on — flip
103509
+ * the latch so `#chooseStallTimeoutMs` picks the mid-run budget for
103510
+ * subsequent arms. A `stream_activity` heartbeat (`firstStep: false`) only
103511
+ * proves liveness mid-step, so it resets the timer but leaves the cold
103512
+ * window in force until a real step lands.
103513
+ *
103514
+ * `toolExecution` updates the in-flight latch BEFORE restarting the timer
103515
+ * so the new window is chosen with the updated latch state: `'latch'` arms
103516
+ * the extended per-tool window, `'unlatch'` reverts to the mid-run window,
103517
+ * `'none'` leaves it unchanged (heartbeats like `step_progress` /
103518
+ * `stream_activity` must NOT unlatch mid-tool).
103519
+ */
103520
+ #applyResetStallTimer(effect) {
103521
+ if (effect.firstStep) {
103522
+ this.#firstStepReceived = true;
103523
+ }
103524
+ if (effect.toolExecution === "latch") {
103525
+ this.#toolExecutionInFlight = true;
103526
+ } else if (effect.toolExecution === "unlatch") {
103527
+ this.#toolExecutionInFlight = false;
103528
+ }
103529
+ this.#restartStallTimer();
103530
+ }
103531
+ /**
103532
+ * Effect-ordering dependency: `log_transition` is emitted BEFORE
103533
+ * `kill_runner` in the `run_stall_timeout` effect list, so the latch (and
103534
+ * `#firstStepReceived`) are still in their at-fire values when this reads
103535
+ * them. Reordering those effects would silently misattribute `stallPhase`.
103536
+ */
103466
103537
  #telemeterRunStallTimeout(effect) {
103467
103538
  if (effect.trigger !== "run_stall_timeout") return;
103539
+ const stallPhase = this.#toolExecutionInFlight ? "tool_execution" : this.#firstStepReceived ? "mid_run" : "first_step";
103468
103540
  telemeter("run_stall_timeout", {
103469
103541
  taskId: this.#config.taskId,
103470
103542
  from: effect.from,
103471
103543
  to: effect.to,
103472
- stallPhase: this.#firstStepReceived ? "mid_run" : "first_step",
103544
+ stallPhase,
103473
103545
  stallTimeoutMs: this.#chooseStallTimeoutMs()
103474
103546
  });
103475
103547
  }
@@ -103510,16 +103582,21 @@ var AgentSessionManager = class {
103510
103582
  }
103511
103583
  } else {
103512
103584
  this.#clearStallTimer();
103585
+ this.#toolExecutionInFlight = false;
103513
103586
  }
103514
103587
  }
103515
103588
  /**
103516
- * Pick the active stall timeout. Before the first step, the cold-start
103517
- * budget (if configured) wins; after it, the mid-run budget takes over.
103518
- * The cold-start field falls back to the mid-run value so a profile that
103519
- * only sets `stallTimeoutMs` keeps its prior single-window behavior.
103589
+ * Pick the active stall timeout. Priority order:
103590
+ * 1. Tool-execution latch active `toolExecutionStallTimeoutMs` (if non-null).
103591
+ * 2. Before first step `firstStepStallTimeoutMs` (if configured, else falls
103592
+ * back to `stallTimeoutMs`).
103593
+ * 3. After first step → `stallTimeoutMs`.
103520
103594
  * Returns `null` to disable the watchdog entirely (Claude/Codex today).
103521
103595
  */
103522
103596
  #chooseStallTimeoutMs() {
103597
+ if (this.#toolExecutionInFlight && this.#config.toolExecutionStallTimeoutMs !== null) {
103598
+ return this.#config.toolExecutionStallTimeoutMs;
103599
+ }
103523
103600
  const mid = this.#config.stallTimeoutMs;
103524
103601
  if (!this.#firstStepReceived) {
103525
103602
  const cold = this.#config.firstStepStallTimeoutMs;
@@ -103549,8 +103626,12 @@ var AgentSessionManager = class {
103549
103626
  this.#manageTimers();
103550
103627
  return;
103551
103628
  }
103552
- this.#config.onStallTimeout?.();
103553
- this.#dispatch({ type: "run_stall_timeout" });
103629
+ let recoveryArmed = false;
103630
+ try {
103631
+ recoveryArmed = this.#config.onStallTimeout?.() ?? false;
103632
+ } finally {
103633
+ this.#dispatch({ type: "run_stall_timeout", recoveryArmed });
103634
+ }
103554
103635
  }, timeoutMs);
103555
103636
  }
103556
103637
  #restartStallTimer() {
@@ -103578,10 +103659,12 @@ var AgentSessionManager = class {
103578
103659
  // src/services/conversation/cursor-recovery-controller.ts
103579
103660
  var MAX_SIGNAL_DEATH_RETRIES = 1;
103580
103661
  var MAX_STALL_RESUME_RETRIES = 1;
103662
+ var MAX_CRASH_DEATH_RETRIES = 1;
103581
103663
  var CursorRecoveryController = class {
103582
103664
  #signalDeathRetries = 0;
103583
103665
  #stallResumeRetries = 0;
103584
103666
  #stallResumeArmed = false;
103667
+ #crashDeathRetries = 0;
103585
103668
  #deps;
103586
103669
  constructor(deps) {
103587
103670
  this.#deps = deps;
@@ -103591,6 +103674,7 @@ var CursorRecoveryController = class {
103591
103674
  this.#signalDeathRetries = 0;
103592
103675
  this.#stallResumeRetries = 0;
103593
103676
  this.#stallResumeArmed = false;
103677
+ this.#crashDeathRetries = 0;
103594
103678
  }
103595
103679
  /**
103596
103680
  * Fired by the manager when the stall watchdog expires, BEFORE the
@@ -103599,10 +103683,14 @@ var CursorRecoveryController = class {
103599
103683
  * banner and strands the orphaned turn until the user returns. Arm a one-shot
103600
103684
  * resume so the imminent `kill_runner` death is resumed instead of surfaced.
103601
103685
  * On repeat (cap exceeded) stay armed-off and let the existing banner run.
103686
+ *
103687
+ * Returns true when a recovery was armed — the manager passes this as
103688
+ * `recoveryArmed` on the `run_stall_timeout` FSM event so the FSM suppresses
103689
+ * the user-facing error banner during the 5-second self-heal (Fix B2).
103602
103690
  */
103603
103691
  onStallTimeout() {
103604
- if (this.#deps.getState() !== "running") return;
103605
- if (this.#deps.getAgentSystem() !== AGENT_SYSTEM_CURSOR) return;
103692
+ if (this.#deps.getState() !== "running") return false;
103693
+ if (this.#deps.getAgentSystem() !== AGENT_SYSTEM_CURSOR) return false;
103606
103694
  this.#stallResumeRetries++;
103607
103695
  if (this.#stallResumeRetries > MAX_STALL_RESUME_RETRIES) {
103608
103696
  this.#deps.log({
@@ -103611,7 +103699,7 @@ var CursorRecoveryController = class {
103611
103699
  retries: this.#stallResumeRetries
103612
103700
  });
103613
103701
  this.#stallResumeRetries = 0;
103614
- return;
103702
+ return false;
103615
103703
  }
103616
103704
  this.#deps.log({
103617
103705
  event: "thread_cursor_stall_resume_armed",
@@ -103619,6 +103707,7 @@ var CursorRecoveryController = class {
103619
103707
  retries: this.#stallResumeRetries
103620
103708
  });
103621
103709
  this.#stallResumeArmed = true;
103710
+ return true;
103622
103711
  }
103623
103712
  /**
103624
103713
  * Consume an armed stall resume on the `kill_runner` death. By now the FSM is
@@ -103633,6 +103722,48 @@ var CursorRecoveryController = class {
103633
103722
  this.#deps.respawnForResume();
103634
103723
  return true;
103635
103724
  }
103725
+ /**
103726
+ * One-shot resume on a Cursor transport-crash death (exit codes 2 or 3).
103727
+ *
103728
+ * Exit 3 = `unhandledRejection` (e.g. NGHTTP2_REFUSED_STREAM /
103729
+ * ENHANCE_YOUR_CALM from the Cursor backend's HTTP/2 rate-limiter escaping
103730
+ * @cursor/sdk). Exit 2 = `uncaughtException` (native or SDK throw). Both
103731
+ * indicate the runner crashed due to a transient transport fault — not a
103732
+ * controlled teardown. One silent auto-retry lets the session resume; a
103733
+ * second failure in the same turn surfaces normally so a deterministic bug
103734
+ * cannot loop.
103735
+ *
103736
+ * Exits 0 (clean) and 1 (parent-disconnect) are never retried here — they
103737
+ * surface as today. Signal deaths are covered by `trySignalDeathRetry`.
103738
+ */
103739
+ tryCrashDeathRetry(event) {
103740
+ if (this.#deps.getState() !== "running") return false;
103741
+ if (this.#deps.getAgentSystem() !== AGENT_SYSTEM_CURSOR) return false;
103742
+ if (event.forceKilled === true) return false;
103743
+ if (event.signal !== null && event.signal !== void 0) return false;
103744
+ if (event.exitCode !== 2 && event.exitCode !== 3) return false;
103745
+ this.#crashDeathRetries++;
103746
+ if (this.#crashDeathRetries > MAX_CRASH_DEATH_RETRIES) {
103747
+ this.#deps.log({
103748
+ event: "thread_cursor_crash_death_retry_cap_exceeded",
103749
+ threadId: this.#deps.threadId,
103750
+ exitCode: event.exitCode,
103751
+ retries: this.#crashDeathRetries
103752
+ });
103753
+ this.#crashDeathRetries = 0;
103754
+ return false;
103755
+ }
103756
+ this.#deps.log({
103757
+ event: "thread_cursor_crash_death_auto_retry",
103758
+ threadId: this.#deps.threadId,
103759
+ exitCode: event.exitCode,
103760
+ retries: this.#crashDeathRetries
103761
+ });
103762
+ this.#deps.clearSubprocess();
103763
+ this.#deps.notifyAbortRetry();
103764
+ this.#deps.respawnForResume();
103765
+ return true;
103766
+ }
103636
103767
  /**
103637
103768
  * One-shot resume on a natural Cursor signal death (#4060). Skips forced,
103638
103769
  * SIGKILL/SIGABRT, signal-less, and repeated deaths. Returns true when it
@@ -103853,6 +103984,7 @@ function rewriteSpawnError(error) {
103853
103984
  var MAX_CONSECUTIVE_RETRIES = 2;
103854
103985
  var MAX_EDE_DIAGNOSTIC_RETRIES = 2;
103855
103986
  var MAX_REQUEST_ABORTED_RETRIES = 1;
103987
+ var MAX_RUN_STATUS_ERROR_RETRIES = 1;
103856
103988
  var REQUEST_ABORTED_PHRASES = ["Request was aborted", "request was aborted"];
103857
103989
  var SessionRecoveryController = class {
103858
103990
  #deps;
@@ -103884,6 +104016,13 @@ var SessionRecoveryController = class {
103884
104016
  #sawResumeFailure = false;
103885
104017
  /** Consecutive transient "Request was aborted" retries; reset on turn_complete, cap-surface, and new user turn. */
103886
104018
  #requestAbortedRetries = 0;
104019
+ /**
104020
+ * Consecutive Cursor `run_status_error` (backend run marked ERROR) retries;
104021
+ * reset on turn_complete, cap-surface, and new user turn — NOT on
104022
+ * init_received, since the retry respawn itself fires init_received and an
104023
+ * early reset would let a persistently-erroring backend loop forever.
104024
+ */
104025
+ #runStatusErrorRetries = 0;
103887
104026
  /**
103888
104027
  * Counts consecutive ede_diagnostic session invalidations. Reset on a
103889
104028
  * successful turn_complete. If this exceeds MAX_EDE_DIAGNOSTIC_RETRIES the
@@ -103941,9 +104080,11 @@ var SessionRecoveryController = class {
103941
104080
  resetRetriesOnTurnComplete() {
103942
104081
  this.#edeDiagnosticRetries = 0;
103943
104082
  this.#requestAbortedRetries = 0;
104083
+ this.#runStatusErrorRetries = 0;
103944
104084
  }
103945
104085
  noteNewUserTurn() {
103946
104086
  this.#requestAbortedRetries = 0;
104087
+ this.#runStatusErrorRetries = 0;
103947
104088
  }
103948
104089
  noteToolUseStarted(toolName, toolUseId) {
103949
104090
  this.#lastToolUseName = toolName;
@@ -104058,7 +104199,8 @@ var SessionRecoveryController = class {
104058
104199
  *
104059
104200
  * A consecutive retry counter prevents infinite loops as a safety net.
104060
104201
  */
104061
- handleSdkError(error) {
104202
+ handleSdkError(error, errorSubtype) {
104203
+ const isRetrying = error.startsWith("Codex (retrying):");
104062
104204
  this.#deps.log({
104063
104205
  event: "thread_sdk_error",
104064
104206
  threadId: this.#deps.threadId,
@@ -104068,7 +104210,8 @@ var SessionRecoveryController = class {
104068
104210
  * error column. Using `error` causes the SDK message to be dropped
104069
104211
  * from log triage (3,329 silent failures observed in one window).
104070
104212
  */
104071
- err: error
104213
+ err: error,
104214
+ ...isRetrying ? { level: "info" } : {}
104072
104215
  });
104073
104216
  if (error.includes("[ede_diagnostic]")) {
104074
104217
  this.#edeDiagnosticRetries++;
@@ -104104,6 +104247,7 @@ var SessionRecoveryController = class {
104104
104247
  effectiveSpawnMode: this.#effectiveSpawnMode.kind
104105
104248
  });
104106
104249
  }
104250
+ if (this.#tryRunStatusErrorRetry(error, errorSubtype)) return;
104107
104251
  if (this.#tryRequestAbortedRetry(error)) return;
104108
104252
  if (this.#tryForkFailedRetry(error)) return;
104109
104253
  if (this.#tryStaleResumeRetry(error)) return;
@@ -104115,6 +104259,43 @@ var SessionRecoveryController = class {
104115
104259
  }
104116
104260
  this.#deps.notifySdkError(surfaceMsg);
104117
104261
  }
104262
+ /**
104263
+ * One-shot resume when Cursor's backend marks a run `status: ERROR`
104264
+ * (translator subtype `run_status_error`). The message is opaque server
104265
+ * state with no text — observed in production 1ms before an
104266
+ * NGHTTP2_ENHANCE_YOUR_CALM stream rejection from the same runner, i.e.
104267
+ * backend rate-limiting, not a session problem (#4229). The session JSONL
104268
+ * is intact, so a silent resume absorbs the blip; the cap surfaces a
104269
+ * persistent backend failure with actionable copy instead of the bare
104270
+ * "(status: ERROR)" string.
104271
+ */
104272
+ #tryRunStatusErrorRetry(error, errorSubtype) {
104273
+ if (errorSubtype !== RUN_STATUS_ERROR_SUBTYPE) return false;
104274
+ if (this.#deps.getState() !== "running") return false;
104275
+ this.#runStatusErrorRetries++;
104276
+ if (this.#runStatusErrorRetries > MAX_RUN_STATUS_ERROR_RETRIES) {
104277
+ this.#deps.log({
104278
+ event: "thread_run_status_error_retry_cap_exceeded",
104279
+ threadId: this.#deps.threadId,
104280
+ retries: this.#runStatusErrorRetries,
104281
+ error
104282
+ });
104283
+ this.#runStatusErrorRetries = 0;
104284
+ const surfaceMsg = "Cursor's backend errored this run twice in a row \u2014 it may be rate-limiting this machine. Wait a moment, then send a message to retry.";
104285
+ this.#deps.persistError(surfaceMsg);
104286
+ this.#deps.notifySdkError(surfaceMsg);
104287
+ return true;
104288
+ }
104289
+ this.#deps.log({
104290
+ event: "thread_run_status_error_auto_retry",
104291
+ threadId: this.#deps.threadId,
104292
+ retries: this.#runStatusErrorRetries,
104293
+ error
104294
+ });
104295
+ this.#deps.notifyAbortRetry();
104296
+ this.#deps.respawnForResume();
104297
+ return true;
104298
+ }
104118
104299
  /**
104119
104300
  * Retry path 1: the first spawn used a fork/resume from parent and the SDK
104120
104301
  * rejected the session (stale/expired). Retry with a fresh session.
@@ -104526,7 +104707,8 @@ var Thread = class {
104526
104707
  log: config.log,
104527
104708
  onSessionStateChange: (newState, prevState) => this.#notifyStateChange(newState, prevState),
104528
104709
  stallTimeoutMs: config.stallTimeoutMs ?? null,
104529
- firstStepStallTimeoutMs: config.firstStepStallTimeoutMs ?? null
104710
+ firstStepStallTimeoutMs: config.firstStepStallTimeoutMs ?? null,
104711
+ toolExecutionStallTimeoutMs: config.toolExecutionStallTimeoutMs ?? null
104530
104712
  },
104531
104713
  this.#buildInitialSnapshot(config)
104532
104714
  );
@@ -105427,6 +105609,7 @@ var Thread = class {
105427
105609
  this.#streamDelta.resetForTurn();
105428
105610
  if (this.#cursorRecovery.consumeStallResumeOnDeath()) return;
105429
105611
  if (this.#cursorRecovery.trySignalDeathRetry(event)) return;
105612
+ if (this.#cursorRecovery.tryCrashDeathRetry(event)) return;
105430
105613
  this.#manager.notifySubprocessDied({
105431
105614
  exitCode: event.exitCode ?? null,
105432
105615
  signal: event.signal ?? null
@@ -105451,7 +105634,7 @@ var Thread = class {
105451
105634
  this.#manager.notifyTurnComplete();
105452
105635
  break;
105453
105636
  case "sdk_error":
105454
- this.#recovery.handleSdkError(event.error);
105637
+ this.#recovery.handleSdkError(event.error, event.errorSubtype);
105455
105638
  break;
105456
105639
  case "subprocess_died":
105457
105640
  this.#handleSubprocessDiedEvent(event);
@@ -105568,6 +105751,12 @@ var Thread = class {
105568
105751
  /** Handled in #enqueueSubprocessEvent (off-queue heartbeat); unreachable here. */
105569
105752
  case "stream_activity":
105570
105753
  return;
105754
+ case "tool_execution_started":
105755
+ this.#manager.notifyToolExecution("started");
105756
+ return;
105757
+ case "tool_execution_settled":
105758
+ this.#manager.notifyToolExecution("settled");
105759
+ return;
105571
105760
  default: {
105572
105761
  const _exhaustive = event;
105573
105762
  throw new Error(`Unhandled subprocess event: ${JSON.stringify(_exhaustive)}`);
@@ -105582,11 +105771,14 @@ import { randomUUID as randomUUID21 } from "crypto";
105582
105771
  import { existsSync as existsSync10, readdirSync as readdirSync5, statSync as statSync4 } from "fs";
105583
105772
  import { mkdir as mkdir32, readFile as readFile45, writeFile as writeFile25 } from "fs/promises";
105584
105773
  import { homedir as homedir9 } from "os";
105585
- import { dirname as dirname33, join as join70 } from "path";
105774
+ import { dirname as dirname33, join as join71 } from "path";
105586
105775
 
105587
105776
  // src/services/plan/plan-detection-policy.ts
105588
105777
  function decidePlanDetection(trigger, snapshot) {
105589
105778
  if (snapshot.approvalReceived) return { type: "noop" };
105779
+ if (snapshot.priorPlanApproved && snapshot.triggerIsNewPlan) {
105780
+ return decideNewCycle(trigger, snapshot);
105781
+ }
105590
105782
  switch (trigger.kind) {
105591
105783
  /**
105592
105784
  * Both entry triggers share logic; they differ only in whether an
@@ -105604,6 +105796,20 @@ function decidePlanDetection(trigger, snapshot) {
105604
105796
  return assertNever(trigger);
105605
105797
  }
105606
105798
  }
105799
+ function decideNewCycle(trigger, snapshot) {
105800
+ switch (trigger.kind) {
105801
+ case "file_tracked":
105802
+ return snapshot.fileAvailable ? { type: "reset_and_detect" } : { type: "start_watch", failOnExhaust: false };
105803
+ case "exit_plan_mode":
105804
+ return snapshot.fileAvailable ? { type: "reset_and_detect" } : { type: "start_watch", failOnExhaust: true };
105805
+ case "watch_resolved":
105806
+ if (trigger.matched) return { type: "reset_and_detect" };
105807
+ if (trigger.failOnExhaust) return { type: "report_no_plan" };
105808
+ return { type: "noop" };
105809
+ default:
105810
+ return assertNever(trigger);
105811
+ }
105812
+ }
105607
105813
  function decideEntry(snapshot, failOnExhaust) {
105608
105814
  if (snapshot.hasDetection) return { type: "noop" };
105609
105815
  if (snapshot.fileAvailable) return { type: "detect" };
@@ -106209,8 +106415,8 @@ function greedyLCS(a, b2) {
106209
106415
  // src/services/plan/plan-file-watcher.ts
106210
106416
  import { existsSync as existsSync9 } from "fs";
106211
106417
  import { homedir as homedir8 } from "os";
106212
- import { join as join69 } from "path";
106213
- var DEFAULT_PLANS_DIR = join69(homedir8(), ".claude", "plans");
106418
+ import { join as join70 } from "path";
106419
+ var DEFAULT_PLANS_DIR = join70(homedir8(), ".claude", "plans");
106214
106420
  var PLAN_WATCH_TIMEOUT_MS = 1e4;
106215
106421
  var PLAN_WATCH_DEBOUNCE_MS = 250;
106216
106422
  function createPlanFileWatcher(deps) {
@@ -106337,9 +106543,7 @@ function createPlanFileWatcher(deps) {
106337
106543
  return { stop };
106338
106544
  }
106339
106545
 
106340
- // src/services/plan/plan-handler.ts
106341
- var PLAN_PUBLISHED_DENY_MESSAGE = `Your plan has been published to the Shipyard canvas for collaborative review. Reviewers can comment, edit, and approve your plan in real-time. You will receive feedback as <${XML_TAGS.COMMENT}> children of the <plan> resource and plan edits as <${XML_TAGS.PLAN_UPDATE}> diffs. Wait for <${XML_TAGS.PLAN_REVIEW} decision="approve"> before calling ExitPlanMode again.`;
106342
- var PLAN_STILL_UNDER_REVIEW_MESSAGE = `Your plan is still under review. Wait for <${XML_TAGS.PLAN_REVIEW} decision="approve"> before calling ExitPlanMode again.`;
106546
+ // src/services/plan/plan-handler-helpers.ts
106343
106547
  function parseAllowedPrompts(input) {
106344
106548
  if (input === null || input === void 0 || typeof input !== "object" || Array.isArray(input))
106345
106549
  return [];
@@ -106375,6 +106579,149 @@ function mergeCommentSources(crdtComments, browserComments) {
106375
106579
  }
106376
106580
  return result;
106377
106581
  }
106582
+
106583
+ // src/services/plan/plan-review-resolution.ts
106584
+ async function injectDecisionMessage(ctx, decision, feedback) {
106585
+ const reviewer = ctx.getHumanParticipantName();
106586
+ ctx.onPlanReviewEnded?.();
106587
+ if (decision === "approve") {
106588
+ ctx.setApprovalReceived(true);
106589
+ const feedbackNote = feedback ? ` Feedback: ${feedback}` : "";
106590
+ ctx.pushSyntheticMessageAndWake([
106591
+ {
106592
+ type: "text",
106593
+ text: `Plan approved by ${reviewer}.${feedbackNote} Continue with the implementation.`
106594
+ }
106595
+ ]);
106596
+ } else {
106597
+ ctx.setApprovalReceived(false);
106598
+ }
106599
+ ctx.setPlanReviewState({
106600
+ decision,
106601
+ reviewer,
106602
+ feedback: feedback ?? null,
106603
+ decidedAt: Date.now()
106604
+ });
106605
+ ctx.notifyPlanChange();
106606
+ ctx.appendMessage({
106607
+ channelId: ctx.channelId,
106608
+ messageId: `review-decision-${ctx.taskId}-${Date.now()}`,
106609
+ participantId: ctx.humanParticipantId,
106610
+ senderKind: "human",
106611
+ content: [{ type: "review_decision", decision, feedback, reviewer }],
106612
+ timestamp: Date.now(),
106613
+ isSynthetic: false
106614
+ }).catch((err3) => {
106615
+ ctx.log({
106616
+ event: "plan_decision_append_failed",
106617
+ taskId: ctx.taskId,
106618
+ decision,
106619
+ error: err3 instanceof Error ? err3.message : String(err3)
106620
+ });
106621
+ });
106622
+ broadcastPlanReview(ctx, decision, feedback, reviewer);
106623
+ }
106624
+ function broadcastPlanReview(ctx, decision, feedback, reviewer) {
106625
+ ctx.getSendControlMessage()?.({
106626
+ type: "plan_review",
106627
+ taskId: ctx.taskId,
106628
+ decision,
106629
+ feedback,
106630
+ reviewer
106631
+ });
106632
+ }
106633
+ async function gatherMergedComments(ctx, browserComments) {
106634
+ const allUndelivered = await ctx.annotationStore.getUndeliveredAnnotations(ctx.taskId);
106635
+ const storeComments = allUndelivered.filter(
106636
+ (a) => a.annotationType === "plan-text"
106637
+ ).map((a) => ({
106638
+ commentId: a.commentId,
106639
+ anchorText: a.anchorText,
106640
+ body: a.body,
106641
+ authorName: a.authorName
106642
+ }));
106643
+ return mergeCommentSources(storeComments, browserComments);
106644
+ }
106645
+ async function resolveLastAgentVersion(ctx) {
106646
+ const versions = await ctx.annotationStore.getPlanVersions(ctx.taskId);
106647
+ return findLastAgentVersion(versions);
106648
+ }
106649
+ async function preparePlanForResolution(ctx, toolUseId, browserComments) {
106650
+ const currentContent = ctx.planRepo.getContent(ctx.taskId);
106651
+ const mergedComments = await gatherMergedComments(ctx, browserComments);
106652
+ ctx.log({
106653
+ event: "plan_review_context",
106654
+ taskId: ctx.taskId,
106655
+ commentCount: mergedComments.length,
106656
+ browserCommentCount: browserComments.length,
106657
+ contentLength: currentContent.length
106658
+ });
106659
+ if (currentContent.length > 0) {
106660
+ const existingVersions = await ctx.annotationStore.getPlanVersions(ctx.taskId);
106661
+ const lastVersion = existingVersions[existingVersions.length - 1];
106662
+ const contentChanged = !lastVersion || lastVersion.markdown !== currentContent;
106663
+ if (contentChanged) {
106664
+ const humanVersion = {
106665
+ markdown: currentContent,
106666
+ timestamp: Date.now(),
106667
+ source: "human",
106668
+ toolUseId
106669
+ };
106670
+ await ctx.annotationStore.addPlanVersion(ctx.taskId, humanVersion);
106671
+ ctx.getSendControlMessage()?.({
106672
+ type: "annotation_version_added",
106673
+ taskId: ctx.taskId,
106674
+ version: humanVersion
106675
+ });
106676
+ }
106677
+ }
106678
+ if (mergedComments.length > 0) {
106679
+ await deliverPlanAnnotations(ctx, mergedComments);
106680
+ }
106681
+ }
106682
+ async function deliverPlanAnnotations(ctx, mergedComments) {
106683
+ try {
106684
+ const lastAgentVersion = await resolveLastAgentVersion(ctx);
106685
+ const currentContent = ctx.planRepo.getContent(ctx.taskId);
106686
+ const parts = [];
106687
+ if (lastAgentVersion?.markdown && lastAgentVersion.markdown !== currentContent) {
106688
+ const diff = computeUnifiedDiff(lastAgentVersion.markdown, currentContent);
106689
+ if (diff) {
106690
+ const participant = ctx.humanParticipantId;
106691
+ parts.push(
106692
+ `<${XML_TAGS.PLAN_UPDATE} participant="${escapeXmlAttr(participant)}">
106693
+ --- original (agent)
106694
+ +++ reviewed (${escapeXmlAttr(participant)})
106695
+ ${diff}
106696
+ </${XML_TAGS.PLAN_UPDATE}>`
106697
+ );
106698
+ }
106699
+ }
106700
+ if (parts.length > 0) {
106701
+ ctx.pushSyntheticMessageAndWake([{ type: "text", text: parts.join("\n\n") }]);
106702
+ }
106703
+ const deliveredIds = mergedComments.map((c) => c.commentId);
106704
+ await ctx.annotationStore.markDelivered(ctx.taskId, deliveredIds);
106705
+ ctx.notifyPlanChange();
106706
+ ctx.log({
106707
+ event: "plan_annotations_delivered",
106708
+ taskId: ctx.taskId,
106709
+ commentCount: mergedComments.length,
106710
+ hasPlanDiff: parts.length > 0
106711
+ });
106712
+ } catch (err3) {
106713
+ const msg = err3 instanceof Error ? err3.message : String(err3);
106714
+ ctx.log({
106715
+ event: "plan_annotations_delivery_failed",
106716
+ taskId: ctx.taskId,
106717
+ error: msg
106718
+ });
106719
+ }
106720
+ }
106721
+
106722
+ // src/services/plan/plan-handler.ts
106723
+ var PLAN_PUBLISHED_DENY_MESSAGE = `Your plan has been published to the Shipyard canvas for collaborative review. Reviewers can comment, edit, and approve your plan in real-time. You will receive feedback as <${XML_TAGS.COMMENT}> children of the <plan> resource and plan edits as <${XML_TAGS.PLAN_UPDATE}> diffs. Wait for <${XML_TAGS.PLAN_REVIEW} decision="approve"> before calling ExitPlanMode again.`;
106724
+ var PLAN_STILL_UNDER_REVIEW_MESSAGE = `Your plan is still under review. Wait for <${XML_TAGS.PLAN_REVIEW} decision="approve"> before calling ExitPlanMode again.`;
106378
106725
  var PlanHandler = class {
106379
106726
  #deps;
106380
106727
  #planFileBridge = null;
@@ -106659,7 +107006,7 @@ var PlanHandler = class {
106659
107006
  * Called when a Write tool result mentions a file in ~/.claude/plans/.
106660
107007
  */
106661
107008
  trackCreatedFile(filePath) {
106662
- const plansDir = join70(homedir9(), ".claude", "plans");
107009
+ const plansDir = join71(homedir9(), ".claude", "plans");
106663
107010
  if (filePath.startsWith(plansDir) && filePath.endsWith(".md")) {
106664
107011
  this.#createdPlanFiles.add(filePath);
106665
107012
  this.#deps.log({
@@ -106691,7 +107038,7 @@ var PlanHandler = class {
106691
107038
  }
106692
107039
  async #runHandleCodexPlanReady(content) {
106693
107040
  if (this.#deps.isDisposed()) return;
106694
- const filePath = join70(getShipyardHome(), "plans", `${this.#deps.taskId}.md`);
107041
+ const filePath = join71(getShipyardHome(), "plans", `${this.#deps.taskId}.md`);
106695
107042
  try {
106696
107043
  await mkdir32(dirname33(filePath), { recursive: true });
106697
107044
  await writeFile25(filePath, content, "utf-8");
@@ -106808,8 +107155,9 @@ var PlanHandler = class {
106808
107155
  decision
106809
107156
  });
106810
107157
  this.#deps.enqueueAsync(async () => {
107158
+ const ctx = this.#reviewResolutionContext();
106811
107159
  try {
106812
- await this.#preparePlanForResolution(toolUseId, opts?.comments ?? []);
107160
+ await preparePlanForResolution(ctx, toolUseId, opts?.comments ?? []);
106813
107161
  this.#applyPermissionMode(opts?.permissionMode);
106814
107162
  } catch (err3) {
106815
107163
  this.#deps.log({
@@ -106820,7 +107168,7 @@ var PlanHandler = class {
106820
107168
  error: err3 instanceof Error ? err3.message : String(err3)
106821
107169
  });
106822
107170
  }
106823
- await this.#injectDecisionMessage(decision, feedback);
107171
+ await injectDecisionMessage(ctx, decision, feedback);
106824
107172
  });
106825
107173
  }
106826
107174
  detectPlanEvents(content) {
@@ -106914,21 +107262,39 @@ var PlanHandler = class {
106914
107262
  }
106915
107263
  return this.#proactiveToolUseId;
106916
107264
  }
106917
- #detectionSnapshot(fileAvailable) {
107265
+ #detectionSnapshot(fileAvailable, toolUseId, matchPath) {
107266
+ const priorPlanApproved = this.#lastPlanDetection?.approved === true;
106918
107267
  return {
106919
107268
  approvalReceived: this.#approvalReceived,
106920
107269
  hasDetection: this.#lastPlanDetection !== null || this.#detectionInFlight,
106921
- fileAvailable
107270
+ fileAvailable,
107271
+ priorPlanApproved,
107272
+ /**
107273
+ * New-plan signal (#4498): a different plan than the one already detected —
107274
+ * an unprocessed toolUseId, or a tracked file differing from the bridge's
107275
+ * (Claude mints a fresh UUID + file per plan). Only meaningful once a prior
107276
+ * plan was approved, so it is gated on `priorPlanApproved`: on the FIRST
107277
+ * detection `#processedPlanToolUseIds` is empty (every id reads as "new"),
107278
+ * and conflating that with a new cycle would be a latent trap if a future
107279
+ * caller read this flag independently of `priorPlanApproved`.
107280
+ */
107281
+ triggerIsNewPlan: priorPlanApproved && (!this.#processedPlanToolUseIds.has(toolUseId) || matchPath !== null && matchPath !== this.#planFileBridge?.filePath)
106922
107282
  };
106923
107283
  }
106924
- /** Snapshot → policy → execute; #4075/#3488 logic is table-tested in `plan-detection-policy.test.ts`. */
107284
+ /** Snapshot → policy → execute; #4075/#3488/#4498 logic is table-tested in `plan-detection-policy.test.ts`. */
106925
107285
  #ensurePlanDetection(trigger, toolUseId) {
106926
107286
  const match = this.#findTaskPlanFile();
106927
- const action = decidePlanDetection(trigger, this.#detectionSnapshot(match !== null));
107287
+ const action = decidePlanDetection(
107288
+ trigger,
107289
+ this.#detectionSnapshot(match !== null, toolUseId, match)
107290
+ );
106928
107291
  switch (action.type) {
106929
107292
  case "detect":
106930
107293
  if (match) this.#handlePlanDetected(toolUseId, match);
106931
107294
  return;
107295
+ case "reset_and_detect":
107296
+ if (match) this.#resetForNewPlanCycle(toolUseId, match);
107297
+ return;
106932
107298
  case "start_watch":
106933
107299
  this.#startPlanWatch(toolUseId, action.failOnExhaust);
106934
107300
  return;
@@ -106943,6 +107309,35 @@ var PlanHandler = class {
106943
107309
  }
106944
107310
  }
106945
107311
  }
107312
+ /**
107313
+ * Begin a new plan cycle for a later phase in the SAME task (#4498). Without
107314
+ * this the new plan is silently dropped (hasDetection stays true) and the UI
107315
+ * keeps showing the stale approved plan. Narrow subset of `resetForRewind`
107316
+ * that preserves `#processedPlanToolUseIds`; a reused processed id gets a
107317
+ * fresh synthetic id so dedup does not skip the new plan.
107318
+ */
107319
+ #resetForNewPlanCycle(toolUseId, filePath) {
107320
+ const detectToolUseId = this.#processedPlanToolUseIds.has(toolUseId) ? `plan_new_cycle_${this.#deps.taskId}_${randomUUID21()}` : toolUseId;
107321
+ this.#deps.log({
107322
+ event: "plan_new_cycle_detected",
107323
+ taskId: this.#deps.taskId,
107324
+ toolUseId: detectToolUseId,
107325
+ filePath
107326
+ });
107327
+ this.#approvalReceived = false;
107328
+ this.#planPublished = false;
107329
+ this.#proactiveToolUseId = null;
107330
+ this.#stopPlanWatch();
107331
+ this.#detectionInFlight = false;
107332
+ const hadReviewState = this.#planReviewState !== null;
107333
+ this.#planReviewState = null;
107334
+ this.#deps.onPlanReviewEnded?.();
107335
+ if (hadReviewState) this.#deps.notifyPlanChange();
107336
+ this.#lastPlanDetection = null;
107337
+ this.#planFileBridge?.dispose();
107338
+ this.#planFileBridge = null;
107339
+ this.#handlePlanDetected(detectToolUseId, filePath);
107340
+ }
106946
107341
  /**
106947
107342
  * Subscribe to the plans directory via `@parcel/watcher`. `failOnExhaust`
106948
107343
  * decides whether a 10s exhaust surfaces an explicit no-plan state
@@ -106970,12 +107365,15 @@ var PlanHandler = class {
106970
107365
  #applyWatchResolution(toolUseId, matched, failOnExhaust, filePath) {
106971
107366
  const action = decidePlanDetection(
106972
107367
  { kind: "watch_resolved", matched, failOnExhaust },
106973
- this.#detectionSnapshot(filePath !== null)
107368
+ this.#detectionSnapshot(filePath !== null, toolUseId, filePath)
106974
107369
  );
106975
107370
  switch (action.type) {
106976
107371
  case "detect":
106977
107372
  if (filePath) this.#handlePlanDetected(toolUseId, filePath);
106978
107373
  return;
107374
+ case "reset_and_detect":
107375
+ if (filePath) this.#resetForNewPlanCycle(toolUseId, filePath);
107376
+ return;
106979
107377
  case "report_no_plan":
106980
107378
  this.#reportNoPlan(toolUseId);
106981
107379
  return;
@@ -107071,12 +107469,12 @@ var PlanHandler = class {
107071
107469
  }).filter((f2) => f2 !== null).sort((a, b2) => b2.mtime - a.mtime)[0];
107072
107470
  return newest?.path ?? null;
107073
107471
  }
107074
- const plansDir = join70(homedir9(), ".claude", "plans");
107472
+ const plansDir = join71(homedir9(), ".claude", "plans");
107075
107473
  if (!existsSync10(plansDir)) return null;
107076
107474
  try {
107077
107475
  const files = readdirSync5(plansDir).filter((f2) => f2.endsWith(".md")).map((f2) => ({
107078
- path: join70(plansDir, f2),
107079
- mtime: statSync4(join70(plansDir, f2)).mtimeMs
107476
+ path: join71(plansDir, f2),
107477
+ mtime: statSync4(join71(plansDir, f2)).mtimeMs
107080
107478
  })).sort((a, b2) => b2.mtime - a.mtime);
107081
107479
  const recent = files[0];
107082
107480
  if (recent && Date.now() - recent.mtime < 3e4) return recent.path;
@@ -107225,161 +107623,33 @@ var PlanHandler = class {
107225
107623
  if (!permissionMode) return;
107226
107624
  this.#deps.applyTaskSettings({ permissionMode });
107227
107625
  }
107228
- async #injectDecisionMessage(decision, feedback) {
107229
- const reviewer = this.#deps.getHumanParticipantName();
107230
- this.#deps.onPlanReviewEnded?.();
107231
- if (decision === "approve") {
107232
- this.#approvalReceived = true;
107233
- const feedbackNote = feedback ? ` Feedback: ${feedback}` : "";
107234
- this.#deps.pushSyntheticMessageAndWake([
107235
- {
107236
- type: "text",
107237
- text: `Plan approved by ${reviewer}.${feedbackNote} Continue with the implementation.`
107238
- }
107239
- ]);
107240
- } else {
107241
- this.#approvalReceived = false;
107242
- }
107243
- this.#planReviewState = {
107244
- decision,
107245
- reviewer,
107246
- feedback: feedback ?? null,
107247
- decidedAt: Date.now()
107248
- };
107249
- this.#deps.notifyPlanChange();
107250
- this.#deps.appendMessage({
107251
- channelId: this.#deps.channelId,
107252
- messageId: `review-decision-${this.#deps.taskId}-${Date.now()}`,
107253
- participantId: this.#deps.humanParticipantId,
107254
- senderKind: "human",
107255
- content: [{ type: "review_decision", decision, feedback, reviewer }],
107256
- timestamp: Date.now(),
107257
- isSynthetic: false
107258
- }).catch((err3) => {
107259
- this.#deps.log({
107260
- event: "plan_decision_append_failed",
107261
- taskId: this.#deps.taskId,
107262
- decision,
107263
- error: err3 instanceof Error ? err3.message : String(err3)
107264
- });
107265
- });
107266
- this.#broadcastPlanReview(decision, feedback, reviewer);
107267
- }
107268
- #broadcastPlanReview(decision, feedback, reviewer) {
107269
- this.#deps.getSendControlMessage()?.({
107270
- type: "plan_review",
107271
- taskId: this.#deps.taskId,
107272
- decision,
107273
- feedback,
107274
- reviewer
107275
- });
107276
- }
107277
107626
  /**
107278
- * Gather undelivered comments from file-backed store and browser,
107279
- * merging both sources and deduplicating by commentId.
107627
+ * Build the narrow Context for the review-resolution free functions. State
107628
+ * (`#approvalReceived`, `#planReviewState`) is read broadly across this class,
107629
+ * so it stays here and is mutated through narrow setters — see
107630
+ * `plan-review-resolution.ts`.
107280
107631
  */
107281
- async #gatherMergedComments(browserComments) {
107282
- const allUndelivered = await this.#deps.annotationStore.getUndeliveredAnnotations(
107283
- this.#deps.taskId
107284
- );
107285
- const storeComments = allUndelivered.filter(
107286
- (a) => a.annotationType === "plan-text"
107287
- ).map((a) => ({
107288
- commentId: a.commentId,
107289
- anchorText: a.anchorText,
107290
- body: a.body,
107291
- authorName: a.authorName
107292
- }));
107293
- return mergeCommentSources(storeComments, browserComments);
107294
- }
107295
- /** Resolve the best agent version from file-backed store. */
107296
- async #resolveLastAgentVersion() {
107297
- const versions = await this.#deps.annotationStore.getPlanVersions(this.#deps.taskId);
107298
- return findLastAgentVersion(versions);
107299
- }
107300
- async #preparePlanForResolution(toolUseId, browserComments) {
107301
- const currentContent = this.#deps.planRepo.getContent(this.#deps.taskId);
107302
- const mergedComments = await this.#gatherMergedComments(browserComments);
107303
- this.#deps.log({
107304
- event: "plan_review_context",
107632
+ #reviewResolutionContext() {
107633
+ return {
107305
107634
  taskId: this.#deps.taskId,
107306
- commentCount: mergedComments.length,
107307
- browserCommentCount: browserComments.length,
107308
- contentLength: currentContent.length
107309
- });
107310
- if (currentContent.length > 0) {
107311
- const existingVersions = await this.#deps.annotationStore.getPlanVersions(this.#deps.taskId);
107312
- const lastVersion = existingVersions[existingVersions.length - 1];
107313
- const contentChanged = !lastVersion || lastVersion.markdown !== currentContent;
107314
- if (contentChanged) {
107315
- const humanVersion = {
107316
- markdown: currentContent,
107317
- timestamp: Date.now(),
107318
- source: "human",
107319
- toolUseId
107320
- };
107321
- await this.#deps.annotationStore.addPlanVersion(this.#deps.taskId, humanVersion);
107322
- this.#deps.getSendControlMessage()?.({
107323
- type: "annotation_version_added",
107324
- taskId: this.#deps.taskId,
107325
- version: humanVersion
107326
- });
107327
- }
107328
- }
107329
- if (mergedComments.length > 0) {
107330
- await this.#deliverPlanAnnotations(mergedComments);
107331
- }
107332
- }
107333
- /**
107334
- * Deliver review comments and plan edit diff. Plan-anchored comments are
107335
- * no longer pushed as a top-level `<review-comment surface="plan">`
107336
- * synthetic — the `<plan>` resource projection now embeds them inline as
107337
- * `<comment>` children. Instead we mark the comments delivered and notify
107338
- * the resource resolver so it re-projects.
107339
- *
107340
- * The `<plan-update>` patch (showing human edits over the agent's
107341
- * baseline) is still emitted synchronously here so the agent sees the
107342
- * diff in the same turn the decision lands. Resource-level lineage
107343
- * patches will continue to surface ongoing CRDT mutations independently.
107344
- */
107345
- async #deliverPlanAnnotations(mergedComments) {
107346
- try {
107347
- const lastAgentVersion = await this.#resolveLastAgentVersion();
107348
- const currentContent = this.#deps.planRepo.getContent(this.#deps.taskId);
107349
- const parts = [];
107350
- if (lastAgentVersion?.markdown && lastAgentVersion.markdown !== currentContent) {
107351
- const diff = computeUnifiedDiff(lastAgentVersion.markdown, currentContent);
107352
- if (diff) {
107353
- const participant = this.#deps.humanParticipantId;
107354
- parts.push(
107355
- `<${XML_TAGS.PLAN_UPDATE} participant="${escapeXmlAttr(participant)}">
107356
- --- original (agent)
107357
- +++ reviewed (${escapeXmlAttr(participant)})
107358
- ${diff}
107359
- </${XML_TAGS.PLAN_UPDATE}>`
107360
- );
107361
- }
107362
- }
107363
- if (parts.length > 0) {
107364
- this.#deps.pushSyntheticMessageAndWake([{ type: "text", text: parts.join("\n\n") }]);
107635
+ channelId: this.#deps.channelId,
107636
+ humanParticipantId: this.#deps.humanParticipantId,
107637
+ planRepo: this.#deps.planRepo,
107638
+ annotationStore: this.#deps.annotationStore,
107639
+ log: this.#deps.log,
107640
+ getSendControlMessage: this.#deps.getSendControlMessage,
107641
+ pushSyntheticMessageAndWake: this.#deps.pushSyntheticMessageAndWake,
107642
+ appendMessage: this.#deps.appendMessage,
107643
+ getHumanParticipantName: this.#deps.getHumanParticipantName,
107644
+ notifyPlanChange: this.#deps.notifyPlanChange,
107645
+ onPlanReviewEnded: this.#deps.onPlanReviewEnded,
107646
+ setApprovalReceived: (value) => {
107647
+ this.#approvalReceived = value;
107648
+ },
107649
+ setPlanReviewState: (state) => {
107650
+ this.#planReviewState = state;
107365
107651
  }
107366
- const deliveredIds = mergedComments.map((c) => c.commentId);
107367
- await this.#deps.annotationStore.markDelivered(this.#deps.taskId, deliveredIds);
107368
- this.#deps.notifyPlanChange();
107369
- this.#deps.log({
107370
- event: "plan_annotations_delivered",
107371
- taskId: this.#deps.taskId,
107372
- commentCount: mergedComments.length,
107373
- hasPlanDiff: parts.length > 0
107374
- });
107375
- } catch (err3) {
107376
- const msg = err3 instanceof Error ? err3.message : String(err3);
107377
- this.#deps.log({
107378
- event: "plan_annotations_delivery_failed",
107379
- taskId: this.#deps.taskId,
107380
- error: msg
107381
- });
107382
- }
107652
+ };
107383
107653
  }
107384
107654
  };
107385
107655
 
@@ -107716,7 +107986,7 @@ function tryBeginBridge(ctx) {
107716
107986
  function endBridge(ctx) {
107717
107987
  ctx.setBridgeInFlight(false);
107718
107988
  }
107719
- async function compactSelfForHandoff(ctx, timeoutMs = COMPACT_TIMEOUT_MS) {
107989
+ async function compactSelfForHandoff(ctx, startSilenceMs = COMPACTION_SILENCE_TIMEOUT_MS) {
107720
107990
  const triggered = await ctx.forceCompact();
107721
107991
  if (!triggered) {
107722
107992
  ctx.log({
@@ -107725,14 +107995,23 @@ async function compactSelfForHandoff(ctx, timeoutMs = COMPACT_TIMEOUT_MS) {
107725
107995
  });
107726
107996
  return false;
107727
107997
  }
107728
- const deadline = Date.now() + timeoutMs;
107998
+ const startDeadline = Date.now() + startSilenceMs;
107729
107999
  const pollIntervalMs = 250;
107730
108000
  let leftIdleOnce = false;
107731
- while (Date.now() < deadline) {
108001
+ while (true) {
107732
108002
  await new Promise((resolve10) => setTimeout(resolve10, pollIntervalMs));
107733
108003
  const kind = ctx.getCompactionStateKind();
107734
108004
  if (!leftIdleOnce) {
107735
- if (kind !== "idle") leftIdleOnce = true;
108005
+ if (kind !== "idle") {
108006
+ leftIdleOnce = true;
108007
+ } else if (Date.now() >= startDeadline) {
108008
+ ctx.log({
108009
+ event: "compact_self_for_handoff_start_silence_timeout",
108010
+ taskId: ctx.taskId,
108011
+ startSilenceMs
108012
+ });
108013
+ return false;
108014
+ }
107736
108015
  continue;
107737
108016
  }
107738
108017
  if (kind === "idle") return true;
@@ -107741,13 +108020,6 @@ async function compactSelfForHandoff(ctx, timeoutMs = COMPACT_TIMEOUT_MS) {
107741
108020
  return false;
107742
108021
  }
107743
108022
  }
107744
- ctx.log({
107745
- event: "compact_self_for_handoff_timeout",
107746
- taskId: ctx.taskId,
107747
- timeoutMs,
107748
- leftIdleOnce
107749
- });
107750
- return false;
107751
108023
  }
107752
108024
  function consumePendingNeutralHandoff(ctx, destProvider) {
107753
108025
  const pending = ctx.getPendingNeutralHandoff();
@@ -109417,7 +109689,7 @@ var RewindCheckpointHandler = class {
109417
109689
 
109418
109690
  // src/services/task/side-thread-registry.ts
109419
109691
  import { mkdir as mkdir33, readFile as readFile46, rename as rename20, writeFile as writeFile26 } from "fs/promises";
109420
- import { dirname as dirname34, join as join71 } from "path";
109692
+ import { dirname as dirname34, join as join72 } from "path";
109421
109693
  var ThreadFileSchema = external_exports.object({
109422
109694
  threads: external_exports.record(external_exports.string(), ThreadMetadataSchema)
109423
109695
  });
@@ -109793,7 +110065,7 @@ var SideThreadRegistry = class {
109793
110065
  }
109794
110066
  /** Persistence */
109795
110067
  #filePath() {
109796
- return join71(this.#deps.dataDir, "threads", `${this.#deps.taskId}.json`);
110068
+ return join72(this.#deps.dataDir, "threads", `${this.#deps.taskId}.json`);
109797
110069
  }
109798
110070
  async #ensureLoaded() {
109799
110071
  if (this.#loadedFromDisk.has(this.#deps.taskId)) return;
@@ -110046,15 +110318,15 @@ function buildHandlers(deps) {
110046
110318
  deps.dispatchToSelf({ kind: "flash_timer_elapsed" });
110047
110319
  });
110048
110320
  } else {
110049
- deps.setCompactTimeoutTimer(durationMs, () => {
110050
- deps.dispatchToSelf({ kind: "compact_timeout", now: Date.now() });
110321
+ deps.setCompactSilenceTimer(durationMs, () => {
110322
+ deps.dispatchToSelf({ kind: "compact_silence_timeout", now: Date.now() });
110051
110323
  });
110052
110324
  }
110053
110325
  },
110054
110326
  clearTimer: (timerKind) => {
110055
110327
  if (timerKind === "pre-warn") deps.clearPreWarnTimer();
110056
110328
  else if (timerKind === "flash") deps.clearFlashTimer();
110057
- else deps.clearCompactTimeoutTimer();
110329
+ else deps.clearCompactSilenceTimer();
110058
110330
  },
110059
110331
  callProfileForceCompact: (instructions) => {
110060
110332
  const opts = instructions !== void 0 ? { instructions } : void 0;
@@ -110096,23 +110368,10 @@ function enrichTelemetryPayload(event, deps) {
110096
110368
  switch (event.kind) {
110097
110369
  case "compaction_started":
110098
110370
  return base2;
110099
- case "compaction_completed": {
110100
- const { priorMessageCount, postMessageCount } = deps.getMessageCounts();
110101
- const enriched = { ...base2, durationMs: event.durationMs };
110102
- if (event.preTokens !== void 0) enriched.preTokens = event.preTokens;
110103
- if (event.postTokens !== void 0) enriched.postTokens = event.postTokens;
110104
- if (event.tokensReclaimed !== void 0) enriched.tokensReclaimed = event.tokensReclaimed;
110105
- if (priorMessageCount !== void 0) enriched.priorMessageCount = priorMessageCount;
110106
- if (postMessageCount !== void 0) enriched.postMessageCount = postMessageCount;
110107
- return enriched;
110108
- }
110371
+ case "compaction_completed":
110372
+ return enrichCompletedPayload(base2, event, deps);
110109
110373
  case "compaction_failed":
110110
- return {
110111
- ...base2,
110112
- durationMs: event.durationMs,
110113
- reason: event.reason,
110114
- errorMessage: truncateError(event.error)
110115
- };
110374
+ return enrichFailedPayload(base2, event);
110116
110375
  case "compaction_subprocess_died":
110117
110376
  return {
110118
110377
  ...base2,
@@ -110123,6 +110382,26 @@ function enrichTelemetryPayload(event, deps) {
110123
110382
  return assertNever7(event);
110124
110383
  }
110125
110384
  }
110385
+ function enrichCompletedPayload(base2, event, deps) {
110386
+ const { priorMessageCount, postMessageCount } = deps.getMessageCounts();
110387
+ const enriched = { ...base2, durationMs: event.durationMs };
110388
+ if (event.preTokens !== void 0) enriched.preTokens = event.preTokens;
110389
+ if (event.postTokens !== void 0) enriched.postTokens = event.postTokens;
110390
+ if (event.tokensReclaimed !== void 0) enriched.tokensReclaimed = event.tokensReclaimed;
110391
+ if (priorMessageCount !== void 0) enriched.priorMessageCount = priorMessageCount;
110392
+ if (postMessageCount !== void 0) enriched.postMessageCount = postMessageCount;
110393
+ return enriched;
110394
+ }
110395
+ function enrichFailedPayload(base2, event) {
110396
+ const enriched = {
110397
+ ...base2,
110398
+ durationMs: event.durationMs,
110399
+ reason: event.reason,
110400
+ errorMessage: truncateError(event.error)
110401
+ };
110402
+ if (event.silenceMs !== void 0) enriched.silenceMs = event.silenceMs;
110403
+ return enriched;
110404
+ }
110126
110405
  function mapSmStatusToWire(status) {
110127
110406
  switch (status) {
110128
110407
  case "pre-warn":
@@ -110241,7 +110520,7 @@ async function buildCompactionSnapshot(deps, instructions) {
110241
110520
  };
110242
110521
  }
110243
110522
  function buildCompactionShellHandlers(ctx) {
110244
- let compactTimeoutHandle = null;
110523
+ let compactSilenceHandle = null;
110245
110524
  const shellDeps = {
110246
110525
  /** Late-bound: every effect invocation reads the current subprocess. */
110247
110526
  get subprocess() {
@@ -110317,19 +110596,19 @@ function buildCompactionShellHandlers(ctx) {
110317
110596
  },
110318
110597
  clearFlashTimer: () => {
110319
110598
  },
110320
- setCompactTimeoutTimer: (durationMs, onElapsed) => {
110321
- if (compactTimeoutHandle !== null) {
110322
- clearTimeout(compactTimeoutHandle);
110599
+ setCompactSilenceTimer: (durationMs, onElapsed) => {
110600
+ if (compactSilenceHandle !== null) {
110601
+ clearTimeout(compactSilenceHandle);
110323
110602
  }
110324
- compactTimeoutHandle = setTimeout(() => {
110325
- compactTimeoutHandle = null;
110603
+ compactSilenceHandle = setTimeout(() => {
110604
+ compactSilenceHandle = null;
110326
110605
  onElapsed();
110327
110606
  }, durationMs);
110328
110607
  },
110329
- clearCompactTimeoutTimer: () => {
110330
- if (compactTimeoutHandle !== null) {
110331
- clearTimeout(compactTimeoutHandle);
110332
- compactTimeoutHandle = null;
110608
+ clearCompactSilenceTimer: () => {
110609
+ if (compactSilenceHandle !== null) {
110610
+ clearTimeout(compactSilenceHandle);
110611
+ compactSilenceHandle = null;
110333
110612
  }
110334
110613
  },
110335
110614
  dispatchToSelf: (event) => {
@@ -110353,8 +110632,7 @@ function buildCompactionShellHandlers(ctx) {
110353
110632
  },
110354
110633
  /**
110355
110634
  * Live-read every dispatch so a cross-runtime switch between dispatches
110356
- * picks up the destination's capability. The shell uses this to gate
110357
- * arming the compact-timeout watchdog.
110635
+ * picks up the destination's capability.
110358
110636
  */
110359
110637
  get runtimeFailureReporting() {
110360
110638
  return ctx.getRuntimeFailureReporting();
@@ -110545,6 +110823,7 @@ function decideByMessageCount(params) {
110545
110823
  return { kind: "noop" };
110546
110824
  }
110547
110825
  function handleMainThreadEvent(ctx, event) {
110826
+ maybeDispatchCompactionActivity(ctx, event);
110548
110827
  switch (event.type) {
110549
110828
  case "init_received":
110550
110829
  ctx.onInitReceived(event.metadata);
@@ -110675,12 +110954,60 @@ function handleMainThreadEvent(ctx, event) {
110675
110954
  break;
110676
110955
  case "stream_activity":
110677
110956
  break;
110957
+ case "tool_execution_started":
110958
+ case "tool_execution_settled":
110959
+ break;
110678
110960
  default: {
110679
110961
  const _exhaustive = event;
110680
110962
  throw new Error(`Unhandled subprocess event: ${JSON.stringify(_exhaustive)}`);
110681
110963
  }
110682
110964
  }
110683
110965
  }
110966
+ function maybeDispatchCompactionActivity(ctx, event) {
110967
+ if (ctx.getCompactionStateKind() !== "compacting") return;
110968
+ if (!isCompactionActivityEvent(event)) return;
110969
+ ctx.dispatchCompactionEvent({ kind: "compact_activity", now: Date.now() });
110970
+ }
110971
+ function isCompactionActivityEvent(event) {
110972
+ switch (event.type) {
110973
+ case "assistant_message":
110974
+ case "background_agent_completed":
110975
+ case "background_agent_result":
110976
+ case "background_agent_started":
110977
+ case "codex_token_snapshot":
110978
+ case "cumulative_usage_snapshot":
110979
+ case "informational_notice":
110980
+ case "memory_recall":
110981
+ case "mcp_mid_thread_reload_needed":
110982
+ case "plan_content_ready":
110983
+ case "step_progress":
110984
+ case "stream_activity":
110985
+ case "stream_delta":
110986
+ case "subagent_progress":
110987
+ case "tool_execution_settled":
110988
+ case "tool_execution_started":
110989
+ case "tool_result_echo":
110990
+ case "tool_use_started":
110991
+ case "tool_use_summary":
110992
+ case "turn_complete":
110993
+ case "user_message_echo":
110994
+ case "api_usage_snapshot":
110995
+ return true;
110996
+ case "auth_not_logged_in":
110997
+ case "billing_error":
110998
+ case "compaction_completed":
110999
+ case "compaction_failed":
111000
+ case "compaction_started":
111001
+ case "init_received":
111002
+ case "rate_limit":
111003
+ case "rate_limit_error":
111004
+ case "sdk_error":
111005
+ case "subprocess_died":
111006
+ return false;
111007
+ default:
111008
+ return assertNever(event);
111009
+ }
111010
+ }
110684
111011
  function handleAssistantMessage(ctx, event) {
110685
111012
  ctx.log({
110686
111013
  event: "debug_assistant_message",
@@ -110804,6 +111131,9 @@ function handleTurnComplete(ctx, event) {
110804
111131
  });
110805
111132
  }
110806
111133
  }
111134
+ function isCodexMissingBearer401(runtimeId, error) {
111135
+ return runtimeId === "codex" && /missing\s+bearer/i.test(error) && error.includes("401");
111136
+ }
110807
111137
  function handleSdkError(ctx, event) {
110808
111138
  ctx.collabQueueTurnComplete();
110809
111139
  ctx.pushManagerTurnEnd();
@@ -110820,6 +111150,13 @@ function handleSdkError(ctx, event) {
110820
111150
  now: Date.now(),
110821
111151
  error: event.error
110822
111152
  });
111153
+ if (isCodexMissingBearer401(ctx.getRuntimeId(), event.error)) {
111154
+ ctx.onAuthNotLoggedIn(ctx.getRuntimeId());
111155
+ telemeter("codex_compact_auth_failure", {
111156
+ taskId: ctx.taskId,
111157
+ runtimeId: ctx.getRuntimeId()
111158
+ });
111159
+ }
110823
111160
  }
110824
111161
  }
110825
111162
  function handleBillingOrRateLimitError(ctx, kind) {
@@ -111709,7 +112046,9 @@ var CompactionController = class {
111709
112046
  this.#dispatchCompactionEventAndPersist(event);
111710
112047
  }
111711
112048
  #dispatchCompactionEventAndPersist(event) {
112049
+ const previousState = this.#compactionState;
111712
112050
  const result = transition(this.#compactionState, event);
112051
+ if (result.newState === previousState && result.effects.length === 0) return;
111713
112052
  this.#compactionState = result.newState;
111714
112053
  this.#deps.persistCompactionRuntimeState(
111715
112054
  result.newState.kind === "idle" ? null : result.newState
@@ -112628,7 +112967,7 @@ async function resolveTaskListPrepend(ctx) {
112628
112967
  // src/services/task/cc-task-file-store.ts
112629
112968
  import { readdir as readdir20, readFile as readFile47 } from "fs/promises";
112630
112969
  import { homedir as homedir10 } from "os";
112631
- import { basename as basename12, dirname as dirname35, join as join72 } from "path";
112970
+ import { basename as basename12, dirname as dirname35, join as join73 } from "path";
112632
112971
  var VALID_STATUSES = /* @__PURE__ */ new Set(["pending", "in_progress", "completed"]);
112633
112972
  var IDLE_DETACH_MS = 5 * 60 * 1e3;
112634
112973
  var IDLE_SWEEP_INTERVAL_MS = 6e4;
@@ -112644,7 +112983,7 @@ function isCCTaskFile(value) {
112644
112983
  }
112645
112984
  function createCCTaskFileWatcher(listId, log) {
112646
112985
  const sanitizedId = sanitize(listId);
112647
- const dir = join72(homedir10(), ".claude", "tasks", sanitizedId);
112986
+ const dir = join73(homedir10(), ".claude", "tasks", sanitizedId);
112648
112987
  const targetDirName = basename12(dir);
112649
112988
  const parentDir = dirname35(dir);
112650
112989
  const watchState = {
@@ -112680,7 +113019,7 @@ function createCCTaskFileWatcher(listId, log) {
112680
113019
  return tasks;
112681
113020
  }
112682
113021
  async function readTask(taskId) {
112683
- const filePath = join72(dir, `${taskId}.json`);
113022
+ const filePath = join73(dir, `${taskId}.json`);
112684
113023
  try {
112685
113024
  const raw = await readFile47(filePath, "utf-8");
112686
113025
  const parsed = JSON.parse(raw);
@@ -112747,7 +113086,7 @@ function createCCTaskFileWatcher(listId, log) {
112747
113086
  watchState.parentSub = null;
112748
113087
  return;
112749
113088
  }
112750
- if (ev.path.endsWith(targetDirName) || ev.path === join72(parentDir, targetDirName)) {
113089
+ if (ev.path.endsWith(targetDirName) || ev.path === join73(parentDir, targetDirName)) {
112751
113090
  void ensureDirWatcher();
112752
113091
  break;
112753
113092
  }
@@ -112863,7 +113202,7 @@ function createCCTaskFileWatcher(listId, log) {
112863
113202
  // src/services/task/cc-task-file-writer.ts
112864
113203
  import { createHash as createHash10 } from "crypto";
112865
113204
  import { mkdir as mkdir34, readdir as readdir21, rename as rename21, unlink as unlink15, writeFile as writeFile27 } from "fs/promises";
112866
- import { join as join73 } from "path";
113205
+ import { join as join74 } from "path";
112867
113206
  function contentHash2(data) {
112868
113207
  return createHash10("sha256").update(data).digest("hex").slice(0, 16);
112869
113208
  }
@@ -112879,7 +113218,7 @@ async function writeTasks(dir, tasks, hashes) {
112879
113218
  const json = JSON.stringify(structuredTaskToCCFile(task), null, 2);
112880
113219
  const hash = contentHash2(json);
112881
113220
  if (hashes.get(id) === hash) continue;
112882
- await atomicWriteFile2(join73(dir, `${id}.json`), json);
113221
+ await atomicWriteFile2(join74(dir, `${id}.json`), json);
112883
113222
  hashes.set(id, hash);
112884
113223
  }
112885
113224
  return targetIds;
@@ -112895,7 +113234,7 @@ async function removeStaleFiles(dir, targetIds, hashes) {
112895
113234
  if (!entry.endsWith(".json")) continue;
112896
113235
  const id = entry.slice(0, -".json".length);
112897
113236
  if (!targetIds.has(id) && hashes.has(id)) {
112898
- await unlink15(join73(dir, entry)).catch(() => {
113237
+ await unlink15(join74(dir, entry)).catch(() => {
112899
113238
  });
112900
113239
  hashes.delete(id);
112901
113240
  }
@@ -113497,10 +113836,10 @@ var StructuredTaskTracker = class {
113497
113836
  import { unwatchFile, watchFile } from "fs";
113498
113837
  import { open as open2 } from "fs/promises";
113499
113838
  import { homedir as homedir11 } from "os";
113500
- import { join as join74 } from "path";
113839
+ import { join as join75 } from "path";
113501
113840
  function computeTranscriptPath(cwd, sessionId, taskId) {
113502
113841
  const cwdSlug = cwd.replace(/[^a-zA-Z0-9]/g, "-").slice(0, 200);
113503
- return join74(
113842
+ return join75(
113504
113843
  homedir11(),
113505
113844
  ".claude",
113506
113845
  "projects",
@@ -114210,14 +114549,59 @@ function trackPlanFileCreation(content, onPlanFile) {
114210
114549
  }
114211
114550
 
114212
114551
  // src/services/token-counter.ts
114213
- import { execFile as execFile11 } from "child_process";
114552
+ import { spawn as spawn13 } from "child_process";
114214
114553
  import { createHash as createHash11 } from "crypto";
114215
114554
  import { unlink as unlink16, writeFile as writeFile28 } from "fs/promises";
114216
114555
  import { createRequire as createRequire4 } from "module";
114217
114556
  import { tmpdir } from "os";
114218
- import { join as join75 } from "path";
114219
- import { promisify as promisify10 } from "util";
114220
- var execFileAsync4 = promisify10(execFile11);
114557
+ import { join as join76 } from "path";
114558
+ function spawnCollect(bin, args, opts) {
114559
+ return new Promise((resolve10, reject) => {
114560
+ const child = spawn13(bin, args, {
114561
+ cwd: opts.cwd,
114562
+ stdio: ["ignore", "pipe", "pipe"],
114563
+ env: opts.env
114564
+ });
114565
+ let stdout = "";
114566
+ let stderr = "";
114567
+ let settled = false;
114568
+ child.stdout?.on("data", (d) => {
114569
+ stdout += d.toString("utf-8");
114570
+ });
114571
+ child.stderr?.on("data", (d) => {
114572
+ stderr += d.toString("utf-8");
114573
+ });
114574
+ const timer = setTimeout(() => {
114575
+ if (settled) return;
114576
+ settled = true;
114577
+ child.kill("SIGTERM");
114578
+ reject(
114579
+ new Error(`Token count timed out after ${opts.timeout}ms. stderr: ${stderr.slice(0, 200)}`)
114580
+ );
114581
+ }, opts.timeout);
114582
+ child.on("close", (code) => {
114583
+ clearTimeout(timer);
114584
+ if (settled) return;
114585
+ settled = true;
114586
+ if (code === 0) {
114587
+ resolve10(stdout);
114588
+ } else {
114589
+ reject(
114590
+ new Error(
114591
+ `Command exited ${code ?? "null"}: ${bin} ${args.slice(0, 3).join(" ")}... stderr: ${stderr.trim().slice(0, 300)}`
114592
+ )
114593
+ );
114594
+ }
114595
+ });
114596
+ child.on("error", (err3) => {
114597
+ clearTimeout(timer);
114598
+ if (settled) return;
114599
+ settled = true;
114600
+ reject(err3);
114601
+ });
114602
+ });
114603
+ }
114604
+ var _spawnImpl = null;
114221
114605
  function resolveDaemonNodeModules(log) {
114222
114606
  try {
114223
114607
  const thisDir = new URL(".", import.meta.url);
@@ -114228,7 +114612,7 @@ function resolveDaemonNodeModules(log) {
114228
114612
  } catch {
114229
114613
  }
114230
114614
  try {
114231
- const cwdRequire = createRequire4(join75(process.cwd(), "package.json"));
114615
+ const cwdRequire = createRequire4(join76(process.cwd(), "package.json"));
114232
114616
  const sdkPath = cwdRequire.resolve("@modelcontextprotocol/sdk/server/mcp.js");
114233
114617
  const idx = sdkPath.lastIndexOf("node_modules");
114234
114618
  if (idx >= 0) return sdkPath.slice(0, idx + "node_modules".length);
@@ -114238,7 +114622,7 @@ function resolveDaemonNodeModules(log) {
114238
114622
  error: err3 instanceof Error ? err3.message : String(err3)
114239
114623
  });
114240
114624
  }
114241
- return join75(process.cwd(), "node_modules");
114625
+ return join76(process.cwd(), "node_modules");
114242
114626
  }
114243
114627
  function planTokenCount(systemPrompt, metadata, cwd, model, mcpServers) {
114244
114628
  return {
@@ -114263,6 +114647,8 @@ function hashPlan(plan) {
114263
114647
  }
114264
114648
  var MAX_CACHE_SIZE = 50;
114265
114649
  var CACHE_TTL_MS = 15 * 60 * 1e3;
114650
+ var FAILURE_BACKOFF_MS = 5 * 60 * 1e3;
114651
+ var failureBackoffUntilMs = 0;
114266
114652
  var activeCount = 0;
114267
114653
  var cache = /* @__PURE__ */ new Map();
114268
114654
  function evictOldest() {
@@ -114435,7 +114821,7 @@ async function serializeMcpConfig(servers, allToolNames, daemonNodeModules, rese
114435
114821
  if (isReservedMcpServerName(serverName, reservedServerNames)) continue;
114436
114822
  const script = buildStubServerScript(serverName, tools);
114437
114823
  if (!script) continue;
114438
- const stubPath = join75(tmpdir(), `shipyard-mcp-stub-${serverName}-${Date.now()}.cjs`);
114824
+ const stubPath = join76(tmpdir(), `shipyard-mcp-stub-${serverName}-${Date.now()}.cjs`);
114439
114825
  await writeFile28(stubPath, script, "utf-8");
114440
114826
  stubPaths.push(stubPath);
114441
114827
  mcpServers[serverName] = {
@@ -114461,11 +114847,37 @@ async function prepareTokenCountArgs(plan, log, reservedServerNames = SDK_RESERV
114461
114847
  reservedServerNames
114462
114848
  );
114463
114849
  if (!serialized.config) return { args, mcpConfigPath: null, stubPaths: serialized.stubPaths };
114464
- const mcpConfigPath = join75(tmpdir(), `shipyard-mcp-${Date.now()}.json`);
114850
+ const mcpConfigPath = join76(tmpdir(), `shipyard-mcp-${Date.now()}.json`);
114465
114851
  await writeFile28(mcpConfigPath, JSON.stringify(serialized.config), "utf-8");
114466
114852
  args.push("--mcp-config", mcpConfigPath);
114467
114853
  return { args, mcpConfigPath, stubPaths: serialized.stubPaths };
114468
114854
  }
114855
+ async function runWithRetry(bin, plan, log, cleanupPaths) {
114856
+ const invokeSpawn = _spawnImpl ?? spawnCollect;
114857
+ const runAttempt = async (reservedServerNames) => {
114858
+ const attemptPrepared = await prepareTokenCountArgs(plan, log, reservedServerNames);
114859
+ cleanupPaths.push(...attemptPrepared.stubPaths);
114860
+ if (attemptPrepared.mcpConfigPath) cleanupPaths.push(attemptPrepared.mcpConfigPath);
114861
+ const stdout = await invokeSpawn(bin, attemptPrepared.args, {
114862
+ cwd: plan.cwd,
114863
+ timeout: 6e4,
114864
+ env: process.env
114865
+ });
114866
+ return { stdout, stubCount: attemptPrepared.stubPaths.length };
114867
+ };
114868
+ try {
114869
+ return await runAttempt(SDK_RESERVED_MCP_SERVER_NAMES);
114870
+ } catch (err3) {
114871
+ const reservedServerName = parseReservedMcpServerName(
114872
+ err3 instanceof Error ? err3.message : String(err3)
114873
+ );
114874
+ if (!reservedServerName) throw err3;
114875
+ log({ event: "token_count_reserved_mcp_retry", serverName: reservedServerName });
114876
+ const retryNames = new Set(SDK_RESERVED_MCP_SERVER_NAMES);
114877
+ retryNames.add(reservedServerName);
114878
+ return await runAttempt(retryNames);
114879
+ }
114880
+ }
114469
114881
  async function executeTokenCount(plan, log) {
114470
114882
  const key = hashPlan(plan);
114471
114883
  const cached = getCached(key);
@@ -114473,51 +114885,39 @@ async function executeTokenCount(plan, log) {
114473
114885
  log({ event: "token_count_cache_hit", cacheSize: cache.size });
114474
114886
  return cached;
114475
114887
  }
114888
+ const now = Date.now();
114889
+ if (now < failureBackoffUntilMs) {
114890
+ log({ event: "token_count_skipped_backoff", backoffRemainingMs: failureBackoffUntilMs - now });
114891
+ return null;
114892
+ }
114476
114893
  if (activeCount >= 1) {
114477
114894
  log({ event: "token_count_skipped_concurrent", activeCount });
114478
114895
  return null;
114479
114896
  }
114897
+ const claudeBin = resolveClaudeBinaryPath(log);
114898
+ if (!claudeBin) return null;
114480
114899
  const cleanupPaths = [];
114481
114900
  activeCount++;
114482
114901
  try {
114483
114902
  const t0 = Date.now();
114484
- log({ event: "token_count_started", cwd: plan.cwd, cacheSize: cache.size });
114485
- const claudeBin = resolveClaudeBinaryPath(log);
114486
- if (!claudeBin) return null;
114487
- const runAttempt = async (reservedServerNames) => {
114488
- const attemptPrepared = await prepareTokenCountArgs(plan, log, reservedServerNames);
114489
- cleanupPaths.push(...attemptPrepared.stubPaths);
114490
- if (attemptPrepared.mcpConfigPath) cleanupPaths.push(attemptPrepared.mcpConfigPath);
114491
- const { stdout } = await execFileAsync4(claudeBin, attemptPrepared.args, {
114492
- cwd: plan.cwd,
114493
- encoding: "utf-8",
114494
- timeout: 6e4,
114495
- env: process.env
114496
- });
114497
- return { stdout, stubCount: attemptPrepared.stubPaths.length };
114498
- };
114499
- let attempt;
114500
- try {
114501
- attempt = await runAttempt(SDK_RESERVED_MCP_SERVER_NAMES);
114502
- } catch (err3) {
114503
- const reservedServerName = parseReservedMcpServerName(
114504
- err3 instanceof Error ? err3.message : String(err3)
114505
- );
114506
- if (!reservedServerName) throw err3;
114507
- log({ event: "token_count_reserved_mcp_retry", serverName: reservedServerName });
114508
- const retryReservedServerNames = new Set(SDK_RESERVED_MCP_SERVER_NAMES);
114509
- retryReservedServerNames.add(reservedServerName);
114510
- attempt = await runAttempt(retryReservedServerNames);
114511
- }
114903
+ log({
114904
+ event: "token_count_started",
114905
+ cwd: plan.cwd,
114906
+ cacheSize: cache.size,
114907
+ binaryPath: claudeBin
114908
+ });
114909
+ const attempt = await runWithRetry(claudeBin, plan, log, cleanupPaths);
114512
114910
  const parsed = JSON.parse(attempt.stdout);
114513
114911
  const resultText = parsed.result ?? "";
114514
114912
  if (!resultText) {
114913
+ failureBackoffUntilMs = Date.now() + FAILURE_BACKOFF_MS;
114515
114914
  log({ event: "token_count_empty_result" });
114516
114915
  return null;
114517
114916
  }
114518
114917
  const result = parseContextOutput(resultText, plan.shipyardSystemPrompt);
114519
114918
  evictOldest();
114520
114919
  cache.set(key, { result, expiry: Date.now() + CACHE_TTL_MS });
114920
+ failureBackoffUntilMs = 0;
114521
114921
  log({
114522
114922
  event: "token_count_complete",
114523
114923
  durationMs: Date.now() - t0,
@@ -114533,7 +114933,13 @@ async function executeTokenCount(plan, log) {
114533
114933
  return result;
114534
114934
  } catch (err3) {
114535
114935
  const errorMessage5 = err3 instanceof Error ? err3.message : String(err3);
114536
- log({ event: "token_count_failed", error: errorMessage5 });
114936
+ failureBackoffUntilMs = Date.now() + FAILURE_BACKOFF_MS;
114937
+ log({
114938
+ event: "token_count_failed",
114939
+ error: errorMessage5,
114940
+ binaryPath: claudeBin,
114941
+ backoffUntilMs: failureBackoffUntilMs
114942
+ });
114537
114943
  return null;
114538
114944
  } finally {
114539
114945
  activeCount--;
@@ -114812,8 +115218,8 @@ var Task = class {
114812
115218
  * The wire `handoff_status.mode` the daemon actually ran for the most recent
114813
115219
  * cross-runtime switch (Wave 2b). Stamped by `applyCrossRuntimeSwitch` from
114814
115220
  * the orchestration layer's real decision (compact-then-bridge that actually
114815
- * compacted, direct when the compact arm timed out, cursor-source-lossy for a
114816
- * text-only source). Read at the destination spawn site so the broadcast
115221
+ * compacted, direct when source compaction could not start or failed,
115222
+ * cursor-source-lossy for a text-only source). Read at the destination spawn site so the broadcast
114817
115223
  * `handoff_status` reflects ground truth instead of the browser's same-
114818
115224
  * threshold prediction. null until the first switch lands.
114819
115225
  */
@@ -115339,7 +115745,8 @@ var Task = class {
115339
115745
  * to avoid an extra `??` in this already-at-budget constructor.
115340
115746
  */
115341
115747
  stallTimeoutMs: deps.stallTimeoutMs,
115342
- firstStepStallTimeoutMs: deps.firstStepStallTimeoutMs
115748
+ firstStepStallTimeoutMs: deps.firstStepStallTimeoutMs,
115749
+ toolExecutionStallTimeoutMs: deps.toolExecutionStallTimeoutMs
115343
115750
  },
115344
115751
  this.#buildMainThreadCallbacks(deps)
115345
115752
  );
@@ -115595,12 +116002,15 @@ var Task = class {
115595
116002
  * OUT of idle before treating return-to-idle as success: `forceCompact()`
115596
116003
  * returns sync but the SM only enters `compacting` LATER on the
115597
116004
  * subprocess's `compaction_started`. Without the latch, poll #1 sees
115598
- * idle and returns true before any compaction. Returns false on
115599
- * failed/timeout/no-subprocess caller falls through to direct
115600
- * bridging and the destination's native compactor handles overflow.
116005
+ * idle and returns true before any compaction. Once compaction starts, this
116006
+ * waits for the SM to settle rather than enforcing a total wall-clock
116007
+ * deadline; the SM's activity-reset silence watchdog handles runtime
116008
+ * liveness. Returns false on failure, no subprocess, or silence before the
116009
+ * compaction starts — caller falls through to direct bridging and the
116010
+ * destination's native compactor handles overflow.
115601
116011
  */
115602
- async compactSelfForHandoff(timeoutMs = COMPACT_TIMEOUT_MS) {
115603
- return compactSelfForHandoff(this.#crossRuntimeCtx(), timeoutMs);
116012
+ async compactSelfForHandoff(startSilenceMs = COMPACTION_SILENCE_TIMEOUT_MS) {
116013
+ return compactSelfForHandoff(this.#crossRuntimeCtx(), startSilenceMs);
115604
116014
  }
115605
116015
  /** Current working directory. May be undefined during early bootstrap before EnterWorktree. */
115606
116016
  get cwd() {
@@ -117599,6 +118009,23 @@ function handlePlanContinueInTask(tasks, log, taskId, toolUseId, decision, feedb
117599
118009
  task.orchestrator.handlePlanContinue(toolUseId, decision, feedback, opts);
117600
118010
  }
117601
118011
 
118012
+ // src/services/task/manager/task-manager-stall-budgets.ts
118013
+ function resolveStallBudgets(agentSystem) {
118014
+ if (!isKnownAgentProvider(agentSystem)) {
118015
+ return {
118016
+ stallTimeoutMs: null,
118017
+ firstStepStallTimeoutMs: null,
118018
+ toolExecutionStallTimeoutMs: null
118019
+ };
118020
+ }
118021
+ const profile = profileRegistry.get(agentSystem);
118022
+ return {
118023
+ stallTimeoutMs: profile.stallTimeoutMs ?? null,
118024
+ firstStepStallTimeoutMs: profile.firstStepStallTimeoutMs ?? null,
118025
+ toolExecutionStallTimeoutMs: profile.toolExecutionStallTimeoutMs
118026
+ };
118027
+ }
118028
+
117602
118029
  // src/services/task/manager/task-manager-threads.ts
117603
118030
  function createThreadInTask(tasks, userId, params) {
117604
118031
  const task = tasks.get(params.taskId);
@@ -119715,8 +120142,7 @@ var TaskManager = class {
119715
120142
  initialOriginalCwd: opts?.initialOriginalCwd,
119716
120143
  taskStateStore: this.#deps.taskStateStore,
119717
120144
  ownerUserId: this.#deps.userId,
119718
- stallTimeoutMs: isKnownAgentProvider(opts.agentSystem) ? profileRegistry.get(opts.agentSystem).stallTimeoutMs ?? null : null,
119719
- firstStepStallTimeoutMs: isKnownAgentProvider(opts.agentSystem) ? profileRegistry.get(opts.agentSystem).firstStepStallTimeoutMs ?? null : null
120145
+ ...resolveStallBudgets(opts.agentSystem)
119720
120146
  });
119721
120147
  }
119722
120148
  };
@@ -119730,11 +120156,11 @@ function pathExistsForLog2(path5) {
119730
120156
  }
119731
120157
 
119732
120158
  // src/services/task/manager/task-state-store.ts
119733
- import { join as join77 } from "path";
120159
+ import { join as join78 } from "path";
119734
120160
 
119735
120161
  // src/services/storage/directory-record-store.ts
119736
120162
  import { mkdir as mkdir35, readdir as readdir22, readFile as readFile48, rename as rename22, stat as stat17, unlink as unlink17 } from "fs/promises";
119737
- import { join as join76 } from "path";
120163
+ import { join as join77 } from "path";
119738
120164
  var SCHEMA_VERSION_FILENAME = "__schema-version";
119739
120165
  var RECORD_SUFFIX = ".json";
119740
120166
  var SAFE_ID_RE = /^[A-Za-z0-9_-][A-Za-z0-9_.-]*$/;
@@ -119754,7 +120180,7 @@ function buildDirectoryRecordStore(opts) {
119754
120180
  let writeQueue = Promise.resolve();
119755
120181
  function recordPath(id) {
119756
120182
  assertSafeRecordId(id);
119757
- return join76(dataDir, `${id}${RECORD_SUFFIX}`);
120183
+ return join77(dataDir, `${id}${RECORD_SUFFIX}`);
119758
120184
  }
119759
120185
  function notify(event) {
119760
120186
  for (const listener of listeners) {
@@ -119769,7 +120195,7 @@ function buildDirectoryRecordStore(opts) {
119769
120195
  }
119770
120196
  async function readSchemaVersion() {
119771
120197
  try {
119772
- const raw = await readFile48(join76(dataDir, SCHEMA_VERSION_FILENAME), "utf-8");
120198
+ const raw = await readFile48(join77(dataDir, SCHEMA_VERSION_FILENAME), "utf-8");
119773
120199
  const parsed = Number.parseInt(raw.trim(), 10);
119774
120200
  return Number.isFinite(parsed) ? parsed : null;
119775
120201
  } catch (err3) {
@@ -119778,7 +120204,7 @@ function buildDirectoryRecordStore(opts) {
119778
120204
  }
119779
120205
  }
119780
120206
  async function writeSchemaVersion(version) {
119781
- const path5 = join76(dataDir, SCHEMA_VERSION_FILENAME);
120207
+ const path5 = join77(dataDir, SCHEMA_VERSION_FILENAME);
119782
120208
  await atomicWriteFile(path5, String(version));
119783
120209
  }
119784
120210
  async function legacyFileExists() {
@@ -119883,7 +120309,7 @@ function buildDirectoryRecordStore(opts) {
119883
120309
  for (const entry of entries) {
119884
120310
  if (!entry.endsWith(RECORD_SUFFIX)) continue;
119885
120311
  const id = entry.slice(0, -RECORD_SUFFIX.length);
119886
- const record = await loadOneRecord(join76(dataDir, entry), entry);
120312
+ const record = await loadOneRecord(join77(dataDir, entry), entry);
119887
120313
  if (record !== null) records.set(id, record);
119888
120314
  }
119889
120315
  return records;
@@ -120184,8 +120610,8 @@ function computePaginatedPage(all, capturedVersion, opts) {
120184
120610
  }
120185
120611
  function buildTaskStateStore(dataDir) {
120186
120612
  const store = buildDirectoryRecordStore({
120187
- dataDir: join77(dataDir, "tasks"),
120188
- legacyFilePath: join77(dataDir, "tasks.json"),
120613
+ dataDir: join78(dataDir, "tasks"),
120614
+ legacyFilePath: join78(dataDir, "tasks.json"),
120189
120615
  recordSchema: TaskRecordSchema,
120190
120616
  currentVersion: TASK_STORE_VERSION,
120191
120617
  migrate(raw) {
@@ -121269,7 +121695,7 @@ async function createDaemon(deps) {
121269
121695
  const annotationStore = buildAnnotationStore(deps.dataDir);
121270
121696
  const deliverableStore = buildDeliverableStore(deps.dataDir);
121271
121697
  const resourceRegistry = createResourceRegistry();
121272
- const assetStore = buildAssetStore(join78(deps.dataDir, "assets"));
121698
+ const assetStore = buildAssetStore(join79(deps.dataDir, "assets"));
121273
121699
  const shipyardResolver = createShipyardResolver();
121274
121700
  shipyardResolver.addSubResolver("asset", createAssetResolver(assetStore));
121275
121701
  const pluginResourceResolver = new PluginResourceResolver(deps.log);
@@ -121278,10 +121704,10 @@ async function createDaemon(deps) {
121278
121704
  const capabilitiesRef = { current: null };
121279
121705
  const rateLimitStoreRef = { current: null };
121280
121706
  const gitCheckpoint = createGitCheckpointService();
121281
- const userSettingsStore = buildUserSettingsStore(join78(deps.dataDir, "user-settings.json"));
121282
- const projectsStore = buildProjectsStore(join78(deps.dataDir, "projects.json"));
121707
+ const userSettingsStore = buildUserSettingsStore(join79(deps.dataDir, "user-settings.json"));
121708
+ const projectsStore = buildProjectsStore(join79(deps.dataDir, "projects.json"));
121283
121709
  const credentialsVaultStore = buildCredentialsVaultStore(
121284
- join78(deps.dataDir, "credentials-vault.json")
121710
+ join79(deps.dataDir, "credentials-vault.json")
121285
121711
  );
121286
121712
  const cursorVaultKeyRef = { current: null };
121287
121713
  let resolveCursorVaultKey;
@@ -121727,7 +122153,7 @@ async function createDaemon(deps) {
121727
122153
  taskId: args.taskId,
121728
122154
  agentSystem: args.agentSystem,
121729
122155
  environmentKey: args.cwd,
121730
- pluginsDir: join78(deps.shipyardHome, "plugins"),
122156
+ pluginsDir: join79(deps.shipyardHome, "plugins"),
121731
122157
  vizWatcher: getOrCreateVizWatcher(args.taskId).watcher,
121732
122158
  mode: "task",
121733
122159
  previewProxy: deps.previewProxy,
@@ -121820,7 +122246,7 @@ async function createDaemon(deps) {
121820
122246
  /** Claude in-process harness — Codex uses the HTTP path with 'codex' above. */
121821
122247
  agentSystem: AGENT_SYSTEM_CLAUDE_CODE,
121822
122248
  environmentKey: args.cwd,
121823
- pluginsDir: join78(deps.shipyardHome, "plugins"),
122249
+ pluginsDir: join79(deps.shipyardHome, "plugins"),
121824
122250
  vizWatcher: args.vizWatcher,
121825
122251
  mode: args.mode ?? "task",
121826
122252
  previewProxy: deps.previewProxy,
@@ -121907,6 +122333,7 @@ async function createDaemon(deps) {
121907
122333
  deps.log
121908
122334
  );
121909
122335
  subprocess.installHarnessTaskIdSetter(setHarnessTaskId);
122336
+ subprocess.installHarnessEpoch(epoch);
121910
122337
  return subprocess;
121911
122338
  }
121912
122339
  const lastUsageLimitBroadcastByTask = /* @__PURE__ */ new Map();
@@ -122146,7 +122573,7 @@ async function createDaemon(deps) {
122146
122573
  if (!cwd) return null;
122147
122574
  try {
122148
122575
  const { gitExecSafe: gitExecSafe2 } = await import("./git-pool-V73Q53NX.js");
122149
- const { getRepoDefaultBranch: getRepoDefaultBranch2 } = await import("./git-repo-QNGPCJLI.js");
122576
+ const { getRepoDefaultBranch: getRepoDefaultBranch2 } = await import("./git-repo-CTZJS3ER.js");
122150
122577
  const branch = await gitExecSafe2(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]) ?? "";
122151
122578
  if (!branch || branch === "HEAD") return null;
122152
122579
  const pr = branchPrStateCache.get(taskId)?.currentBranchPR;
@@ -122834,7 +123261,7 @@ async function createDaemon(deps) {
122834
123261
  const stallProfilerConfig = getStallProfilerConfig();
122835
123262
  const stallProfiler = new StallProfiler({
122836
123263
  log: deps.log,
122837
- outDir: join78(deps.dataDir, "stall-profiles"),
123264
+ outDir: join79(deps.dataDir, "stall-profiles"),
122838
123265
  thresholdMs: stallProfilerConfig.thresholdMs,
122839
123266
  captureMs: stallProfilerConfig.captureMs,
122840
123267
  rateLimitMs: stallProfilerConfig.rateLimitMs
@@ -123434,10 +123861,10 @@ async function createDaemon(deps) {
123434
123861
  import { existsSync as existsSync12 } from "fs";
123435
123862
 
123436
123863
  // src/services/bootstrap/self-update.ts
123437
- import { execFile as execFile12, spawn as spawn13 } from "child_process";
123864
+ import { execFile as execFile11, spawn as spawn14 } from "child_process";
123438
123865
  import { createHash as createHash12 } from "crypto";
123439
123866
  import { chmod as chmod3, mkdir as mkdir37, readFile as readFile49, rename as rename23, unlink as unlink18, writeFile as writeFile29 } from "fs/promises";
123440
- import { join as join79 } from "path";
123867
+ import { join as join80 } from "path";
123441
123868
 
123442
123869
  // src/services/bootstrap/self-update-installer-scripts.ts
123443
123870
  function buildPosixInstallerScript(params) {
@@ -123810,7 +124237,7 @@ function parseNpmViewOutput(stdout) {
123810
124237
  }
123811
124238
  async function defaultResolveVersion(pkg, tag) {
123812
124239
  return new Promise((resolve10, reject) => {
123813
- execFile12(
124240
+ execFile11(
123814
124241
  "npm",
123815
124242
  ["view", `${pkg}@${tag}`, "version", "dist.tarball", "dist.shasum", "--json"],
123816
124243
  { timeout: NPM_VIEW_TIMEOUT_MS, encoding: "utf-8" },
@@ -123839,7 +124266,7 @@ async function downloadTarball(url, destPath, fetchFn) {
123839
124266
  throw new Error(`download failed: HTTP ${response.status} ${response.statusText}`);
123840
124267
  }
123841
124268
  const bytes = new Uint8Array(await response.arrayBuffer());
123842
- await mkdir37(join79(destPath, ".."), { recursive: true });
124269
+ await mkdir37(join80(destPath, ".."), { recursive: true });
123843
124270
  try {
123844
124271
  await writeFile29(tmpPath, bytes);
123845
124272
  await rename23(tmpPath, destPath);
@@ -123869,7 +124296,7 @@ async function verifyChecksum(path5, expectedHash) {
123869
124296
  }
123870
124297
  }
123871
124298
  async function stageInstallerScript(scriptPath, params) {
123872
- await mkdir37(join79(scriptPath, ".."), { recursive: true });
124299
+ await mkdir37(join80(scriptPath, ".."), { recursive: true });
123873
124300
  const body = process.platform === "win32" ? buildWindowsInstallerScript(params) : buildPosixInstallerScript(params);
123874
124301
  await writeFile29(scriptPath, body);
123875
124302
  await chmod3(scriptPath, 493);
@@ -123919,16 +124346,16 @@ function buildInstallerParams(state, deps) {
123919
124346
  targetVersion: state.resolved.version,
123920
124347
  previousVersion: deps.currentVersion,
123921
124348
  parentPid: deps.pid,
123922
- statusFilePath: join79(deps.shipyardHome, "update-status.json"),
123923
- pidFilePath: join79(deps.shipyardHome, "daemon.pid"),
123924
- logPath: join79(deps.shipyardHome, "updates", `install-${deps.pid}.log`),
123925
- snapshotPath: join79(deps.shipyardHome, "updates", `rollback-${deps.currentVersion}`),
124349
+ statusFilePath: join80(deps.shipyardHome, "update-status.json"),
124350
+ pidFilePath: join80(deps.shipyardHome, "daemon.pid"),
124351
+ logPath: join80(deps.shipyardHome, "updates", `install-${deps.pid}.log`),
124352
+ snapshotPath: join80(deps.shipyardHome, "updates", `rollback-${deps.currentVersion}`),
123926
124353
  npmBin: "npm"
123927
124354
  };
123928
124355
  }
123929
124356
  function tarballPathFor(shipyardHome, version, shasum) {
123930
124357
  const shaPrefix = shasum.slice(0, 12);
123931
- return join79(shipyardHome, "updates", `${version}-${shaPrefix}.tgz`);
124358
+ return join80(shipyardHome, "updates", `${version}-${shaPrefix}.tgz`);
123932
124359
  }
123933
124360
  async function runResolveStep(step, state, deps) {
123934
124361
  const resolver = deps.resolveVersion ?? defaultResolveVersion;
@@ -123960,7 +124387,7 @@ async function runStageScriptStep(step, state, deps) {
123960
124387
  );
123961
124388
  await stageInstallerScript(step.scriptPath, params);
123962
124389
  }
123963
- var defaultSpawnCommand = (cmd, args, opts) => spawn13(cmd, [...args], opts ?? {});
124390
+ var defaultSpawnCommand = (cmd, args, opts) => spawn14(cmd, [...args], opts ?? {});
123964
124391
  function runSpawnStep(step, state, deps) {
123965
124392
  const spawnFn = deps.spawnCommand ?? defaultSpawnCommand;
123966
124393
  spawnInstaller(step.scriptPath, step.logPath, spawnFn);
@@ -124150,26 +124577,27 @@ var WARN_EVENTS = /* @__PURE__ */ new Set([
124150
124577
  "webrtc_offer_handshake_failed",
124151
124578
  "webrtc_initiate_handshake_failed",
124152
124579
  /**
124153
- * At-least-once delivery retry signals. A healthy connection sees zero
124154
- * resends; sustained `alo_resend` traffic means the wire is sick. At L30
124155
- * (info) these were invisible to error-filtered dashboards the May 4-5
124156
- * storm (1.21M events/day peak) was caught only by log volume, not by
124157
- * level. Promote to L40 (warn) so the next storm trips the same filters
124158
- * an operator already watches. `alo_dead_letter` and the orphan reaper
124159
- * are stronger failure signals (entry permanently dropped / channel
124160
- * wedged) and warrant warn for the same reason.
124580
+ * At-least-once delivery health summary. Replaces per-event `alo_resend`
124581
+ * warn noise (411K lines / 25-day corpus). One summary per stream per 30s
124582
+ * with pending count, resend count, drop count, and oldest message age.
124583
+ * `alo_dead_letter` and the orphan reaper are stronger failure signals
124584
+ * (entry permanently dropped / channel wedged) and remain at warn.
124585
+ *
124586
+ * `alo_resend` per-event lines are demoted to debug (see DEBUG_EVENTS
124587
+ * below); `alo_resend_exhausted` is an explicit error emitted by the shell
124588
+ * when an entry exceeds maxDeliver — the shell carries `{level:'error'}` so
124589
+ * classifyLogLevel honours it without needing it in WARN_EVENTS.
124161
124590
  */
124162
- "alo_resend",
124591
+ "alo_health",
124163
124592
  "alo_dead_letter",
124164
124593
  "alo_shell_orphan_reaped",
124165
124594
  /**
124166
- * Memory/health heartbeats. These are pure-info by nature, but the packaged
124167
- * Electron app runs the daemon at the default `LOG_LEVEL=warn` (env.ts), so at
124168
- * info they never reach the on-disk log the exact reason a 50GB-RAM incident
124169
- * had ZERO daemon memory samples in its logs (sessions also died sub-10s, but
124170
- * even the healthy days after were warn-only). Memory observability must not be
124171
- * defeated by the default level. Promote so RSS/heap curves are always on disk;
124172
- * the D1 telemetry path (metricsCollector.capture) is already level-independent.
124595
+ * Memory/health heartbeats promoted to WARN for observability: a 50GB-RAM incident
124596
+ * had ZERO daemon memory samples in its logs (sessions died sub-10s, and even
124597
+ * healthy post-incident days were warn-only under the old default). Keeping them at
124598
+ * WARN ensures RSS/heap curves appear even in environments where LOG_LEVEL is
124599
+ * explicitly set to warn; the D1 telemetry path (metricsCollector.capture) is
124600
+ * already level-independent and always fires regardless.
124173
124601
  */
124174
124602
  "health_snapshot",
124175
124603
  "browser_metrics_sample"
@@ -124208,11 +124636,17 @@ var DEBUG_EVENTS = /* @__PURE__ */ new Set([
124208
124636
  * occasionally carries an error will get silently demoted to debug.
124209
124637
  * Only demote events that are pure happy-path counters with no error
124210
124638
  * field. `msg_received` is intentionally EXCLUDED because it can carry
124211
- * a parse error; `alo_resend` is intentionally LEFT IN `WARN_EVENTS`
124212
- * above because sustained resend traffic is a sick-wire signal.
124639
+ * a parse error.
124640
+ *
124641
+ * `alo_resend` per-event lines are demoted here (#4511). The shell
124642
+ * carries `{level:'debug'}` on every event; this belt-and-suspenders
124643
+ * entry ensures classifyLogLevel agrees even if the level field is ever
124644
+ * removed. The `alo_health` periodic summary in WARN_EVENTS is the
124645
+ * intended production-visible signal for retry pressure.
124213
124646
  */
124214
124647
  "channel_send",
124215
124648
  "alo_ack_received",
124649
+ "alo_resend",
124216
124650
  "dc_buffered_sample",
124217
124651
  "control_message_handled"
124218
124652
  ]);
@@ -124295,7 +124729,7 @@ function routeSignalingMessage(peerManager, signalingHandle, log) {
124295
124729
  }
124296
124730
 
124297
124731
  // src/services/lsp/lsp-bridge.ts
124298
- import { spawn as spawn14 } from "child_process";
124732
+ import { spawn as spawn15 } from "child_process";
124299
124733
 
124300
124734
  // src/services/lsp/lsp-server-supervisor.ts
124301
124735
  var SHUTDOWN_GRACE_MS = 5e3;
@@ -124571,7 +125005,7 @@ function handleLSPChannel(cwd, send, log, languageId) {
124571
125005
  serverId: server.serverId
124572
125006
  }),
124573
125007
  log,
124574
- spawn: spawn14,
125008
+ spawn: spawn15,
124575
125009
  now: () => Date.now(),
124576
125010
  setTimeoutFn: (fn, ms) => setTimeout(fn, ms),
124577
125011
  clearTimeoutFn: (timer) => {
@@ -125003,7 +125437,7 @@ function buildLocalDirectChannelCallbacks(deps) {
125003
125437
 
125004
125438
  // src/services/skills-cache/codex-skills-cache.ts
125005
125439
  import { mkdir as mkdir38, readFile as readFile50, rename as rename24, writeFile as writeFile30 } from "fs/promises";
125006
- import { dirname as dirname37, join as join80 } from "path";
125440
+ import { dirname as dirname37, join as join81 } from "path";
125007
125441
  var CACHE_FILENAME = "codex-skills-cache.json";
125008
125442
  var SkillInfoCacheEntrySchema = external_exports.object({
125009
125443
  name: external_exports.string(),
@@ -125020,7 +125454,7 @@ var CodexSkillsCacheFileSchema = external_exports.object({
125020
125454
  skills: external_exports.array(SkillInfoCacheEntrySchema)
125021
125455
  });
125022
125456
  function defaultCodexSkillsCachePath(shipyardHome) {
125023
- return join80(shipyardHome, "state", CACHE_FILENAME);
125457
+ return join81(shipyardHome, "state", CACHE_FILENAME);
125024
125458
  }
125025
125459
  async function loadCodexSkillsCache(path5) {
125026
125460
  let raw;
@@ -125084,11 +125518,11 @@ async function saveCodexSkillsCache(path5, cache2) {
125084
125518
  // src/services/skills-mirror/manager.ts
125085
125519
  import { lstat, mkdir as mkdir40, readlink, stat as stat18, symlink, unlink as unlink20 } from "fs/promises";
125086
125520
  import { homedir as homedir13 } from "os";
125087
- import { dirname as dirname39, join as join82 } from "path";
125521
+ import { dirname as dirname39, join as join83 } from "path";
125088
125522
 
125089
125523
  // src/services/skills-mirror/manifest.ts
125090
125524
  import { mkdir as mkdir39, readFile as readFile51, rename as rename25, unlink as unlink19, writeFile as writeFile31 } from "fs/promises";
125091
- import { dirname as dirname38, join as join81 } from "path";
125525
+ import { dirname as dirname38, join as join82 } from "path";
125092
125526
  var MANIFEST_VERSION = 1;
125093
125527
  var ManifestEntrySchema = external_exports.object({
125094
125528
  /** Display name of the skill (e.g. `qa`, or `plugin:name` when namespaced). */
@@ -125108,7 +125542,7 @@ function emptyManifest() {
125108
125542
  return { version: MANIFEST_VERSION, entries: [] };
125109
125543
  }
125110
125544
  function defaultManifestPath(shipyardHome) {
125111
- return join81(shipyardHome, "state", "skill-mirror-manifest.json");
125545
+ return join82(shipyardHome, "state", "skill-mirror-manifest.json");
125112
125546
  }
125113
125547
  async function loadManifest(path5) {
125114
125548
  try {
@@ -125139,15 +125573,15 @@ async function deleteManifest(path5) {
125139
125573
  function defaultPaths() {
125140
125574
  const home = homedir13();
125141
125575
  return {
125142
- claudeSkillsDir: join82(home, ".claude", "skills"),
125143
- codexSkillsDir: join82(home, ".agents", "skills"),
125576
+ claudeSkillsDir: join83(home, ".claude", "skills"),
125577
+ codexSkillsDir: join83(home, ".agents", "skills"),
125144
125578
  manifestPath: defaultManifestPath(getShipyardHome())
125145
125579
  };
125146
125580
  }
125147
125581
  function plannedSymlinkPath(skill, paths) {
125148
125582
  const linkName = skill.namespace ? `${skill.namespace}:${skill.name}` : skill.name;
125149
- if (skill.sourceAgent === "claude-code") return join82(paths.codexSkillsDir, linkName);
125150
- if (skill.sourceAgent === "codex") return join82(paths.claudeSkillsDir, linkName);
125583
+ if (skill.sourceAgent === "claude-code") return join83(paths.codexSkillsDir, linkName);
125584
+ if (skill.sourceAgent === "codex") return join83(paths.claudeSkillsDir, linkName);
125151
125585
  return null;
125152
125586
  }
125153
125587
  function isPermissionError(err3) {
@@ -125360,7 +125794,7 @@ async function ensureMirror(skills, config, paths = defaultPaths()) {
125360
125794
 
125361
125795
  // src/services/skills-mirror/prefs.ts
125362
125796
  import { mkdir as mkdir41, readFile as readFile52, rename as rename26, writeFile as writeFile32 } from "fs/promises";
125363
- import { dirname as dirname40, join as join83 } from "path";
125797
+ import { dirname as dirname40, join as join84 } from "path";
125364
125798
  var SkillsMirrorPrefsSchema = external_exports.object({
125365
125799
  enabled: external_exports.boolean().default(true)
125366
125800
  });
@@ -125369,7 +125803,7 @@ function defaultSkillsMirrorPrefs() {
125369
125803
  return { enabled: true };
125370
125804
  }
125371
125805
  function prefsPath(dataDir) {
125372
- return join83(dataDir, FILENAME);
125806
+ return join84(dataDir, FILENAME);
125373
125807
  }
125374
125808
  async function loadSkillsMirrorPrefs(dataDir) {
125375
125809
  try {
@@ -125418,7 +125852,7 @@ async function reconcileSkillsMirror(args) {
125418
125852
  // src/services/storage/daemon-settings-store.ts
125419
125853
  import { createHash as createHash13 } from "crypto";
125420
125854
  import { mkdir as mkdir42, readFile as readFile53 } from "fs/promises";
125421
- import { join as join84 } from "path";
125855
+ import { join as join85 } from "path";
125422
125856
  var ProjectSettingsSchema = external_exports.object({
125423
125857
  disabledMcpServers: external_exports.array(external_exports.string()).optional(),
125424
125858
  previewMigrationV36Done: external_exports.boolean().default(false)
@@ -125427,9 +125861,9 @@ function hashProjectPath(projectPath) {
125427
125861
  return createHash13("sha256").update(projectPath).digest("hex").slice(0, 16);
125428
125862
  }
125429
125863
  function buildDaemonSettingsStore(dataDir) {
125430
- const settingsDir = join84(dataDir, "settings");
125864
+ const settingsDir = join85(dataDir, "settings");
125431
125865
  function settingsPath(projectPath) {
125432
- return join84(settingsDir, `${hashProjectPath(projectPath)}.json`);
125866
+ return join85(settingsDir, `${hashProjectPath(projectPath)}.json`);
125433
125867
  }
125434
125868
  async function ensureDir() {
125435
125869
  await mkdir42(settingsDir, { recursive: true });
@@ -125454,12 +125888,12 @@ function buildDaemonSettingsStore(dataDir) {
125454
125888
 
125455
125889
  // src/services/storage/plugin-config-store.ts
125456
125890
  import { mkdir as mkdir43, readFile as readFile54 } from "fs/promises";
125457
- import { join as join85 } from "path";
125891
+ import { join as join86 } from "path";
125458
125892
  function buildPluginConfigStore(pluginsDir) {
125459
125893
  const cache2 = /* @__PURE__ */ new Map();
125460
125894
  const writeQueues = /* @__PURE__ */ new Map();
125461
125895
  function configPath(pluginId) {
125462
- return join85(pluginsDir, pluginId, "config.json");
125896
+ return join86(pluginsDir, pluginId, "config.json");
125463
125897
  }
125464
125898
  async function readConfigFromDisk(pluginId) {
125465
125899
  const fp = configPath(pluginId);
@@ -125513,7 +125947,7 @@ function buildPluginConfigStore(pluginsDir) {
125513
125947
  const fresh = await readConfigFromDisk(pluginId);
125514
125948
  fresh[key] = value;
125515
125949
  cache2.set(pluginId, fresh);
125516
- await mkdir43(join85(pluginsDir, pluginId), { recursive: true });
125950
+ await mkdir43(join86(pluginsDir, pluginId), { recursive: true });
125517
125951
  await atomicWriteFile(configPath(pluginId), JSON.stringify(fresh, null, 2));
125518
125952
  })
125519
125953
  );
@@ -125759,7 +126193,7 @@ async function serve(options = {}) {
125759
126193
  }
125760
126194
  async function runServeBody(options, captureRefs) {
125761
126195
  const shipyardHome = options.shipyardHome ?? getShipyardHome();
125762
- const dataDir = join86(shipyardHome, options.isDev ? "data-dev" : "data");
126196
+ const dataDir = join87(shipyardHome, options.isDev ? "data-dev" : "data");
125763
126197
  const log = createChildLogger({ mode: "serve" });
125764
126198
  log.info(
125765
126199
  { event: "daemon_start", version: getDaemonVersion(), pid: process.pid },
@@ -125767,7 +126201,7 @@ async function runServeBody(options, captureRefs) {
125767
126201
  );
125768
126202
  const { workspaceRoot, unscopedWorkspace, unscopedReason } = await resolveWorkspaceScope();
125769
126203
  registerBuiltinPlugins();
125770
- const pluginConfigStore = buildPluginConfigStore(join86(dataDir, "plugins"));
126204
+ const pluginConfigStore = buildPluginConfigStore(join87(dataDir, "plugins"));
125771
126205
  await mkdir44(dataDir, { recursive: true });
125772
126206
  log.info(
125773
126207
  {
@@ -125860,9 +126294,9 @@ async function runServeBody(options, captureRefs) {
125860
126294
  await bootstrapPhase("pid_sweep_orphans", () => pidTracker.sweepOrphans(logAdapter));
125861
126295
  await bootstrapPhase(
125862
126296
  "legacy_epoch_prune",
125863
- () => pruneOldEpochData(join86(dataDir, "loro"), LEGACY_EPOCH, logAdapter)
126297
+ () => pruneOldEpochData(join87(dataDir, "loro"), LEGACY_EPOCH, logAdapter)
125864
126298
  );
125865
- const storage = new FileStorageAdapter(join86(dataDir, "loro"));
126299
+ const storage = new FileStorageAdapter(join87(dataDir, "loro"));
125866
126300
  const personalWebrtcAdapter = new WebRtcDataChannelAdapter();
125867
126301
  const peerRoleRegistry = createPeerRoleRegistry();
125868
126302
  const presencePoolRef = { pool: {} };
@@ -125970,14 +126404,14 @@ async function runServeBody(options, captureRefs) {
125970
126404
  current: []
125971
126405
  };
125972
126406
  const terminalPtys = /* @__PURE__ */ new Map();
125973
- const terminalLogsDir = join86(shipyardHome, "data", "terminals");
126407
+ const terminalLogsDir = join87(shipyardHome, "data", "terminals");
125974
126408
  await mkdir44(terminalLogsDir, { recursive: true, mode: 448 });
125975
126409
  const publishedArtifactStore = createPublishedArtifactStore({
125976
- rootDir: join86(dataDir, "published"),
126410
+ rootDir: join87(dataDir, "published"),
125977
126411
  log: logAdapter
125978
126412
  });
125979
126413
  const previewStateStore = createPreviewStateStore({
125980
- rootDir: join86(dataDir, "preview-state"),
126414
+ rootDir: join87(dataDir, "preview-state"),
125981
126415
  logger: log
125982
126416
  });
125983
126417
  let codexSkillsApplyScopedPatchRef = null;
@@ -126056,7 +126490,7 @@ async function runServeBody(options, captureRefs) {
126056
126490
  daemon.healthMetrics.start();
126057
126491
  publishedArtifactStore.setSendControlMessage((msg) => daemon.taskManager.broadcastControl(msg));
126058
126492
  previewStateStore.setSendControlMessage((msg) => daemon.taskManager.broadcastControl(msg));
126059
- const pluginsDir = join86(shipyardHome, "plugins");
126493
+ const pluginsDir = join87(shipyardHome, "plugins");
126060
126494
  await mkdir44(pluginsDir, { recursive: true });
126061
126495
  let loadedPlugins = [];
126062
126496
  const loadedPluginsRef = {
@@ -126458,7 +126892,9 @@ async function runServeBody(options, captureRefs) {
126458
126892
  }, delay2);
126459
126893
  capabilityBackstopTimer.unref();
126460
126894
  };
126461
- scheduleCapabilityBackstop();
126895
+ void daemon.capabilityDetectionReady.then(() => {
126896
+ if (!disposed) scheduleCapabilityBackstop();
126897
+ });
126462
126898
  lifecycle.onShutdown(async () => {
126463
126899
  disposed = true;
126464
126900
  if (capabilityBackstopTimer) clearTimeout(capabilityBackstopTimer);
@@ -126581,4 +127017,4 @@ export {
126581
127017
  decideWorkspaceScope,
126582
127018
  serve
126583
127019
  };
126584
- //# sourceMappingURL=serve-D5GKV2RU.js.map
127020
+ //# sourceMappingURL=serve-P3U2C5YH.js.map