frizz 0.12.10 → 0.12.12

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 (102) hide show
  1. package/dist/claude-agent-broker.js +56 -17
  2. package/dist/codex-app-server-daemon.js +57 -11
  3. package/dist/dev-child.js +1207 -517
  4. package/dist/frizz.js +591 -77
  5. package/package.json +1 -1
  6. package/runtime/cc-worker/bin/frizz-mcp.mjs +76 -18
  7. package/runtime/cc-worker/hooks/bash-background.d.mts +1 -1
  8. package/runtime/cc-worker/hooks/bash-background.mjs +17 -6
  9. package/runtime/cc-worker/hooks/deny-ask.mjs +5 -4
  10. package/runtime/cc-worker/hooks/deny-plan.mjs +3 -3
  11. package/runtime/cc-worker/hooks/session-seed.mjs +1 -1
  12. package/runtime/cc-worker/skills/gh/SKILL.md +3 -3
  13. package/web-dist/assets/{TerminalPane-BLyRh8S1.js → TerminalPane-CTuT1746.js} +1 -1
  14. package/web-dist/assets/{abnfDiagram-VRR7QNED-B0n1m57h.js → abnfDiagram-VRR7QNED-C9lXEgoU.js} +1 -1
  15. package/web-dist/assets/architecture-TIHT7OUA-cPNCo9oK.js +1 -0
  16. package/web-dist/assets/{architectureDiagram-ZJ3FMSHR-CW78Jo4Q.js → architectureDiagram-ZJ3FMSHR-BmJyQxby.js} +1 -1
  17. package/web-dist/assets/{blockDiagram-677ZJIJ3-DasDnytT.js → blockDiagram-677ZJIJ3-DYTepy7i.js} +1 -1
  18. package/web-dist/assets/{c4Diagram-LMCZKHZV-B7cCJxqg.js → c4Diagram-LMCZKHZV-CNaJlw58.js} +1 -1
  19. package/web-dist/assets/channel-D5dgm6BA.js +1 -0
  20. package/web-dist/assets/{chunk-32BRIVSS-BBBSn1aq.js → chunk-32BRIVSS-uAMm81q2.js} +1 -1
  21. package/web-dist/assets/{chunk-52WLFC77-B5lATDKz.js → chunk-52WLFC77-DjszZCE7.js} +1 -1
  22. package/web-dist/assets/{chunk-C7G6YPKG-B86e9JiF.js → chunk-C7G6YPKG-BuHoup14.js} +1 -1
  23. package/web-dist/assets/{chunk-EX3LRPZG-CpKL4dGX.js → chunk-EX3LRPZG-Bg9UEQvR.js} +1 -1
  24. package/web-dist/assets/{chunk-FWX5IMBZ-Dp3AvQlT.js → chunk-FWX5IMBZ-CHnQfTLw.js} +2 -2
  25. package/web-dist/assets/{chunk-HOUHSVGY-i9yVPrLo.js → chunk-HOUHSVGY-ByjeQLq2.js} +1 -1
  26. package/web-dist/assets/{chunk-ICXQ74PX--Y7uMkOf.js → chunk-ICXQ74PX-DeM9b_DJ.js} +1 -1
  27. package/web-dist/assets/{chunk-MOJQB5TN-DyMHpsmW.js → chunk-MOJQB5TN-DxLERy2q.js} +1 -1
  28. package/web-dist/assets/{chunk-OGEWGWER-1mtDKLBA.js → chunk-OGEWGWER-sBPjsAxh.js} +1 -1
  29. package/web-dist/assets/{chunk-PUDLZKDR-CFgSrqyc.js → chunk-PUDLZKDR-CarEdLd9.js} +1 -1
  30. package/web-dist/assets/{chunk-Q4XR5HBZ-BPhNENQ8.js → chunk-Q4XR5HBZ-BfXpvkfr.js} +1 -1
  31. package/web-dist/assets/{chunk-V7JOEXUC-CMrXD-em.js → chunk-V7JOEXUC-B3FaJqr_.js} +1 -1
  32. package/web-dist/assets/{chunk-VAUOI2AC-B6lbggln.js → chunk-VAUOI2AC-DHsDLPEL.js} +1 -1
  33. package/web-dist/assets/{chunk-VR4S4FIN-B9qypAjb.js → chunk-VR4S4FIN-ObNNjO-z.js} +1 -1
  34. package/web-dist/assets/{chunk-WYO6CB5R-CI9tkIhS.js → chunk-WYO6CB5R-JZxgPfBf.js} +1 -1
  35. package/web-dist/assets/{chunk-ZGVPDNZ5-DFJPi6M6.js → chunk-ZGVPDNZ5-rLSAye5U.js} +1 -1
  36. package/web-dist/assets/classDiagram-OUVF2IWQ-Bj_Hjv7y.js +1 -0
  37. package/web-dist/assets/classDiagram-v2-EOCWNBFH-Bj_Hjv7y.js +1 -0
  38. package/web-dist/assets/{cynefin-VYW2F7L2-g28vb2Hf.js → cynefin-VYW2F7L2-B0-Xy-xW.js} +1 -1
  39. package/web-dist/assets/{cynefinDiagram-TSTJHNR4-bBtOt0pp.js → cynefinDiagram-TSTJHNR4-Dd3QCTMp.js} +1 -1
  40. package/web-dist/assets/{dagre-VKFMJZFB-DuIcY2-t.js → dagre-VKFMJZFB-c_sDjRhh.js} +1 -1
  41. package/web-dist/assets/{diagram-FQU43EPY-CsYoD3ve.js → diagram-FQU43EPY-DlrOA7Q2.js} +1 -1
  42. package/web-dist/assets/{diagram-G47NLZAW-H3NxrIKS.js → diagram-G47NLZAW-DNwGdtwN.js} +1 -1
  43. package/web-dist/assets/{diagram-NH7WQ7WH-DIojGMfZ.js → diagram-NH7WQ7WH-CPNunIAd.js} +1 -1
  44. package/web-dist/assets/{diagram-OA4YK3LP-D0L444Xe.js → diagram-OA4YK3LP-BZVJWnon.js} +1 -1
  45. package/web-dist/assets/{diagram-WEI45ONY-C3PEfMxp.js → diagram-WEI45ONY-B8UEevkd.js} +1 -1
  46. package/web-dist/assets/{ebnfDiagram-CCIWWBDH-Dknugppg.js → ebnfDiagram-CCIWWBDH-BGCxDQf1.js} +1 -1
  47. package/web-dist/assets/{erDiagram-Q63AITRT-Ds5u_SEw.js → erDiagram-Q63AITRT-CKnUuelS.js} +1 -1
  48. package/web-dist/assets/eventmodeling-45OFAUF4-DvqeSNur.js +1 -0
  49. package/web-dist/assets/flowDiagram-23GEKE2U-DT_LFo10.js +1 -0
  50. package/web-dist/assets/{ganttDiagram-NO4QXBWP-KFFdgLKc.js → ganttDiagram-NO4QXBWP-2-k1ZZhN.js} +1 -1
  51. package/web-dist/assets/{gitGraph-TEB2WS4Q-B62DXBoO.js → gitGraph-TEB2WS4Q-Ci3lt5M_.js} +1 -1
  52. package/web-dist/assets/{gitGraphDiagram-IHSO6WYX-BPtTY3Ld.js → gitGraphDiagram-IHSO6WYX-B_AQwuvk.js} +1 -1
  53. package/web-dist/assets/index-CSVjp00Q.css +1 -0
  54. package/web-dist/assets/index-D5VGTEZg.js +319 -0
  55. package/web-dist/assets/{info-DKCQHKI2-Ct-TkGHr.js → info-DKCQHKI2-m3mT757b.js} +1 -1
  56. package/web-dist/assets/{infoDiagram-FWYZ7A6U-j0jFbDdJ.js → infoDiagram-FWYZ7A6U-CQw6f3Hj.js} +1 -1
  57. package/web-dist/assets/{ishikawaDiagram-FXEZZL3T-DgfNQn9k.js → ishikawaDiagram-FXEZZL3T-C-7JgXGg.js} +1 -1
  58. package/web-dist/assets/{journeyDiagram-5HDEW3XC-BfykOv3I.js → journeyDiagram-5HDEW3XC-DhPxGm-x.js} +1 -1
  59. package/web-dist/assets/{kanban-definition-HUTT4EX6-BvhHHrMj.js → kanban-definition-HUTT4EX6-pYAdpAN-.js} +1 -1
  60. package/web-dist/assets/{line-DZ-APW6S.js → line-DtHJLrdk.js} +1 -1
  61. package/web-dist/assets/{mermaid-parser.core-BWisef3-.js → mermaid-parser.core-CiDWeD1W.js} +3 -3
  62. package/web-dist/assets/{mermaid.core-DnlDGPhM.js → mermaid.core-BIRnljFs.js} +3 -3
  63. package/web-dist/assets/{mindmap-definition-LN4V7U3C-BoTGWSEy.js → mindmap-definition-LN4V7U3C-AwCgXi_v.js} +1 -1
  64. package/web-dist/assets/{packet-7NZHBO7P-DaifVkPE.js → packet-7NZHBO7P-CnOMRKg1.js} +1 -1
  65. package/web-dist/assets/{pegDiagram-2B236MQR-DpLfBJwm.js → pegDiagram-2B236MQR-KwEiKY6p.js} +1 -1
  66. package/web-dist/assets/{pie-RZYD4A2V-BvB0Jxcm.js → pie-RZYD4A2V-BXVw5n_F.js} +1 -1
  67. package/web-dist/assets/{pieDiagram-ENE6RG2P-CSdBcMSK.js → pieDiagram-ENE6RG2P-BKeGzq7n.js} +1 -1
  68. package/web-dist/assets/{quadrantDiagram-ABIIQ3AL-i7k-hoMS.js → quadrantDiagram-ABIIQ3AL-_B_KnoiC.js} +1 -1
  69. package/web-dist/assets/{radar-I7S5WNFK-DpnvOCzG.js → radar-I7S5WNFK-BDV-2CJh.js} +1 -1
  70. package/web-dist/assets/{railroad-3IZDKUUU-Bj4Lhzob.js → railroad-3IZDKUUU-CJLCawbl.js} +1 -1
  71. package/web-dist/assets/railroad-abnf-AHOZXSZD-BLgEAPud.js +1 -0
  72. package/web-dist/assets/railroad-ebnf-EBAXGLYW-Czt3Iqq1.js +1 -0
  73. package/web-dist/assets/railroad-peg-LSFZ7HO6-HPyLt5Zu.js +1 -0
  74. package/web-dist/assets/{railroadDiagram-RFXS5EU6-3HJeC9YI.js → railroadDiagram-RFXS5EU6-BVV6ZmA7.js} +1 -1
  75. package/web-dist/assets/{requirementDiagram-TGXJPOKE-CeBCsZVw.js → requirementDiagram-TGXJPOKE-D3JoXNiN.js} +1 -1
  76. package/web-dist/assets/{sankeyDiagram-HTMAVEWB-BPhonqDa.js → sankeyDiagram-HTMAVEWB-D7r0TcuX.js} +1 -1
  77. package/web-dist/assets/{sequenceDiagram-DBY2YBRQ-C5nocNwQ.js → sequenceDiagram-DBY2YBRQ-B35koHCj.js} +1 -1
  78. package/web-dist/assets/{stateDiagram-2N3HPSRC-CzAZsCPb.js → stateDiagram-2N3HPSRC-DrFIFrPb.js} +1 -1
  79. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-D57MsDPe.js +1 -0
  80. package/web-dist/assets/{swimlanes-5IMT3BWC-DRn8Vs4e.js → swimlanes-5IMT3BWC-D14gjDvr.js} +1 -1
  81. package/web-dist/assets/swimlanesDiagram-G3AALYLV-DC2nRhL8.js +8 -0
  82. package/web-dist/assets/{timeline-definition-FHXFAJF6-Bc99eShR.js → timeline-definition-FHXFAJF6-DYuBi6LS.js} +1 -1
  83. package/web-dist/assets/{treeView-QDETBFTQ--Hn_52w1.js → treeView-QDETBFTQ-DWCgtuaU.js} +1 -1
  84. package/web-dist/assets/{treemap-6X3UGDF4-BCI_8eXP.js → treemap-6X3UGDF4-QuJpQ-gt.js} +1 -1
  85. package/web-dist/assets/{vennDiagram-L72KCM5P-BR-5GJF6.js → vennDiagram-L72KCM5P-BgtM82oJ.js} +1 -1
  86. package/web-dist/assets/{wardley-OPB4EBWU-4HuUekM-.js → wardley-OPB4EBWU-CWh_aioC.js} +1 -1
  87. package/web-dist/assets/{wardleyDiagram-EHGQE667-BI9w8zpX.js → wardleyDiagram-EHGQE667-BNWSkBva.js} +1 -1
  88. package/web-dist/assets/{xychartDiagram-FW5EYKEG-BZ72575j.js → xychartDiagram-FW5EYKEG-Bc6i6hg5.js} +1 -1
  89. package/web-dist/index.html +2 -2
  90. package/web-dist/assets/architecture-TIHT7OUA-BOgxi8qb.js +0 -1
  91. package/web-dist/assets/channel-cwFXt8Vr.js +0 -1
  92. package/web-dist/assets/classDiagram-OUVF2IWQ-BLP_ClYo.js +0 -1
  93. package/web-dist/assets/classDiagram-v2-EOCWNBFH-BLP_ClYo.js +0 -1
  94. package/web-dist/assets/eventmodeling-45OFAUF4-DgF-cIkx.js +0 -1
  95. package/web-dist/assets/flowDiagram-23GEKE2U-CB3C8FCr.js +0 -1
  96. package/web-dist/assets/index-B-zUKqGI.js +0 -319
  97. package/web-dist/assets/index-BIGkwpMf.css +0 -1
  98. package/web-dist/assets/railroad-abnf-AHOZXSZD-scsBBZv_.js +0 -1
  99. package/web-dist/assets/railroad-ebnf-EBAXGLYW-z2-UIe31.js +0 -1
  100. package/web-dist/assets/railroad-peg-LSFZ7HO6-Dm1rBPEF.js +0 -1
  101. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-DEGF_nZS.js +0 -1
  102. package/web-dist/assets/swimlanesDiagram-G3AALYLV-CBLKdauI.js +0 -8
package/dist/dev-child.js CHANGED
@@ -13456,8 +13456,8 @@ function parkExpiredWakeMessage(status) {
13456
13456
  ...status,
13457
13457
  "",
13458
13458
  "Re-park if they are genuinely still going \u2014 there is no limit on that, and a long job is not a",
13459
- "failure. If something is finished, read its result. If nothing is left, end in ```done or ask a",
13460
- "```question."
13459
+ "failure. If something is finished, read its result. If nothing is left, end in ```done or register",
13460
+ "a question with `mcp__frizz__ask`."
13461
13461
  ].join("\n");
13462
13462
  }
13463
13463
  function parkFinishedWakeMessage(status, several) {
@@ -13469,7 +13469,7 @@ function parkFinishedWakeMessage(status, several) {
13469
13469
  "READ ITS OUTPUT AND CARRY ON. This is not a broken fence and there is nothing to fix: the wait",
13470
13470
  "you declared simply ended. Do NOT relaunch the same work \u2014 its result is already on disk.",
13471
13471
  "",
13472
- "Then park on whatever comes next, or end in ```done or a ```question if nothing is left."
13472
+ "Then park on whatever comes next, or end in ```done or a registered question (`mcp__frizz__ask`) if nothing is left."
13473
13473
  ].join("\n");
13474
13474
  }
13475
13475
  function prWatchExpiredWakeMessage(ref) {
@@ -13483,6 +13483,12 @@ function ownWatchExpiredWakeMessage(kind, target) {
13483
13483
 
13484
13484
  If you are still waiting on it, register it again with \`mcp__frizz__watch\` and a fresh \`for:\`. If you are not, and it was the only thing you were waiting on, end in a proper terminal state instead of parking on it again.`;
13485
13485
  }
13486
+ function questionFencesLive(spawnedAt) {
13487
+ if (spawnedAt === void 0 || spawnedAt === null) return true;
13488
+ const at = typeof spawnedAt === "number" ? spawnedAt : Date.parse(spawnedAt);
13489
+ if (!Number.isFinite(at)) return true;
13490
+ return at < Date.parse(QUESTION_FENCE_RETIRED_AT);
13491
+ }
13486
13492
  function askedQuestionDepth(q) {
13487
13493
  let deepest = 1;
13488
13494
  for (const option of q.options ?? []) {
@@ -13679,7 +13685,7 @@ function parseGithubWakeSteer(text2) {
13679
13685
  function frizzRoute(path3) {
13680
13686
  return `${FRIZZ_ROUTE_PREFIX}${path3.startsWith("/") ? path3 : `/${path3}`}`;
13681
13687
  }
13682
- var import_yaml, ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_TEXT_EXTENSIONS, ATTACHMENT_OFFICE_EXTENSIONS, ATTACHMENT_DATA_EXTENSIONS, ATTACHMENT_ARCHIVE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrizzStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, AwaitingHint, RETIRED_AWAITING_KINDS, RETIRED_LINE_RE, RETIRED_AWAITING_REPLACEMENT, AWAITING_KEY_RE, AWAITING_YAML_KEYS, AWAITING_SEQUENCE_KEYS, AWAITING_HINT_MAX, AWAITING_HINT_VALUE_MAX, AWAITING_TITLE_MAX, AWAITING_ITEM_KINDS, AWAITING_DURATION_RE, DURATION_UNIT_MS, AWAITING_FOR_MAX_MS, SnoozeUntil, SNOOZE_PROMPT_MAX, SnoozePrompt, RECURRING_PROMPT_MAX, RECURRING_MIN_INTERVAL_SECONDS, RECURRING_MAX_INTERVAL_SECONDS, RecurringPromptText, RecurringIntervalSeconds, ThreadRecurringPrompt, INTERRUPT_MARKERS, ALLDONE_SENTINEL, OPT_OUT_NOTE, OVER_QUESTION_NOTE, TIMER_PROMPT_MAX, TIMER_MAX_DELAY_SECONDS, TIMER_MAX_ARMED, TimerPromptText, PR_WATCH_SPENT_TRAILER, PR_WATCH_ARMED_TRAILER, SHELL_DONE_TRAILER, SIGNOFF_NUDGE_MARKER, SIGNOFF_NUDGE_MESSAGE, PARK_CORRECTION_NAMES_LEAD, PARK_CORRECTION_RETIRED_LEAD, PARK_CORRECTION_QUESTION_LEAD, ThreadWatchKind, GithubChecksState, GithubMergeState, GithubWatchStatus, ThreadWatchView, PR_WATCH_FOR_MAX_MS, PR_WATCH_DEFAULT_FOR_MS, PR_WATCH_MAX_ARMED, PrWatchView, AddOwnPrWatchInput, AddOwnPrWatchResult, DropOwnPrWatchInput, DropOwnPrWatchResult, OwnWatchKind, AddOwnWatchInput, OwnWatchView, AddOwnWatchResult, DropOwnWatchInput, DropOwnWatchResult, OWN_WATCH_MAX_ARMED, AskQuestionKind, ASK_MAX_DEPTH, AskedOptionSchema, AskedQuestionSchema, AskInput, RegisteredQuestionView, AskResult, UnaskInput, UnaskResult, QuestionAnswerSchema, AnswerQuestionsInput, AnswerQuestionsResult, DismissQuestionsInput, DismissQuestionsResult, BURIED_ANSWERS_HEADER, ANSWER_FOLLOW_UP_MARKER, DISMISSED_ANSWER, MarkOwnDoneInput, MarkOwnDoneResult, ListOwnPrWatchesInput, OwnPrWatchesResult, ThreadTimerView, ThreadFence, LimitWindow, LimitPause, ProviderError, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AccountEmails, AuthSnapshot, AccountLogoutInput, AccountLogoutResult, AccountLoginStartInput, AccountLoginStartResult, AccountLoginStatusInput, AccountLoginStatusResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, AdoptSessionInput, FollowUpInput, UnqueueFollowUpInput, UnqueueFollowUpResult, DeliverQueuedNowInput, DeliverQueuedNowResult, SetThreadSnoozeInput, SetThreadPinnedInput, SetThreadRecurringPromptInput, SetOwnThreadRecurringPromptInput, SetOwnThreadRecurringPromptResult, GetOwnThreadRecurringPromptInput, OwnThreadRecurringPromptResult, SetOwnThreadTimerInput, CancelOwnThreadTimerInput, ListOwnThreadTimersInput, OwnThreadTimersResult, ThreadActivityItem, ListOwnThreadActivityInput, OwnThreadActivityResult, SetOwnThreadTimerResult, CancelOwnThreadTimerResult, SetOwnThreadStopHookInput, SetOwnThreadHeartbeatInput, ThreadPluginReloadResult, RenameThreadInput, AiRenameThreadInput, AiRenameThreadResult, SetOwnThreadTitleInput, SetOwnThreadTitleResult, SetThreadPermissionInput, SetThreadPermissionResult, ThreadProfileOptionsInput, ThreadProfileOptionsResult, ThreadSkillSource, ThreadSkill, ThreadSkillsInput, ThreadSkillsResult, SetThreadProfileInput, SetThreadProfileResult, DISPATCH_TASK_BANNER, DISPATCH_TASK_BANNER_MARKER, GITHUB_DISPATCH_UI_BOUNDARY, WAKE_TIME_HEADER_TAIL, WAKE_TRAILERS, HUMAN_GAP_FLOOR_MS, HUMAN_GAP_NOTE_TAIL, WAKE_DELIVERY_TOKEN_TAIL, WAKE_DELIVERY_TOKEN_LINE, NOISE_PREFIXES, AGENT_MESSAGE_WRAPPER, GithubWakeItem, GithubWakeSteer, WAKE_SCOPE, WAKE_REVIEW_LEAD, WAKE_BACKLOG_TAIL, WAKE_REF, WAKE_SINGLE, WAKE_MULTI_HEAD, WAKE_ITEM, WAKE_MORE, PR_WATCH_FINISHED, PR_WATCH_CI_PASSED, PR_WATCH_CI_GATED, PR_WATCH_STATE, PR_WATCH_CI_FAILED, GithubStatus, GithubRefCard, GithubRefPreviewInput, GithubRefPreviewResult, GithubItem, GithubListInput, GithubListResult, GithubBatchInput, GithubBatchResult, BoardMeta, ServerEvent, TranscriptEdit, TranscriptTodo, TranscriptToolCall, TranscriptPart, TranscriptMessage, TranscriptPageCursor, EditedFile, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketFilePath, SocketTranscriptClientMsg, SocketFileClientMsg, SocketClientMsg, ProjectCard, PROJECT_ICON_EXTENSIONS, PROJECT_ICON_MAX_BASE64_CHARS, DirectoryPickResult, ThreadLocation, FRIZZ_ROUTE_PREFIX, DEFAULT_PORT, threadIdentityName;
13688
+ var import_yaml, ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_TEXT_EXTENSIONS, ATTACHMENT_OFFICE_EXTENSIONS, ATTACHMENT_DATA_EXTENSIONS, ATTACHMENT_ARCHIVE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrizzStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, AwaitingHint, RETIRED_AWAITING_KINDS, RETIRED_LINE_RE, RETIRED_AWAITING_REPLACEMENT, AWAITING_KEY_RE, AWAITING_YAML_KEYS, AWAITING_SEQUENCE_KEYS, AWAITING_HINT_MAX, AWAITING_HINT_VALUE_MAX, AWAITING_TITLE_MAX, AWAITING_ITEM_KINDS, AWAITING_DURATION_RE, DURATION_UNIT_MS, AWAITING_FOR_MAX_MS, SnoozeUntil, SNOOZE_PROMPT_MAX, SnoozePrompt, RECURRING_PROMPT_MAX, RECURRING_MIN_INTERVAL_SECONDS, RECURRING_MAX_INTERVAL_SECONDS, RecurringPromptText, RecurringIntervalSeconds, ThreadRecurringPrompt, INTERRUPT_MARKERS, ALLDONE_SENTINEL, OPT_OUT_NOTE, OVER_QUESTION_NOTE, TIMER_PROMPT_MAX, TIMER_MAX_DELAY_SECONDS, TIMER_MAX_ARMED, TimerPromptText, PR_WATCH_SPENT_TRAILER, PR_WATCH_ARMED_TRAILER, SHELL_DONE_TRAILER, SIGNOFF_NUDGE_MARKER, SIGNOFF_NUDGE_MESSAGE, PARK_CORRECTION_NAMES_LEAD, PARK_CORRECTION_RETIRED_LEAD, PARK_CORRECTION_QUESTION_LEAD, ThreadWatchKind, GithubChecksState, GithubMergeState, GithubWatchStatus, ThreadWatchView, PR_WATCH_FOR_MAX_MS, PR_WATCH_DEFAULT_FOR_MS, PR_WATCH_MAX_ARMED, PrWatchView, AddOwnPrWatchInput, AddOwnPrWatchResult, DropOwnPrWatchInput, DropOwnPrWatchResult, OwnWatchKind, AddOwnWatchInput, OwnWatchView, AddOwnWatchResult, DropOwnWatchInput, DropOwnWatchResult, OWN_WATCH_MAX_ARMED, QUESTION_FENCE_RETIRED_AT, AskQuestionKind, ASK_MAX_DEPTH, AskedOptionSchema, AskedQuestionSchema, AskInput, RegisteredQuestionView, AskResult, UnaskInput, UnaskResult, QuestionAnswerSchema, AnswerQuestionsInput, AnswerQuestionsResult, DismissQuestionsInput, DismissQuestionsResult, BURIED_ANSWERS_HEADER, ANSWER_FOLLOW_UP_MARKER, DISMISSED_ANSWER, MarkOwnDoneInput, MarkOwnDoneResult, ListOwnPrWatchesInput, OwnPrWatchesResult, ThreadTimerView, ThreadFence, LimitWindow, LimitPause, ProviderError, ThreadLinkView, UpsertOwnLinkInput, UpsertOwnLinkResult, DropOwnLinkInput, DropOwnLinkResult, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AccountEmails, AuthSnapshot, AccountLogoutInput, AccountLogoutResult, AccountLoginStartInput, AccountLoginStartResult, AccountLoginStatusInput, AccountLoginStatusResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, AdoptSessionInput, FollowUpInput, UnqueueFollowUpInput, UnqueueFollowUpResult, DeliverQueuedNowInput, DeliverQueuedNowResult, SetThreadSnoozeInput, SetThreadPinnedInput, SetThreadRecurringPromptInput, SetOwnThreadRecurringPromptInput, SetOwnThreadRecurringPromptResult, GetOwnThreadRecurringPromptInput, OwnThreadRecurringPromptResult, SetOwnThreadTimerInput, CancelOwnThreadTimerInput, ListOwnThreadTimersInput, OwnThreadTimersResult, ThreadActivityItem, ListOwnThreadActivityInput, OwnThreadActivityResult, SetOwnThreadTimerResult, CancelOwnThreadTimerResult, SetOwnThreadStopHookInput, SetOwnThreadHeartbeatInput, ThreadPluginReloadResult, RenameThreadInput, AiRenameThreadInput, AiRenameThreadResult, SetOwnThreadTitleInput, SetOwnThreadTitleResult, SetThreadPermissionInput, SetThreadPermissionResult, ThreadProfileOptionsInput, ThreadProfileOptionsResult, ThreadSkillSource, ThreadSkill, ThreadSkillsInput, ThreadSkillsResult, SetThreadProfileInput, SetThreadProfileResult, DISPATCH_TASK_BANNER, DISPATCH_TASK_BANNER_MARKER, GITHUB_DISPATCH_UI_BOUNDARY, WAKE_TIME_HEADER_TAIL, WAKE_TRAILERS, HUMAN_GAP_FLOOR_MS, HUMAN_GAP_NOTE_TAIL, WAKE_DELIVERY_TOKEN_TAIL, WAKE_DELIVERY_TOKEN_LINE, NOISE_PREFIXES, AGENT_MESSAGE_WRAPPER, GithubWakeItem, GithubWakeSteer, WAKE_SCOPE, WAKE_REVIEW_LEAD, WAKE_BACKLOG_TAIL, WAKE_REF, WAKE_SINGLE, WAKE_MULTI_HEAD, WAKE_ITEM, WAKE_MORE, PR_WATCH_FINISHED, PR_WATCH_CI_PASSED, PR_WATCH_CI_GATED, PR_WATCH_STATE, PR_WATCH_CI_FAILED, GithubStatus, GithubRefCard, GithubRefPreviewInput, GithubRefPreviewResult, GithubItem, GithubListInput, GithubListResult, GithubBatchInput, GithubBatchResult, BoardMeta, ServerEvent, TranscriptEdit, TranscriptTodo, TranscriptToolCall, TranscriptPart, TranscriptMessage, TranscriptPageCursor, EditedFile, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketFilePath, SocketTranscriptClientMsg, SocketFileClientMsg, SocketClientMsg, ProjectCard, PROJECT_ICON_EXTENSIONS, PROJECT_ICON_MAX_BASE64_CHARS, DirectoryPickResult, ThreadLocation, FRIZZ_ROUTE_PREFIX, DEFAULT_PORT, threadIdentityName;
13683
13689
  var init_src = __esm({
13684
13690
  "packages/shared/src/index.ts"() {
13685
13691
  "use strict";
@@ -13803,7 +13809,14 @@ var init_src = __esm({
13803
13809
  slug: external_exports.string(),
13804
13810
  displayName: external_exports.string(),
13805
13811
  defaultEffort: external_exports.string(),
13806
- efforts: external_exports.array(external_exports.string())
13812
+ efforts: external_exports.array(external_exports.string()),
13813
+ // The model's stock context window and the largest one it accepts, in tokens, straight from the
13814
+ // cache's `context_window` / `max_context_window` (272K / 872K on GPT-5.6, 128K / 128K on Spark).
13815
+ // The Settings "Context window" presets are built from these so the drawer names the numbers codex
13816
+ // will actually run at (maintainer 2026-09-11: "reflect the actual numbers"). Optional: a cache from
13817
+ // before the fields existed, or the degraded fallback, simply carries neither.
13818
+ contextWindow: external_exports.number().int().positive().optional(),
13819
+ maxContextWindow: external_exports.number().int().positive().optional()
13807
13820
  });
13808
13821
  ThreadProfileOption = external_exports.object({
13809
13822
  model: external_exports.string().min(1),
@@ -13990,7 +14003,7 @@ var init_src = __esm({
13990
14003
  RETIRED_AWAITING_REPLACEMENT = {
13991
14004
  "watch": "`shells: [<the id your runtime gave you>]` (or `agents: [<id>]`) \u2014 the same id, in the current sequence",
13992
14005
  "pr-watch": "register the PR with `mcp__frizz__watch_pr`, then name it `prs: [owner/repo#123]`",
13993
- "human": "there is no human gate any more \u2014 if you need a person, ask a ```question instead of parking",
14006
+ "human": "there is no human gate any more \u2014 if you need a person, register a question with `mcp__frizz__ask` instead of parking",
13994
14007
  "ci": "CI is not a wait of its own: register the PR with `mcp__frizz__watch_pr` and you are woken when its checks settle",
13995
14008
  "session": "there is no cross-session wait \u2014 name the sub-agent you dispatched with `agents: [<id>]`",
13996
14009
  // THE 2026-08-24 CUTOVER. The frontmatter is YAML now, and YAML has no repeated keys — so the four
@@ -14050,7 +14063,7 @@ var init_src = __esm({
14050
14063
  INTERRUPT_MARKERS = ["[Request interrupted by user]", "[Request interrupted by user for tool use]"];
14051
14064
  ALLDONE_SENTINEL = "ALLDONE";
14052
14065
  OPT_OUT_NOTE = "To stop these, sign off with a ```done fence \u2014 but ONLY when the work is genuinely finished: it files this thread away, and nothing but new work from the human reopens it.";
14053
- OVER_QUESTION_NOTE = "Your ```question is still unanswered, and a Goal armed at rest means the operator is not waiting to answer it: decide it yourself, say in one line which way you went and what would reverse it, and carry on. Do NOT re-ask it.";
14066
+ OVER_QUESTION_NOTE = "Your registered question is still unanswered, and a Goal armed at rest means the operator is not waiting to answer it: decide it yourself, say in one line which way you went and what would reverse it, and carry on. Do NOT re-ask it.";
14054
14067
  TIMER_PROMPT_MAX = SNOOZE_PROMPT_MAX;
14055
14068
  TIMER_MAX_DELAY_SECONDS = 30 * 24 * 60 * 60;
14056
14069
  TIMER_MAX_ARMED = 64;
@@ -14082,14 +14095,16 @@ var init_src = __esm({
14082
14095
  "the write-up.",
14083
14096
  "",
14084
14097
  "**DECIDE RATHER THAN ASK.** Stop only for a decision that is genuinely the human's AND that blocks you",
14085
- "right now: ask that one in a question fence. Every other open choice INSIDE the task \u2014 a name, a",
14098
+ "right now: register that one with `mcp__frizz__ask`. Every other open choice INSIDE the task \u2014 a name, a",
14086
14099
  "default, a reversible design call \u2014 is yours to make: decide it, say in one line which way you went and",
14087
14100
  "what would reverse it, and carry on. A choice that would ENLARGE the task is not one of those: an",
14088
14101
  "unanswered question is not permission to go build the answer.",
14089
14102
  "",
14090
- "Otherwise, add a fence at the END of your next message:",
14103
+ "Otherwise, sign off \u2014 a registration, or a fence at the END of your next message:",
14091
14104
  "",
14092
- "- `` ```question `` \u2014 you need the human. One question per fence, lettered options, one recommended.",
14105
+ "- `mcp__frizz__ask` \u2014 you need the human. NOT a fence: the ```question fence is retired, and a fence",
14106
+ " with a question in its body is plain prose. Register it (options with one-line trade-offs, the",
14107
+ " recommended one first), then rest normally \u2014 an open registered question is the sign-off.",
14093
14108
  "- `` ```done `` \u2014 genuinely FINISHED. A DISMISSAL: the card is filed away and nobody looks again, so",
14094
14109
  " if anything is still owed, it is not done. Body: 1-3 sentences, then bullets, each opening with a",
14095
14110
  " **bolded verb phrase**.",
@@ -14304,6 +14319,7 @@ var init_src = __esm({
14304
14319
  watches: external_exports.array(OwnWatchView)
14305
14320
  }).strict();
14306
14321
  OWN_WATCH_MAX_ARMED = 24;
14322
+ QUESTION_FENCE_RETIRED_AT = "2026-09-11T17:30:00Z";
14307
14323
  AskQuestionKind = external_exports.enum(["question", "multi"]);
14308
14324
  ASK_MAX_DEPTH = 3;
14309
14325
  AskedOptionSchema = external_exports.lazy(() => external_exports.object({
@@ -14444,6 +14460,20 @@ var init_src = __esm({
14444
14460
  retrying: external_exports.boolean().optional(),
14445
14461
  at: external_exports.string().optional()
14446
14462
  });
14463
+ ThreadLinkView = external_exports.object({
14464
+ id: external_exports.string(),
14465
+ kind: external_exports.enum(["link", "file"]),
14466
+ label: external_exports.string(),
14467
+ target: external_exports.string()
14468
+ }).strict();
14469
+ UpsertOwnLinkInput = external_exports.object({
14470
+ slug: ThreadSlug,
14471
+ label: external_exports.string().trim().min(1).max(240).regex(/^[^\x00-\x1f\x7f]+$/),
14472
+ target: external_exports.string().trim().min(1).max(8192).regex(/^[^\x00-\x1f\x7f]+$/)
14473
+ }).strict();
14474
+ UpsertOwnLinkResult = external_exports.object({ link: ThreadLinkView }).strict();
14475
+ DropOwnLinkInput = external_exports.object({ slug: ThreadSlug, id: external_exports.string().min(1) }).strict();
14476
+ DropOwnLinkResult = external_exports.object({ dropped: external_exports.boolean() }).strict();
14447
14477
  ThreadView = external_exports.object({
14448
14478
  id: ThreadSlug,
14449
14479
  // slug; filename is <slug>.md
@@ -14512,6 +14542,8 @@ var init_src = __esm({
14512
14542
  // Live background SHELLS the worker launched (tailer-derived). Same default-[] discipline. Rendered
14513
14543
  // in the anchored background-ops strip alongside sub-agents; ids make current rows drillable.
14514
14544
  bgShells: external_exports.array(BgShellView).default([]),
14545
+ // Optional for snapshots from a server that predates link registration.
14546
+ links: external_exports.array(ThreadLinkView).optional(),
14515
14547
  // The thread's ARMED WATCHERS — registry-derived, not folded from the transcript, which is what makes
14516
14548
  // them survive the worker saying one more sentence. Same default-[] discipline as the two above.
14517
14549
  //
@@ -14830,6 +14862,15 @@ var init_src = __esm({
14830
14862
  // writes were 51% of a day's spend while the entries were invalidated every 15–30 minutes anyway
14831
14863
  // (see claudePromptCacheEnv). Optional so an old blob parses; defaultSettings pins "auto".
14832
14864
  promptCacheTtl: external_exports.enum(["auto", "5m", "1h"]).optional(),
14865
+ // The context window a NEW Codex thread runs with, in tokens. Codex's own default is the model's
14866
+ // stock window (272K on GPT-5.6), but the catalogue also carries a larger `max_context_window` (872K
14867
+ // on GPT-5.6-sol/terra/luna and GPT-6-astra, measured 2026-09-11 in ~/.codex/models_cache.json) that
14868
+ // a thread only reaches when asked. Reaches codex as the `model_context_window` config override on
14869
+ // `thread/start` and every cold `thread/resume` (see codexContextWindowConfig); codex clamps it to
14870
+ // the model's maximum, so a value above it is harmless, and it moves the auto-compact threshold with
14871
+ // it (codex compacts at 90% of the resolved window). Unset ⇒ nothing is sent and the model's stock
14872
+ // window applies. Optional so an old blob parses; defaultSettings leaves it unset.
14873
+ codexContextWindow: external_exports.number().int().positive().optional(),
14833
14874
  // The GitHub batch-dispatch prompt template (the picker's per-item worker prompt). Optional: when
14834
14875
  // unset OR blank the server falls back to its exported DEFAULT_GITHUB_PROMPT. Substitution tokens
14835
14876
  // the server fills: {repo} {n} {title} {url} {labels} {body}. The leading `THREAD: <slug>` tag is
@@ -15049,6 +15090,7 @@ var init_src = __esm({
15049
15090
  }).strict();
15050
15091
  OwnThreadActivityResult = external_exports.object({
15051
15092
  activity: external_exports.array(ThreadActivityItem),
15093
+ links: external_exports.array(ThreadLinkView).optional(),
15052
15094
  /** Every question still owed an answer. NOT a `ThreadActivityItem` and deliberately its own list: a
15053
15095
  * question is not running work, it waits on a PERSON, and there is no `questions:` key in the awaiting
15054
15096
  * fence for it to be written into. Until 2026-08-28 a worker could read its open questions back only
@@ -17788,7 +17830,7 @@ function rpcRequestIdJson(value) {
17788
17830
  }
17789
17831
  function canonicalJson(value) {
17790
17832
  const visiting = /* @__PURE__ */ new WeakSet();
17791
- const normalize3 = (candidate) => {
17833
+ const normalize2 = (candidate) => {
17792
17834
  if (candidate === null || typeof candidate === "string" || typeof candidate === "boolean") return candidate;
17793
17835
  if (typeof candidate === "number") {
17794
17836
  if (!Number.isFinite(candidate)) throw new InteractionStoreError("invalid-response", "provider JSON contains a non-finite number");
@@ -17800,14 +17842,14 @@ function canonicalJson(value) {
17800
17842
  if (visiting.has(candidate)) throw new InteractionStoreError("invalid-response", "provider JSON contains a cycle");
17801
17843
  visiting.add(candidate);
17802
17844
  try {
17803
- if (Array.isArray(candidate)) return candidate.map((item) => item === void 0 ? null : normalize3(item));
17845
+ if (Array.isArray(candidate)) return candidate.map((item) => item === void 0 ? null : normalize2(item));
17804
17846
  const object = candidate;
17805
- return Object.fromEntries(Object.keys(object).sort().filter((key) => object[key] !== void 0).map((key) => [key, normalize3(object[key])]));
17847
+ return Object.fromEntries(Object.keys(object).sort().filter((key) => object[key] !== void 0).map((key) => [key, normalize2(object[key])]));
17806
17848
  } finally {
17807
17849
  visiting.delete(candidate);
17808
17850
  }
17809
17851
  };
17810
- return JSON.stringify(normalize3(value));
17852
+ return JSON.stringify(normalize2(value));
17811
17853
  }
17812
17854
  function boundedProviderJson(value) {
17813
17855
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
@@ -19385,6 +19427,20 @@ function createStorage(source, projectId) {
19385
19427
  `);
19386
19428
  const prWatchCursorStmt = scope.prepare("UPDATE pr_watch SET cursor = ? WHERE project_id = @project_id AND id = ? AND state = 'armed'");
19387
19429
  const delPrWatches = scope.prepare("DELETE FROM pr_watch WHERE project_id = @project_id AND thread_slug = ?");
19430
+ const upsertThreadLinkStmt = scope.prepare(`
19431
+ INSERT INTO thread_link (project_id, id, thread_slug, kind, label, target, created_at)
19432
+ VALUES (@project_id, @id, @slug, @kind, @label, @target, @createdAtMs)
19433
+ ON CONFLICT (project_id, thread_slug, label) DO UPDATE SET kind = excluded.kind, target = excluded.target
19434
+ RETURNING *
19435
+ `);
19436
+ const threadLinksStmt = scope.prepare(
19437
+ "SELECT * FROM thread_link WHERE project_id = @project_id ORDER BY created_at, rowid"
19438
+ );
19439
+ const threadLinksBySlugStmt = scope.prepare(
19440
+ "SELECT * FROM thread_link WHERE project_id = @project_id AND thread_slug = ? ORDER BY created_at, rowid"
19441
+ );
19442
+ const dropThreadLinkStmt = scope.prepare("DELETE FROM thread_link WHERE project_id = @project_id AND thread_slug = ? AND id = ?");
19443
+ const delThreadLinks = scope.prepare("DELETE FROM thread_link WHERE project_id = @project_id AND thread_slug = ?");
19388
19444
  const armThreadWatchStmt = scope.prepare(`
19389
19445
  INSERT INTO thread_watch (project_id, id, thread_slug, kind, target, state, created_at, expires_at, settled_at)
19390
19446
  VALUES (@project_id, @id, @slug, @kind, @target, 'armed', @createdAtMs, @expiresAtMs, NULL)
@@ -19524,6 +19580,7 @@ function createStorage(source, projectId) {
19524
19580
  delThreadTimers.run(existing.slug);
19525
19581
  delPrWatches.run(existing.slug);
19526
19582
  delThreadWatches.run(existing.slug);
19583
+ delThreadLinks.run(existing.slug);
19527
19584
  delThreadQuestions.run(existing.slug);
19528
19585
  delThreadDone.run(existing.slug);
19529
19586
  delSubAgentSteers.run(existing.slug);
@@ -19975,6 +20032,10 @@ function createStorage(source, projectId) {
19975
20032
  dropPrWatch: (slug, id, settledAtMs) => dropPrWatchStmt.run(settledAtMs, id, slug).changes === 1,
19976
20033
  settlePrWatch: (id, settledAtMs) => settlePrWatchStmt.run(settledAtMs, id).changes === 1,
19977
20034
  setPrWatchCursor: (id, cursor) => prWatchCursorStmt.run(cursor, id).changes === 1,
20035
+ upsertThreadLink: (link) => upsertThreadLinkStmt.get(link),
20036
+ listThreadLinks: (slug) => threadLinksBySlugStmt.all(slug),
20037
+ threadLinksBySlug: () => groupBySlug(threadLinksStmt.all()),
20038
+ dropThreadLink: (slug, id) => dropThreadLinkStmt.run(slug, id).changes === 1,
19978
20039
  // IDEMPOTENT BY (thread, kind, target), which is what the partial unique index enforces. A worker
19979
20040
  // woken by an expiry re-registers the same wait, and a worker that simply calls twice must not end
19980
20041
  // up with two rows to drop — so an existing armed row is RETURNED rather than replaced. Replacing
@@ -20436,6 +20497,17 @@ var init_storage = __esm({
20436
20497
  ON pr_watch(project_id, state);
20437
20498
  CREATE INDEX IF NOT EXISTS pr_watch_slug
20438
20499
  ON pr_watch(project_id, thread_slug, state, created_at);
20500
+ -- Saved destinations have no liveness or expiry. The label is a stable slot within one thread.
20501
+ CREATE TABLE IF NOT EXISTS thread_link (
20502
+ id TEXT PRIMARY KEY,
20503
+ project_id TEXT NOT NULL,
20504
+ thread_slug TEXT NOT NULL,
20505
+ kind TEXT NOT NULL CHECK (kind IN ('link', 'file')),
20506
+ label TEXT NOT NULL,
20507
+ target TEXT NOT NULL,
20508
+ created_at INTEGER NOT NULL,
20509
+ UNIQUE (project_id, thread_slug, label)
20510
+ );
20439
20511
  -- A worker's registered WATCHES on its own running work (2026-08-26). See
20440
20512
  -- plans/rest-by-registration.md: a wait stops being a line the worker re-writes at every rest and
20441
20513
  -- becomes a row it creates once, which the human sees in the queue and which wakes the thread itself.
@@ -20549,7 +20621,8 @@ var init_storage = __esm({
20549
20621
  "thread_watch",
20550
20622
  "thread_question",
20551
20623
  "thread_done",
20552
- "subagent_steer"
20624
+ "subagent_steer",
20625
+ "thread_link"
20553
20626
  ];
20554
20627
  }
20555
20628
  });
@@ -20694,6 +20767,9 @@ var init_settings = __esm({
20694
20767
  // "auto" = the CLI's own tier (1h on a subscription). See the schema and claudePromptCacheEnv for
20695
20768
  // when "5m" is the cheaper choice.
20696
20769
  promptCacheTtl: "auto",
20770
+ // Unset = codex's own window for the model (272K on GPT-5.6). The schema and codexContextWindowConfig
20771
+ // explain the 872K ceiling a larger value is clamped to.
20772
+ codexContextWindow: void 0,
20697
20773
  // Hidden until asked for — see the schema for why.
20698
20774
  projectRail: false
20699
20775
  });
@@ -21350,7 +21426,23 @@ function toCodexModel(raw2) {
21350
21426
  const def = typeof m.default_reasoning_level === "string" ? m.default_reasoning_level : "";
21351
21427
  const defaultEffort = def && efforts.includes(def) ? def : efforts[0];
21352
21428
  const priority = typeof m.priority === "number" && Number.isFinite(m.priority) ? m.priority : Number.MAX_SAFE_INTEGER;
21353
- return { model: { slug, displayName, defaultEffort, efforts }, priority };
21429
+ const window = (key) => {
21430
+ const v = m[key];
21431
+ return typeof v === "number" && Number.isInteger(v) && v > 0 ? v : void 0;
21432
+ };
21433
+ const contextWindow = window("context_window");
21434
+ const maxContextWindow = window("max_context_window");
21435
+ return {
21436
+ model: {
21437
+ slug,
21438
+ displayName,
21439
+ defaultEffort,
21440
+ efforts,
21441
+ ...contextWindow === void 0 ? {} : { contextWindow },
21442
+ ...maxContextWindow === void 0 ? {} : { maxContextWindow }
21443
+ },
21444
+ priority
21445
+ };
21354
21446
  }
21355
21447
  function parseCodexModelsCache(raw2) {
21356
21448
  let doc;
@@ -22098,6 +22190,35 @@ var init_detached_daemons = __esm({
22098
22190
  }
22099
22191
  });
22100
22192
 
22193
+ // packages/server/src/backend/daemon-tree.ts
22194
+ import { spawnSync } from "node:child_process";
22195
+ function endDaemonTree(pid, signal, deps = {}) {
22196
+ const kill = deps.kill ?? defaultKill;
22197
+ if ((deps.platform ?? process.platform) !== "win32") {
22198
+ try {
22199
+ kill(pid, signal);
22200
+ } catch {
22201
+ }
22202
+ return;
22203
+ }
22204
+ const killed = (deps.spawnSync ?? spawnSync)("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
22205
+ if (killed.error || killed.status !== 0) {
22206
+ try {
22207
+ kill(pid, signal);
22208
+ } catch {
22209
+ }
22210
+ }
22211
+ }
22212
+ var defaultKill;
22213
+ var init_daemon_tree = __esm({
22214
+ "packages/server/src/backend/daemon-tree.ts"() {
22215
+ "use strict";
22216
+ defaultKill = (pid, signal) => {
22217
+ process.kill(pid, signal);
22218
+ };
22219
+ }
22220
+ });
22221
+
22101
22222
  // packages/server/src/backend/ipc-path.ts
22102
22223
  import { join as join15 } from "node:path";
22103
22224
  function frizzIpcPath(name) {
@@ -22260,6 +22381,7 @@ function forkBroker(options) {
22260
22381
  const socketPath = claudeBrokerSocketPath(options.stateDir, options.sessionId);
22261
22382
  const recordPath2 = claudeBrokerRecordPath(options.stateDir, options.sessionId);
22262
22383
  mkdirSync9(dirname8(recordPath2), { recursive: true });
22384
+ const generation = randomUUID8();
22263
22385
  const config = {
22264
22386
  socketPath,
22265
22387
  cwd: options.cwd,
@@ -22268,7 +22390,7 @@ function forkBroker(options) {
22268
22390
  permissionMode: options.permissionMode,
22269
22391
  env: options.env,
22270
22392
  recordPath: recordPath2,
22271
- generation: randomUUID8(),
22393
+ generation,
22272
22394
  // The daemon writes its own death forensics here. Same dir as the record, so a session's socket,
22273
22395
  // record and diagnostics stay together and a project teardown removes all three.
22274
22396
  diagnosticLogPath: claudeBrokerDiagnosticLogPath(options.stateDir, options.sessionId),
@@ -22286,18 +22408,52 @@ function forkBroker(options) {
22286
22408
  cwd: options.cwd,
22287
22409
  env: { ...process.env, FRIZZ_CLAUDE_BROKER: JSON.stringify(config) },
22288
22410
  detached: true,
22289
- stdio: "ignore"
22411
+ stdio: "ignore",
22412
+ // A detached process owns no console on Windows; keep the daemon from ever being handed a
22413
+ // visible one (Windows audit 2026-09-11, finding 4 — the Codex daemon's spawn tells the story).
22414
+ windowsHide: true
22290
22415
  });
22291
22416
  child.unref();
22292
22417
  const deadline = Date.now() + (options.timeoutMs ?? 3e4);
22418
+ const recordedCause = () => readClaudeBrokerExit(options.stateDir, options.sessionId, generation);
22293
22419
  return new Promise((resolve12, reject) => {
22420
+ let settled = false;
22421
+ let timer;
22422
+ const settle = (outcome) => {
22423
+ if (settled) return;
22424
+ settled = true;
22425
+ if (timer) clearTimeout(timer);
22426
+ outcome();
22427
+ };
22294
22428
  const poll = () => {
22295
22429
  const record = readBrokerRecord(recordPath2);
22296
- if (record && pidAlive(record.daemonPid)) return resolve12(record);
22297
- if (Date.now() > deadline) return reject(new Error(`Claude broker for session ${options.sessionId} did not become ready`));
22298
- setTimeout(poll, 50);
22430
+ if (record && pidAlive(record.daemonPid)) return settle(() => resolve12(record));
22431
+ if (Date.now() > deadline) {
22432
+ const exit = recordedCause();
22433
+ return settle(() => reject(new Error(
22434
+ `Claude broker for session ${options.sessionId} did not become ready${exit ? `: ${describeClaudeBrokerExit(exit)}` : ""}`
22435
+ )));
22436
+ }
22437
+ timer = setTimeout(poll, 50);
22299
22438
  };
22300
- child.once("error", reject);
22439
+ child.once("error", (error) => settle(() => reject(error)));
22440
+ child.once("exit", (code, signal) => {
22441
+ if (settled) return;
22442
+ const how = signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`;
22443
+ const until = Date.now() + 500;
22444
+ const read = () => {
22445
+ if (settled) return;
22446
+ const exit = recordedCause();
22447
+ if (!exit && Date.now() < until) {
22448
+ setTimeout(read, 25);
22449
+ return;
22450
+ }
22451
+ settle(() => reject(new Error(
22452
+ `Claude broker for session ${options.sessionId} exited before it became ready (${how}): ${describeClaudeBrokerExit(exit)}`
22453
+ )));
22454
+ };
22455
+ read();
22456
+ });
22301
22457
  poll();
22302
22458
  });
22303
22459
  }
@@ -22306,7 +22462,7 @@ async function adoptOrForkBroker(options) {
22306
22462
  if (existing) return { record: existing, reattached: true };
22307
22463
  return { record: await forkBroker(options), reattached: false };
22308
22464
  }
22309
- function killBroker(stateDir, sessionId, retireReason) {
22465
+ function killBroker(stateDir, sessionId, retireReason, deps = {}) {
22310
22466
  const recordPath2 = claudeBrokerRecordPath(stateDir, sessionId);
22311
22467
  const record = liveBrokerRecord(recordPath2);
22312
22468
  if (retireReason) {
@@ -22314,10 +22470,7 @@ function killBroker(stateDir, sessionId, retireReason) {
22314
22470
  } else takeBrokerRetirement(stateDir, sessionId);
22315
22471
  if (record) {
22316
22472
  rememberBrokerIdentity(recordPath2, record);
22317
- try {
22318
- process.kill(record.daemonPid, "SIGTERM");
22319
- } catch {
22320
- }
22473
+ endDaemonTree(record.daemonPid, "SIGTERM", deps);
22321
22474
  }
22322
22475
  try {
22323
22476
  unlinkSync(recordPath2);
@@ -22331,6 +22484,7 @@ var init_claude_broker_host = __esm({
22331
22484
  "use strict";
22332
22485
  init_detached_daemons();
22333
22486
  init_claude_broker_diagnostics();
22487
+ init_daemon_tree();
22334
22488
  init_ipc_path();
22335
22489
  WINDOWS_SHIM_TARGET = /^\s*"%dp0%\\(.+?)"/mu;
22336
22490
  }
@@ -23286,10 +23440,30 @@ var init_tail_cache = __esm({
23286
23440
  import { statSync as statSync8, openSync as openSync10, readSync as readSync4, closeSync as closeSync9, readdirSync as readdirSync5, realpathSync as realpathSync5, mkdirSync as mkdirSync10, writeFileSync as writeFileSync8, readFileSync as readFileSync12, existsSync as existsSync5 } from "node:fs";
23287
23441
  import { execFile as execFile2 } from "node:child_process";
23288
23442
  import { promisify as promisify2 } from "node:util";
23289
- import { basename as basename4, join as join18 } from "node:path";
23443
+ import { basename as basename4, join as join18, win32 } from "node:path";
23290
23444
  import { homedir as homedir7 } from "node:os";
23291
- async function probeShellsAlive(outputFiles) {
23445
+ async function probeShellsAlive(outputFiles, opts = {}) {
23446
+ const platform3 = opts.platform ?? process.platform;
23447
+ const exec = opts.exec ?? execFileAsync;
23292
23448
  const verdicts = /* @__PURE__ */ new Map();
23449
+ if (platform3 === "win32") {
23450
+ const present = outputFiles.filter((file) => {
23451
+ if (existsSync5(file)) return true;
23452
+ verdicts.set(file, void 0);
23453
+ return false;
23454
+ });
23455
+ if (present.length === 0) return verdicts;
23456
+ const [shell, args] = windowsShellHolderCommand(present, opts.env ?? process.env);
23457
+ let report;
23458
+ try {
23459
+ report = (await exec(shell, args, { encoding: "utf8", timeout: 8e3, windowsHide: true })).stdout;
23460
+ } catch {
23461
+ for (const file of present) verdicts.set(file, void 0);
23462
+ return verdicts;
23463
+ }
23464
+ for (const [file, alive] of parseWindowsShellHolderReport(String(report), present)) verdicts.set(file, alive);
23465
+ return verdicts;
23466
+ }
23293
23467
  const live = [];
23294
23468
  for (const file of outputFiles) {
23295
23469
  if (!existsSync5(file)) {
@@ -23305,7 +23479,7 @@ async function probeShellsAlive(outputFiles) {
23305
23479
  if (live.length === 0) return verdicts;
23306
23480
  let stdout = "";
23307
23481
  try {
23308
- stdout = (await execFileAsync("lsof", ["-F", "pn", "--", ...live.map((f) => f.real)], { encoding: "utf8", timeout: 8e3 })).stdout;
23482
+ stdout = (await exec("lsof", ["-F", "pn", "--", ...live.map((f) => f.real)], { encoding: "utf8", timeout: 8e3 })).stdout;
23309
23483
  } catch (err) {
23310
23484
  const e = err;
23311
23485
  const ran = typeof e.code === "number" && !e.killed;
@@ -23320,6 +23494,25 @@ async function probeShellsAlive(outputFiles) {
23320
23494
  for (const f of live) verdicts.set(f.requested, held.has(f.real));
23321
23495
  return verdicts;
23322
23496
  }
23497
+ function windowsShellHolderCommand(paths, env = process.env) {
23498
+ const shell = win32.join(env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
23499
+ const list = paths.map((p) => `'${p.replace(/'/g, "''")}'`).join(",");
23500
+ const script = `foreach($p in @(${list})){try{$f=[System.IO.File]::Open($p,[System.IO.FileMode]::Open,[System.IO.FileAccess]::Read,[System.IO.FileShare]::None);$f.Close();'free'}catch [System.IO.IOException]{if((($_.Exception.HResult) -band 0xFFFF) -in 32,33){'held'}else{'unknown'}}catch{'unknown'}}`;
23501
+ return [shell, ["-NoProfile", "-NonInteractive", "-NoLogo", "-Command", script]];
23502
+ }
23503
+ function parseWindowsShellHolderReport(report, paths) {
23504
+ const verdicts = /* @__PURE__ */ new Map();
23505
+ const lines = report.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
23506
+ if (lines.length !== paths.length) {
23507
+ for (const file of paths) verdicts.set(file, void 0);
23508
+ return verdicts;
23509
+ }
23510
+ paths.forEach((file, i) => {
23511
+ const word = lines[i];
23512
+ verdicts.set(file, word === "held" ? true : word === "free" ? false : void 0);
23513
+ });
23514
+ return verdicts;
23515
+ }
23323
23516
  function activityMinute(at) {
23324
23517
  if (!at) return "";
23325
23518
  const ms = Date.parse(at);
@@ -25403,18 +25596,20 @@ ${detail2}
25403
25596
  }, delay2);
25404
25597
  nudgeTimer.unref?.();
25405
25598
  }
25406
- function registeredStateIsCurrent(state) {
25599
+ function currentRowFor(state) {
25407
25600
  const current = deps.storage.getSession(state.slug);
25408
- return Boolean(
25409
- current && current.session_id === state.sessionId && (current.runtime_generation ?? 0) === state.runtimeGeneration
25410
- );
25601
+ return current && current.session_id === state.sessionId && (current.runtime_generation ?? 0) === state.runtimeGeneration ? current : void 0;
25602
+ }
25603
+ function registeredStateIsCurrent(state) {
25604
+ return currentRowFor(state) !== void 0;
25411
25605
  }
25412
25606
  return {
25413
25607
  get(slug) {
25414
25608
  const registered = states.get(slug);
25415
- const s = registered && registeredStateIsCurrent(registered) ? registered : registered ? void 0 : foreignStates.get(slug);
25609
+ const row = registered ? currentRowFor(registered) : void 0;
25610
+ const s = registered ? row ? registered : void 0 : foreignStates.get(slug);
25416
25611
  if (!s) return void 0;
25417
- const pendingQuestion = s.lastAssistantHasQuestion;
25612
+ const pendingQuestion = s.lastAssistantHasQuestion && questionFencesLive(row?.spawned_at);
25418
25613
  const nowMs = now();
25419
25614
  return { turn: s.turn, permPrompt: s.permPrompt, permPolicy: s.permPolicy, permDenies: s.permDenies, model: s.model, effort: s.effort, profileAt: s.profileAt, profileRevision: s.profileRevision, permissionMode: s.permissionMode, permissionModeAt: s.permissionModeAt, permissionModeRevision: s.permissionModeRevision, lastActivityAt: s.lastActivityAt, lastAssistantAt: s.lastAssistantAt, lastAssistant: s.lastAssistant, aiTitle: s.aiTitle, customTitle: s.customTitle, customTitleRevision: s.customTitleRevision, subAgents: subAgentViews(s, nowMs), droppedReports: [...s.queuedReports.values()], bgShells: [...bgShellViews(s), ...codexBgShellViews(s)], retiredShells: retiredShellViews(s), pendingAsk: s.pendingAsk, pendingQuestion, lastAssistantAllDone: s.lastAssistantAllDone, lastUserAt: s.lastUserAt, lastUserText: s.lastUserText, firstUserText: s.firstUserText, lastFence: s.lastFence, noTranscript: s.noTranscript, authFault: s.authFault, apiFault: s.apiFault, providerError: s.providerError, limitFault: s.limitFault, contextTokens: s.contextTokens, contextWindow: s.contextWindow, lastCompactionAt: s.lastCompactionAt };
25420
25615
  },
@@ -25979,14 +26174,15 @@ function parseToolArguments(args) {
25979
26174
  }
25980
26175
  function imageField(output) {
25981
26176
  if (!Array.isArray(output)) return {};
26177
+ const images = [];
25982
26178
  for (const part of output) {
25983
26179
  if (!part || typeof part !== "object") continue;
25984
26180
  const p = part;
25985
26181
  if (p.type !== "input_image" && p.type !== "output_image" && p.type !== "image") continue;
25986
26182
  const url = typeof p.image_url === "string" ? p.image_url : typeof p.url === "string" ? p.url : void 0;
25987
- if (url?.startsWith("data:image/")) return { image: url };
26183
+ if (url?.startsWith("data:image/")) images.push(url);
25988
26184
  }
25989
- return {};
26185
+ return images.length ? { image: images[0], images } : {};
25990
26186
  }
25991
26187
  function codexItemText(content) {
25992
26188
  if (!Array.isArray(content)) return "";
@@ -26255,6 +26451,101 @@ var init_codex = __esm({
26255
26451
  }
26256
26452
  });
26257
26453
 
26454
+ // packages/server/src/backend/codex-executable.ts
26455
+ import { accessSync as accessSync2, constants as fsConstants2, readFileSync as readFileSync14 } from "node:fs";
26456
+ import { posix, win32 as win322 } from "node:path";
26457
+ function defaultAccess(path3, mode) {
26458
+ try {
26459
+ accessSync2(path3, mode === "executable" ? fsConstants2.X_OK : fsConstants2.F_OK);
26460
+ return true;
26461
+ } catch {
26462
+ return false;
26463
+ }
26464
+ }
26465
+ function defaultReadFile(path3) {
26466
+ try {
26467
+ return readFileSync14(path3, "utf8");
26468
+ } catch {
26469
+ return void 0;
26470
+ }
26471
+ }
26472
+ function searchPath2(env) {
26473
+ const direct = env.PATH ?? env.Path ?? env.path;
26474
+ if (direct !== void 0) return direct;
26475
+ for (const [key, value] of Object.entries(env)) if (key.toLowerCase() === "path") return value ?? "";
26476
+ return "";
26477
+ }
26478
+ function vendoredNativeBeside(launcherJs, arch, access) {
26479
+ const triple = WINDOWS_TRIPLE[arch];
26480
+ if (!triple) return void 0;
26481
+ const { dirname: dirname24, join: join46 } = win322;
26482
+ const pkgRoot = dirname24(dirname24(launcherJs));
26483
+ const platformPkg = `codex-win32-${arch}`;
26484
+ const vendorRoots = [
26485
+ join46(pkgRoot, "node_modules", "@openai", platformPkg, "vendor"),
26486
+ // nested under the package
26487
+ join46(dirname24(pkgRoot), platformPkg, "vendor"),
26488
+ // hoisted beside it: <prefix>\node_modules\@openai\<pkg>
26489
+ join46(pkgRoot, "vendor")
26490
+ // the pre-platform-package layout, binaries inside @openai/codex itself
26491
+ ];
26492
+ for (const root of vendorRoots) {
26493
+ const exe = join46(root, triple, "bin", "codex.exe");
26494
+ if (access(exe, "exists")) return exe;
26495
+ }
26496
+ return void 0;
26497
+ }
26498
+ function resolveCodexExecutable(bin, deps = {}) {
26499
+ const env = deps.env ?? process.env;
26500
+ const platform3 = deps.platform ?? process.platform;
26501
+ const arch = deps.arch ?? process.arch;
26502
+ const execPath = deps.execPath ?? process.execPath;
26503
+ const access = deps.access ?? defaultAccess;
26504
+ const readFile2 = deps.readFile ?? defaultReadFile;
26505
+ const windows = platform3 === "win32";
26506
+ const path3 = windows ? win322 : posix;
26507
+ const candidate = bin && bin.length > 0 ? bin : "codex";
26508
+ if (path3.isAbsolute(candidate) || /[\\/]/u.test(candidate)) return { file: candidate, args: [] };
26509
+ for (const dir of searchPath2(env).split(path3.delimiter)) {
26510
+ if (!dir) continue;
26511
+ if (!windows) {
26512
+ const full2 = path3.join(dir, candidate);
26513
+ if (access(full2, "executable")) return { file: full2, args: [] };
26514
+ continue;
26515
+ }
26516
+ const exe = path3.join(dir, `${candidate}.exe`);
26517
+ if (access(exe, "exists")) return { file: exe, args: [] };
26518
+ const body = readFile2(path3.join(dir, `${candidate}.cmd`));
26519
+ const target = body === void 0 ? void 0 : WINDOWS_SHIM_TARGET2.exec(body)?.[1];
26520
+ if (!target) continue;
26521
+ const full = path3.join(dir, target);
26522
+ if (!access(full, "exists")) continue;
26523
+ if (!/\.(?:js|mjs|cjs)$/iu.test(full)) return { file: full, args: [] };
26524
+ const native = vendoredNativeBeside(full, arch, access);
26525
+ if (native) return { file: native, args: [] };
26526
+ return { file: execPath, args: [full] };
26527
+ }
26528
+ throw new CodexExecutableNotFoundError(candidate);
26529
+ }
26530
+ var CodexExecutableNotFoundError, WINDOWS_SHIM_TARGET2, WINDOWS_TRIPLE;
26531
+ var init_codex_executable = __esm({
26532
+ "packages/server/src/backend/codex-executable.ts"() {
26533
+ "use strict";
26534
+ CodexExecutableNotFoundError = class extends Error {
26535
+ code = "ENOENT";
26536
+ constructor(candidate) {
26537
+ super(`could not resolve '${candidate}' to an executable on PATH`);
26538
+ this.name = "CodexExecutableNotFoundError";
26539
+ }
26540
+ };
26541
+ WINDOWS_SHIM_TARGET2 = /"%dp0%\\([^"]+)"\s+%\*/u;
26542
+ WINDOWS_TRIPLE = {
26543
+ x64: "x86_64-pc-windows-msvc",
26544
+ arm64: "aarch64-pc-windows-msvc"
26545
+ };
26546
+ }
26547
+ });
26548
+
26258
26549
  // packages/server/src/backend/worker-env.ts
26259
26550
  function isFrizzInternalEnvKey(key) {
26260
26551
  return key.startsWith(FRIZZ_INTERNAL_PREFIX);
@@ -27623,14 +27914,14 @@ var require_receiver = __commonJS({
27623
27914
  * @return {(Error|RangeError)} The error
27624
27915
  * @private
27625
27916
  */
27626
- createError(ErrorCtor, message, prefix, statusCode, errorCode4) {
27917
+ createError(ErrorCtor, message, prefix, statusCode, errorCode5) {
27627
27918
  this._loop = false;
27628
27919
  this._errored = true;
27629
27920
  const err = new ErrorCtor(
27630
27921
  prefix ? `Invalid WebSocket frame: ${message}` : message
27631
27922
  );
27632
27923
  Error.captureStackTrace(err, this.createError);
27633
- err.code = errorCode4;
27924
+ err.code = errorCode5;
27634
27925
  err[kStatusCode] = statusCode;
27635
27926
  return err;
27636
27927
  }
@@ -29996,6 +30287,11 @@ function claudeCompactionEnv(settings) {
29996
30287
  if (window === void 0 || !Number.isInteger(window) || window <= 0) return {};
29997
30288
  return { [CLAUDE_AUTO_COMPACT_WINDOW_ENV]: String(window) };
29998
30289
  }
30290
+ function codexContextWindowConfig(settings) {
30291
+ const window = settings?.codexContextWindow;
30292
+ if (window === void 0 || !Number.isInteger(window) || window <= 0) return {};
30293
+ return { [CODEX_CONTEXT_WINDOW_KEY]: window };
30294
+ }
29999
30295
  function claudePromptCacheEnv(settings) {
30000
30296
  const ttl = settings?.promptCacheTtl;
30001
30297
  if (ttl !== "5m" && ttl !== "1h") return {};
@@ -30007,7 +30303,7 @@ function claudeCompactionWindowOf(env) {
30007
30303
  const window = Number(raw2);
30008
30304
  return Number.isInteger(window) && window > 0 ? window : void 0;
30009
30305
  }
30010
- var FRIZZ_MCP, WORKER_MAX_WEB_SEARCHES, WORKER_MAX_SUBAGENTS, WORKER_MAX_CONCURRENT_SUBAGENTS, CLAUDE_WORKER_ENV, CLAUDE_AUTO_COMPACT_WINDOW_ENV, CLAUDE_PROMPT_CACHE_TTL_ENV, CLAUDE_SUBAGENT_PROMPT_CACHE_TTL_ENV, WORKER_DISALLOWED_TOOLS;
30306
+ var FRIZZ_MCP, WORKER_MAX_WEB_SEARCHES, WORKER_MAX_SUBAGENTS, WORKER_MAX_CONCURRENT_SUBAGENTS, CLAUDE_WORKER_ENV, CLAUDE_AUTO_COMPACT_WINDOW_ENV, CODEX_CONTEXT_WINDOW_KEY, CLAUDE_PROMPT_CACHE_TTL_ENV, CLAUDE_SUBAGENT_PROMPT_CACHE_TTL_ENV, WORKER_DISALLOWED_TOOLS;
30011
30307
  var init_types2 = __esm({
30012
30308
  "packages/server/src/backend/types.ts"() {
30013
30309
  "use strict";
@@ -30025,6 +30321,7 @@ var init_types2 = __esm({
30025
30321
  BASH_MAX_TIMEOUT_MS: String(24 * 60 * 60 * 1e3)
30026
30322
  };
30027
30323
  CLAUDE_AUTO_COMPACT_WINDOW_ENV = "CLAUDE_CODE_AUTO_COMPACT_WINDOW";
30324
+ CODEX_CONTEXT_WINDOW_KEY = "model_context_window";
30028
30325
  CLAUDE_PROMPT_CACHE_TTL_ENV = "CLAUDE_CODE_PROMPT_CACHE_TTL";
30029
30326
  CLAUDE_SUBAGENT_PROMPT_CACHE_TTL_ENV = "CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL";
30030
30327
  WORKER_DISALLOWED_TOOLS = ["AskUserQuestion"];
@@ -30088,7 +30385,7 @@ import { spawn as spawn2 } from "node:child_process";
30088
30385
  import { connect } from "node:net";
30089
30386
  import { Agent as HttpAgent } from "node:http";
30090
30387
  import { createHash as createHash7, randomUUID as randomUUID9 } from "node:crypto";
30091
- import { existsSync as existsSync6, mkdirSync as mkdirSync11, readFileSync as readFileSync14, unlinkSync as unlinkSync2, writeFileSync as writeFileSync9 } from "node:fs";
30388
+ import { existsSync as existsSync6, mkdirSync as mkdirSync11, readFileSync as readFileSync15, unlinkSync as unlinkSync2, writeFileSync as writeFileSync9 } from "node:fs";
30092
30389
  import { join as join20 } from "node:path";
30093
30390
  import { PassThrough, Writable } from "node:stream";
30094
30391
  import { StringDecoder } from "node:string_decoder";
@@ -30112,7 +30409,7 @@ function pidAlive2(pid) {
30112
30409
  }
30113
30410
  function readNativeRecord(stateDir, projectId) {
30114
30411
  try {
30115
- const value = JSON.parse(readFileSync14(nativeRecordPath(stateDir, projectId), "utf8"));
30412
+ const value = JSON.parse(readFileSync15(nativeRecordPath(stateDir, projectId), "utf8"));
30116
30413
  if (typeof value.listenerPid !== "number" || typeof value.socketPath !== "string" || typeof value.generation !== "string") return null;
30117
30414
  return {
30118
30415
  projectId,
@@ -30282,7 +30579,10 @@ async function startListener(options) {
30282
30579
  cwd: options.cwd,
30283
30580
  env: options.env,
30284
30581
  detached: true,
30285
- stdio: "ignore"
30582
+ stdio: "ignore",
30583
+ // No console window for codex.exe on Windows (Windows audit 2026-09-11, finding 4; the story is on
30584
+ // the daemon's spawn in codex-app-server-daemon.ts).
30585
+ windowsHide: true
30286
30586
  });
30287
30587
  child.unref();
30288
30588
  const record = {
@@ -30354,7 +30654,7 @@ var init_codex_app_server_native = __esm({
30354
30654
  };
30355
30655
  }
30356
30656
  log.error("codex", `codex app-server native listener unavailable (${error.message}); falling back to an in-process app-server \u2014 turns will NOT survive a frizz restart`);
30357
- const child = spawn2(options.codexBin, codexAppServerArgv(["--stdio"], options.frizzMcp), { cwd: options.cwd, env: options.env, stdio: ["pipe", "pipe", "pipe"] });
30657
+ const child = spawn2(options.codexBin, codexAppServerArgv(["--stdio"], options.frizzMcp), { cwd: options.cwd, env: options.env, stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
30358
30658
  return { process: child, generation: randomUUID9(), reattached: false, daemonPid: process.pid, droppedWhileDetached: 0, authAccountId: options.authAccountId };
30359
30659
  }
30360
30660
  };
@@ -30362,10 +30662,10 @@ var init_codex_app_server_native = __esm({
30362
30662
  });
30363
30663
 
30364
30664
  // packages/server/src/backend/codex-app-server-host.ts
30365
- import { spawn as spawn3, spawnSync } from "node:child_process";
30665
+ import { spawn as spawn3 } from "node:child_process";
30366
30666
  import { createConnection } from "node:net";
30367
30667
  import { createHash as createHash8, randomUUID as randomUUID10 } from "node:crypto";
30368
- import { existsSync as existsSync7, mkdirSync as mkdirSync12, readFileSync as readFileSync15, unlinkSync as unlinkSync3 } from "node:fs";
30668
+ import { existsSync as existsSync7, mkdirSync as mkdirSync12, readFileSync as readFileSync16, unlinkSync as unlinkSync3 } from "node:fs";
30369
30669
  import { join as join21 } from "node:path";
30370
30670
  import { PassThrough as PassThrough2, Writable as Writable2 } from "node:stream";
30371
30671
  import { StringDecoder as StringDecoder2 } from "node:string_decoder";
@@ -30389,7 +30689,7 @@ function pidAlive3(pid) {
30389
30689
  }
30390
30690
  function readDaemonRecord(stateDir, projectId) {
30391
30691
  try {
30392
- const value = JSON.parse(readFileSync15(recordPath(stateDir, projectId), "utf8"));
30692
+ const value = JSON.parse(readFileSync16(recordPath(stateDir, projectId), "utf8"));
30393
30693
  if (typeof value.daemonPid !== "number" || typeof value.socketPath !== "string" || typeof value.generation !== "string") return null;
30394
30694
  return {
30395
30695
  projectId,
@@ -30406,7 +30706,7 @@ function readDaemonRecord(stateDir, projectId) {
30406
30706
  }
30407
30707
  function readDaemonExitBreadcrumb(stateDir, projectId) {
30408
30708
  try {
30409
- const value = JSON.parse(readFileSync15(`${recordPath(stateDir, projectId)}.exit`, "utf8"));
30709
+ const value = JSON.parse(readFileSync16(`${recordPath(stateDir, projectId)}.exit`, "utf8"));
30410
30710
  if (typeof value.generation !== "string" || typeof value.reason !== "string") return null;
30411
30711
  return {
30412
30712
  generation: value.generation,
@@ -30429,22 +30729,6 @@ function liveDaemonRecord(stateDir, projectId) {
30429
30729
  }
30430
30730
  return null;
30431
30731
  }
30432
- function endDaemonTree(pid, signal) {
30433
- if (process.platform !== "win32") {
30434
- try {
30435
- process.kill(pid, signal);
30436
- } catch {
30437
- }
30438
- return;
30439
- }
30440
- const killed = spawnSync("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
30441
- if (killed.error || killed.status !== 0) {
30442
- try {
30443
- process.kill(pid, signal);
30444
- } catch {
30445
- }
30446
- }
30447
- }
30448
30732
  function killCodexAppServerDaemon(stateDir, projectId) {
30449
30733
  const record = liveDaemonRecord(stateDir, projectId);
30450
30734
  if (!record) return;
@@ -30505,7 +30789,11 @@ function forkDaemon(options) {
30505
30789
  // the payload and is applied by the daemon, keeping the audited env allowlist authoritative.
30506
30790
  env: { ...process.env, FRIZZ_CODEX_APP_SERVER_DAEMON: payload },
30507
30791
  detached: true,
30508
- stdio: "ignore"
30792
+ stdio: "ignore",
30793
+ // Belt to the daemon's own braces: a detached process owns no console on Windows, so anything
30794
+ // console-subsystem it starts without CREATE_NO_WINDOW gets a fresh visible one (Windows audit
30795
+ // 2026-09-11, finding 4). The daemon hides its app-server; this keeps the daemon itself hidden.
30796
+ windowsHide: true
30509
30797
  });
30510
30798
  child.unref();
30511
30799
  const deadline = Date.now() + (options.timeoutMs ?? 3e4);
@@ -30610,7 +30898,11 @@ function inProcessCodexAppServer(options) {
30610
30898
  const child = spawn3(options.codexBin, codexAppServerArgv(["--stdio"], options.frizzMcp), {
30611
30899
  cwd: options.cwd,
30612
30900
  env: options.env,
30613
- stdio: ["pipe", "pipe", "pipe"]
30901
+ stdio: ["pipe", "pipe", "pipe"],
30902
+ // No console window for codex.exe (Windows audit 2026-09-11, finding 4). The server child has a
30903
+ // console of its own so this site was not the visible-window bug, but a codex.exe that inherits
30904
+ // it dies with it, and the shape test in daemon-tree.test.ts holds every provider spawn to it.
30905
+ windowsHide: true
30614
30906
  });
30615
30907
  const kill = child.kill.bind(child);
30616
30908
  child.kill = ((signal) => {
@@ -30637,6 +30929,7 @@ var init_codex_app_server_host = __esm({
30637
30929
  "packages/server/src/backend/codex-app-server-host.ts"() {
30638
30930
  "use strict";
30639
30931
  init_detached_daemons();
30932
+ init_daemon_tree();
30640
30933
  init_codex_app_server_native();
30641
30934
  init_ipc_path();
30642
30935
  init_codex_mcp();
@@ -31084,7 +31377,7 @@ var init_claude_quota = __esm({
31084
31377
  // packages/server/src/backend/auth-status.ts
31085
31378
  import { join as join23 } from "node:path";
31086
31379
  import { homedir as homedir10, platform as platform2 } from "node:os";
31087
- import { readFileSync as readFileSync16, statSync as statSync10 } from "node:fs";
31380
+ import { readFileSync as readFileSync17, statSync as statSync10 } from "node:fs";
31088
31381
  import { execFile as execFile4 } from "node:child_process";
31089
31382
  import { promisify as promisify4 } from "node:util";
31090
31383
  function claudeConfigDir2() {
@@ -31094,7 +31387,7 @@ function claudeConfigDir2() {
31094
31387
  function claudeFileState(configDir) {
31095
31388
  let raw2;
31096
31389
  try {
31097
- raw2 = readFileSync16(join23(configDir, ".credentials.json"), "utf8");
31390
+ raw2 = readFileSync17(join23(configDir, ".credentials.json"), "utf8");
31098
31391
  } catch (err) {
31099
31392
  return err.code === "ENOENT" ? "absent" : "error";
31100
31393
  }
@@ -31127,7 +31420,7 @@ function readCodexAuthState(codexHome = defaultCodexHome()) {
31127
31420
  if (process.env.OPENAI_API_KEY || process.env.CODEX_API_KEY || process.env.CODEX_ACCESS_TOKEN) return "authed";
31128
31421
  let raw2;
31129
31422
  try {
31130
- raw2 = readFileSync16(join23(codexHome, "auth.json"), "utf8");
31423
+ raw2 = readFileSync17(join23(codexHome, "auth.json"), "utf8");
31131
31424
  } catch (err) {
31132
31425
  return err.code === "ENOENT" ? "signed-out" : "unknown";
31133
31426
  }
@@ -31147,7 +31440,7 @@ function readCodexAuthState(codexHome = defaultCodexHome()) {
31147
31440
  function readCodexAccountId(codexHome = defaultCodexHome()) {
31148
31441
  if (process.env.OPENAI_API_KEY || process.env.CODEX_API_KEY || process.env.CODEX_ACCESS_TOKEN) return void 0;
31149
31442
  try {
31150
- const doc = JSON.parse(readFileSync16(join23(codexHome, "auth.json"), "utf8"));
31443
+ const doc = JSON.parse(readFileSync17(join23(codexHome, "auth.json"), "utf8"));
31151
31444
  if (!doc || typeof doc !== "object") return void 0;
31152
31445
  const tokens = doc.tokens;
31153
31446
  if (!tokens || typeof tokens !== "object") return void 0;
@@ -31157,9 +31450,10 @@ function readCodexAccountId(codexHome = defaultCodexHome()) {
31157
31450
  return void 0;
31158
31451
  }
31159
31452
  }
31160
- async function readCodexBinaryState(codexBin = "codex", exec = execFileAsync3) {
31453
+ async function readCodexBinaryState(codexBin = "codex", exec = execFileAsync3, resolve12 = resolveCodexExecutable) {
31161
31454
  try {
31162
- await exec(codexBin, ["--version"], { timeout: 5e3 });
31455
+ const codex = resolve12(codexBin);
31456
+ await exec(codex.file, [...codex.args, "--version"], { timeout: 5e3 });
31163
31457
  return "present";
31164
31458
  } catch (err) {
31165
31459
  return err.code === "ENOENT" ? "missing" : "unknown";
@@ -31218,7 +31512,7 @@ function readClaudeAccountEmail(path3 = claudeAccountFile()) {
31218
31512
  if (memo3 && memo3.path === path3 && memo3.mtimeMs === stat2.mtimeMs && memo3.size === stat2.size) return memo3.email;
31219
31513
  let email;
31220
31514
  try {
31221
- const doc = JSON.parse(readFileSync16(path3, "utf8"));
31515
+ const doc = JSON.parse(readFileSync17(path3, "utf8"));
31222
31516
  email = asEmail(doc?.oauthAccount?.emailAddress);
31223
31517
  } catch {
31224
31518
  email = void 0;
@@ -31229,7 +31523,7 @@ function readClaudeAccountEmail(path3 = claudeAccountFile()) {
31229
31523
  function readCodexAccountEmail(codexHome = defaultCodexHome()) {
31230
31524
  let doc;
31231
31525
  try {
31232
- doc = JSON.parse(readFileSync16(join23(codexHome, "auth.json"), "utf8"));
31526
+ doc = JSON.parse(readFileSync17(join23(codexHome, "auth.json"), "utf8"));
31233
31527
  } catch {
31234
31528
  return void 0;
31235
31529
  }
@@ -31284,6 +31578,7 @@ var init_auth_status = __esm({
31284
31578
  init_claude_quota();
31285
31579
  init_codex();
31286
31580
  init_claude_broker_host();
31581
+ init_codex_executable();
31287
31582
  execFileAsync3 = promisify4(execFile4);
31288
31583
  ProviderAuthRequiredError = class extends Error {
31289
31584
  backend;
@@ -31624,7 +31919,7 @@ function fileChangeDisplays(changes) {
31624
31919
  }
31625
31920
  function canonicalJson2(value) {
31626
31921
  const visiting = /* @__PURE__ */ new WeakSet();
31627
- const normalize3 = (candidate) => {
31922
+ const normalize2 = (candidate) => {
31628
31923
  if (candidate === null || typeof candidate === "string" || typeof candidate === "boolean") return candidate;
31629
31924
  if (typeof candidate === "number") {
31630
31925
  if (!Number.isFinite(candidate)) throw new Error("non-finite JSON number");
@@ -31634,14 +31929,14 @@ function canonicalJson2(value) {
31634
31929
  if (visiting.has(candidate)) throw new Error("cyclic JSON value");
31635
31930
  visiting.add(candidate);
31636
31931
  try {
31637
- if (Array.isArray(candidate)) return candidate.map((item) => item === void 0 ? null : normalize3(item));
31932
+ if (Array.isArray(candidate)) return candidate.map((item) => item === void 0 ? null : normalize2(item));
31638
31933
  const object = candidate;
31639
- return Object.fromEntries(Object.keys(object).sort().filter((key) => object[key] !== void 0).map((key) => [key, normalize3(object[key])]));
31934
+ return Object.fromEntries(Object.keys(object).sort().filter((key) => object[key] !== void 0).map((key) => [key, normalize2(object[key])]));
31640
31935
  } finally {
31641
31936
  visiting.delete(candidate);
31642
31937
  }
31643
31938
  };
31644
- return JSON.stringify(normalize3(value));
31939
+ return JSON.stringify(normalize2(value));
31645
31940
  }
31646
31941
  function logicalRequestId(method, parts) {
31647
31942
  const digest = createHash10("sha256").update(JSON.stringify([method, ...parts])).digest("hex");
@@ -31957,6 +32252,7 @@ var init_codex_app_server = __esm({
31957
32252
  init_auth_status();
31958
32253
  init_codex_mcp();
31959
32254
  init_codex_error();
32255
+ init_types2();
31960
32256
  init_logging();
31961
32257
  CODEX_APP_SERVER_PROVIDER = "codex-app-server";
31962
32258
  CODEX_NATIVE_LISTEN_FLAG = "FRIZZ_CODEX_NATIVE_LISTEN";
@@ -33624,19 +33920,29 @@ var init_codex_app_server = __esm({
33624
33920
  */
33625
33921
  /**
33626
33922
  * The `config` bag for one thread's `thread/start` / `thread/resume`: frizz's MCP server mounted
33627
- * PER THREAD so it knows who is calling, plus whatever the caller passed.
33923
+ * PER THREAD so it knows who is calling, the operator's Codex context window from Settings, plus
33924
+ * whatever the caller passed.
33628
33925
  *
33629
33926
  * This is the only channel that can carry a caller identity on codex. The argv mount on the
33630
33927
  * app-server is process-wide and serves every thread in the project, so `FRIZZ_THREAD_SLUG` was
33631
33928
  * simply absent and every tool that acts on the caller's own thread failed at the moment of use —
33632
33929
  * see `codexThreadMcpConfig` for the list and the measurement. Returns `{}` when there is nothing
33633
- * to send, so a caller with no config and no resolved MCP descriptor sends no `config` key at all,
33634
- * exactly as before.
33930
+ * to send, so a caller with no config, no resolved MCP descriptor and no context-window setting
33931
+ * sends no `config` key at all, exactly as before.
33932
+ *
33933
+ * The context window rides the same bag rather than the process-wide argv because the app-server is
33934
+ * a long-lived daemon: an argv value would be frozen at its first spawn, while a per-thread override
33935
+ * reads Settings at every start and every cold resume — the same "next thread, and a thread that
33936
+ * comes back" semantics the Claude compaction window has (see codexContextWindowConfig).
33635
33937
  *
33636
33938
  * The caller's own config wins on a key collision: this is a default, not an override.
33637
33939
  */
33638
33940
  threadConfig(threadSlug, callerConfig) {
33639
- const config = { ...codexThreadMcpConfig(this.options.frizzMcp, threadSlug), ...callerConfig };
33941
+ const config = {
33942
+ ...codexThreadMcpConfig(this.options.frizzMcp, threadSlug),
33943
+ ...codexContextWindowConfig(this.options.getSettings?.()),
33944
+ ...callerConfig
33945
+ };
33640
33946
  return Object.keys(config).length ? { config } : {};
33641
33947
  }
33642
33948
  resumeSandboxOverride(row) {
@@ -34522,9 +34828,12 @@ function queryCodexRateLimits(codexHome = defaultCodexHome(), codexBin = "codex"
34522
34828
  return new Promise((resolve12) => {
34523
34829
  let child;
34524
34830
  try {
34525
- child = spawn4(codexBin, ["app-server"], {
34831
+ const codex = resolveCodexExecutable(codexBin);
34832
+ child = spawn4(codex.file, [...codex.args, "app-server"], {
34526
34833
  stdio: ["pipe", "pipe", "ignore"],
34527
- env: { ...process.env, CODEX_HOME: codexHome }
34834
+ env: { ...process.env, CODEX_HOME: codexHome },
34835
+ // No console window for codex.exe (Windows audit 2026-09-11, finding 4).
34836
+ windowsHide: true
34528
34837
  });
34529
34838
  } catch {
34530
34839
  resolve12(void 0);
@@ -34604,6 +34913,7 @@ var init_codex_quota = __esm({
34604
34913
  "packages/server/src/backend/codex-quota.ts"() {
34605
34914
  "use strict";
34606
34915
  init_codex();
34916
+ init_codex_executable();
34607
34917
  init_codex_app_server();
34608
34918
  descByName2 = (a, b) => a < b ? 1 : a > b ? -1 : 0;
34609
34919
  TAIL_BYTES = 512 * 1024;
@@ -34681,22 +34991,181 @@ var init_quota = __esm({
34681
34991
  }
34682
34992
  });
34683
34993
 
34994
+ // packages/server/src/local-file.ts
34995
+ import { spawn as spawn5 } from "node:child_process";
34996
+ import { existsSync as existsSync8, readFileSync as readFileSync18, realpathSync as realpathSync6, statSync as statSync12 } from "node:fs";
34997
+ import { homedir as homedir11 } from "node:os";
34998
+ import { isAbsolute as isAbsolute4, join as join25, resolve as resolve5, sep as sep2 } from "node:path";
34999
+ function isUnder(real, root) {
35000
+ let rootReal;
35001
+ try {
35002
+ rootReal = realpathSync6(root);
35003
+ } catch {
35004
+ return false;
35005
+ }
35006
+ return real === rootReal || real.startsWith(rootReal.endsWith(sep2) ? rootReal : rootReal + sep2);
35007
+ }
35008
+ function resolveLocalFile(rawPath, roots) {
35009
+ if (!isAbsolute4(rawPath)) throw new Error("Local path must be absolute");
35010
+ let real;
35011
+ try {
35012
+ real = realpathSync6(rawPath);
35013
+ } catch {
35014
+ throw new Error("Local file was not found");
35015
+ }
35016
+ if (!roots.some((root) => isUnder(real, root))) throw new Error("Local file is outside Frizz's trusted roots");
35017
+ try {
35018
+ if (!statSync12(real).isFile()) throw new Error("Local path is not a regular file");
35019
+ } catch (error) {
35020
+ if (error instanceof Error && error.message === "Local path is not a regular file") throw error;
35021
+ throw new Error("Local file was not found");
35022
+ }
35023
+ return real;
35024
+ }
35025
+ function resolveOpenableFile(raw2, projectDir, roots, home = homedir11()) {
35026
+ const trimmed = raw2.trim().replace(/:\d+(?::\d+)?$/, "");
35027
+ if (!trimmed) return null;
35028
+ const abs = trimmed === "~" ? home : /^~[\/\\]/.test(trimmed) ? join25(home, trimmed.slice(2)) : isAbsolute4(trimmed) ? trimmed : resolve5(projectDir, trimmed);
35029
+ try {
35030
+ return resolveLocalFile(abs, roots);
35031
+ } catch {
35032
+ return null;
35033
+ }
35034
+ }
35035
+ function readLocalTextFile(rawPath, roots) {
35036
+ const path3 = resolveLocalFile(rawPath, roots);
35037
+ const bytes = readFileSync18(path3);
35038
+ if (bytes.subarray(0, 8192).includes(0)) throw new Error("Local file is not a text file");
35039
+ if (bytes.length <= MARKDOWN_READ_LIMIT) return { path: path3, text: bytes.toString("utf8"), truncated: false };
35040
+ const head = bytes.subarray(0, MARKDOWN_READ_LIMIT);
35041
+ const lastBreak = head.lastIndexOf(10);
35042
+ return { path: path3, text: head.subarray(0, lastBreak > 0 ? lastBreak : head.length).toString("utf8"), truncated: true };
35043
+ }
35044
+ function readLocalMarkdown(rawPath, roots) {
35045
+ if (!MARKDOWN_FILE_EXT.test(rawPath.trim())) throw new Error("Local file is not a Markdown file");
35046
+ const path3 = resolveLocalFile(rawPath, roots);
35047
+ if (!MARKDOWN_FILE_EXT.test(path3)) throw new Error("Local file is not a Markdown file");
35048
+ const bytes = readFileSync18(path3);
35049
+ if (bytes.length <= MARKDOWN_READ_LIMIT) return { path: path3, markdown: bytes.toString("utf8"), truncated: false };
35050
+ const head = bytes.subarray(0, MARKDOWN_READ_LIMIT);
35051
+ const lastBreak = head.lastIndexOf(10);
35052
+ return { path: path3, markdown: head.subarray(0, lastBreak > 0 ? lastBreak : head.length).toString("utf8"), truncated: true };
35053
+ }
35054
+ function resolveWatchableLocalFile(rawPath, roots) {
35055
+ const path3 = resolveLocalFile(rawPath, roots);
35056
+ if (MARKDOWN_FILE_EXT.test(rawPath.trim()) && !MARKDOWN_FILE_EXT.test(path3)) {
35057
+ throw new Error("Local file is not a Markdown file");
35058
+ }
35059
+ return path3;
35060
+ }
35061
+ function defaultSpawn(command, args, options) {
35062
+ return spawn5(command, [...args], options);
35063
+ }
35064
+ async function awaitOpenerStart(child, command) {
35065
+ child.unref();
35066
+ await new Promise((resolve12, reject) => {
35067
+ child.once("error", (error) => reject(new Error(
35068
+ error.code === "ENOENT" ? `${command} is not installed or not on PATH` : `could not start ${command}: ${error.message}`
35069
+ )));
35070
+ child.once("spawn", resolve12);
35071
+ });
35072
+ }
35073
+ function localFileOpenCommand(path3, selected, options = {}) {
35074
+ const platform3 = options.platform ?? process.platform;
35075
+ if (platform3 === "darwin") {
35076
+ return selected === "cursor" ? { command: "open", args: ["-a", "Cursor", path3] } : selected === "vscode" ? { command: "open", args: ["-a", "Visual Studio Code", path3] } : selected === "finder" ? { command: "open", args: ["-R", path3] } : { command: "open", args: [path3] };
35077
+ }
35078
+ if (platform3 === "win32") {
35079
+ if (selected === "system") return { command: "explorer.exe", args: [path3] };
35080
+ if (selected === "finder") return { command: "explorer.exe", args: [`/select,${path3}`] };
35081
+ const editor = WINDOWS_EDITOR_EXE[selected];
35082
+ const env = options.env ?? process.env;
35083
+ const exists = options.exists ?? existsSync8;
35084
+ const localAppData = env.LOCALAPPDATA;
35085
+ if (localAppData) {
35086
+ const exe = join25(localAppData, ...editor.exe);
35087
+ if (exists(exe)) return { command: exe, args: [path3] };
35088
+ }
35089
+ if (/["%\r\n]/u.test(path3)) throw new Error(`cannot hand ${path3} to the ${editor.shim} shim through cmd.exe`);
35090
+ return { command: "cmd.exe", args: ["/d", "/s", "/c", `"${editor.shim} "${path3}""`], verbatim: true };
35091
+ }
35092
+ return selected === "cursor" ? { command: "cursor", args: [path3] } : selected === "vscode" ? { command: "code", args: [path3] } : { command: "xdg-open", args: [path3] };
35093
+ }
35094
+ async function openLocalFile(rawPath, opener, roots, options = {}) {
35095
+ const path3 = resolveLocalFile(rawPath, roots);
35096
+ const selected = options.forceSystem ? "system" : opener;
35097
+ if (selected === "copy") return { action: "copy", path: path3 };
35098
+ const spec = localFileOpenCommand(path3, selected, options);
35099
+ const child = (options.spawn ?? defaultSpawn)(spec.command, spec.args, {
35100
+ detached: true,
35101
+ stdio: "ignore",
35102
+ shell: false,
35103
+ windowsHide: true,
35104
+ ...spec.verbatim ? { windowsVerbatimArguments: true } : {}
35105
+ });
35106
+ await awaitOpenerStart(child, spec.command);
35107
+ return { action: "opened", path: path3 };
35108
+ }
35109
+ var MARKDOWN_FILE_EXT, MARKDOWN_READ_LIMIT, WINDOWS_EDITOR_EXE;
35110
+ var init_local_file = __esm({
35111
+ "packages/server/src/local-file.ts"() {
35112
+ "use strict";
35113
+ MARKDOWN_FILE_EXT = /\.(?:md|mdx|markdown)$/i;
35114
+ MARKDOWN_READ_LIMIT = 1024 * 1024;
35115
+ WINDOWS_EDITOR_EXE = {
35116
+ cursor: { exe: ["Programs", "cursor", "Cursor.exe"], shim: "cursor" },
35117
+ vscode: { exe: ["Programs", "Microsoft VS Code", "Code.exe"], shim: "code" }
35118
+ };
35119
+ }
35120
+ });
35121
+
35122
+ // packages/server/src/thread-links.ts
35123
+ import { isAbsolute as isAbsolute5 } from "node:path";
35124
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
35125
+ function threadLinkView(row) {
35126
+ return { id: row.id, kind: row.kind, label: row.label, target: row.target };
35127
+ }
35128
+ function resolveThreadLink(target, projectDir, roots) {
35129
+ if (/^https?:\/\//i.test(target)) {
35130
+ const url = new URL(target);
35131
+ if (url.username || url.password) throw new Error("Link URLs must not contain credentials");
35132
+ return { kind: "link", target: url.href };
35133
+ }
35134
+ let path3 = target;
35135
+ if (/^file:/i.test(target)) {
35136
+ const url = new URL(target);
35137
+ if (url.search || url.hash) throw new Error("File links must not contain a query or fragment");
35138
+ path3 = fileURLToPath2(url);
35139
+ } else if (!isAbsolute5(target) && /^[a-z][a-z\d+.-]*:/i.test(target)) {
35140
+ throw new Error("Register an HTTP(S) URL or a local file");
35141
+ }
35142
+ const resolved = resolveOpenableFile(path3, projectDir, roots);
35143
+ if (!resolved) throw new Error("Local file was not found or is outside Frizz's trusted roots");
35144
+ return { kind: "file", target: resolved };
35145
+ }
35146
+ var init_thread_links = __esm({
35147
+ "packages/server/src/thread-links.ts"() {
35148
+ "use strict";
35149
+ init_local_file();
35150
+ }
35151
+ });
35152
+
34684
35153
  // packages/server/src/frizz.ts
34685
35154
  import { execFile as execFile5 } from "node:child_process";
34686
- import { lstatSync as lstatSync2, realpathSync as realpathSync6 } from "node:fs";
34687
- import { basename as basename5, dirname as dirname9, join as join25, resolve as resolve5 } from "node:path";
35155
+ import { lstatSync as lstatSync2, realpathSync as realpathSync7 } from "node:fs";
35156
+ import { basename as basename5, dirname as dirname9, join as join26, resolve as resolve6 } from "node:path";
34688
35157
  import { promisify as promisify5 } from "node:util";
34689
35158
  function frizzScriptsDir() {
34690
35159
  if (process.env.FRIZZ_SCRIPTS_DIR) return process.env.FRIZZ_SCRIPTS_DIR;
34691
- return resolve5(import.meta.dirname, "..", "..", "..", "board");
35160
+ return resolve6(import.meta.dirname, "..", "..", "..", "board");
34692
35161
  }
34693
35162
  function directFrizzRoot(projectDir) {
34694
35163
  try {
34695
- const projectRoot = realpathSync6(projectDir);
34696
- const path3 = join25(projectRoot, ".frizz");
35164
+ const projectRoot = realpathSync7(projectDir);
35165
+ const path3 = join26(projectRoot, ".frizz");
34697
35166
  const stat2 = lstatSync2(path3);
34698
35167
  if (!stat2.isDirectory() || stat2.isSymbolicLink()) return null;
34699
- const real = realpathSync6(path3);
35168
+ const real = realpathSync7(path3);
34700
35169
  return dirname9(real) === projectRoot && basename5(real) === ".frizz" ? real : null;
34701
35170
  } catch {
34702
35171
  return null;
@@ -34707,7 +35176,7 @@ function frizzDirExists(projectDir) {
34707
35176
  }
34708
35177
  async function readBoard(projectDir, scriptsDir = frizzScriptsDir()) {
34709
35178
  if (!directFrizzRoot(projectDir)) throw new Error("unsafe or missing .frizz directory");
34710
- const { stdout } = await execFileP("node", [join25(scriptsDir, "index.mjs"), "--json"], {
35179
+ const { stdout } = await execFileP("node", [join26(scriptsDir, "index.mjs"), "--json"], {
34711
35180
  cwd: projectDir,
34712
35181
  env: { ...process.env, CLAUDE_PROJECT_DIR: projectDir },
34713
35182
  maxBuffer: 32 * 1024 * 1024
@@ -34732,7 +35201,7 @@ async function readBoard(projectDir, scriptsDir = frizzScriptsDir()) {
34732
35201
  };
34733
35202
  }
34734
35203
  async function runThreadUpdate(projectDir, slug, args, scriptsDir = frizzScriptsDir()) {
34735
- await execFileP("node", [join25(scriptsDir, "thread-update.mjs"), slug, ...args], {
35204
+ await execFileP("node", [join26(scriptsDir, "thread-update.mjs"), slug, ...args], {
34736
35205
  cwd: projectDir,
34737
35206
  env: { ...process.env, CLAUDE_PROJECT_DIR: projectDir }
34738
35207
  });
@@ -34817,12 +35286,12 @@ var init_awaiting = __esm({
34817
35286
  });
34818
35287
 
34819
35288
  // packages/server/src/backend/project-mcp-servers.ts
34820
- import { readFileSync as readFileSync17 } from "node:fs";
34821
- import { homedir as homedir11 } from "node:os";
34822
- import { join as join26 } from "node:path";
35289
+ import { readFileSync as readFileSync19 } from "node:fs";
35290
+ import { homedir as homedir12 } from "node:os";
35291
+ import { join as join27 } from "node:path";
34823
35292
  function readJsonObject(path3) {
34824
35293
  try {
34825
- const parsed = JSON.parse(readFileSync17(path3, "utf8"));
35294
+ const parsed = JSON.parse(readFileSync19(path3, "utf8"));
34826
35295
  return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
34827
35296
  } catch {
34828
35297
  return void 0;
@@ -34913,17 +35382,17 @@ function userScopeRemoteServers(account, cwd, env) {
34913
35382
  }
34914
35383
  function projectMcpServers(cwd, options = {}) {
34915
35384
  const env = options.env ?? process.env;
34916
- const home = options.home ?? homedir11();
35385
+ const home = options.home ?? homedir12();
34917
35386
  const configOverride = options.claudeConfigDir ?? (env.CLAUDE_CONFIG_DIR?.trim() ? env.CLAUDE_CONFIG_DIR.trim() : void 0);
34918
- const accountFile = configOverride ? join26(configOverride, ".claude.json") : join26(home, ".claude.json");
34919
- const userSettingsFile = join26(configOverride ?? join26(home, ".claude"), "settings.json");
35387
+ const accountFile = configOverride ? join27(configOverride, ".claude.json") : join27(home, ".claude.json");
35388
+ const userSettingsFile = join27(configOverride ?? join27(home, ".claude"), "settings.json");
34920
35389
  const account = readJsonObject(accountFile);
34921
35390
  const out = userScopeRemoteServers(account, cwd, env);
34922
- const declared = objectField(readJsonObject(join26(cwd, ".mcp.json")), "mcpServers");
35391
+ const declared = objectField(readJsonObject(join27(cwd, ".mcp.json")), "mcpServers");
34923
35392
  if (!declared) return out;
34924
35393
  const approval = collectApproval([
34925
- readJsonObject(join26(cwd, ".claude", "settings.json")),
34926
- readJsonObject(join26(cwd, ".claude", "settings.local.json")),
35394
+ readJsonObject(join27(cwd, ".claude", "settings.json")),
35395
+ readJsonObject(join27(cwd, ".claude", "settings.local.json")),
34927
35396
  readJsonObject(userSettingsFile),
34928
35397
  objectField(objectField(account, "projects"), cwd)
34929
35398
  ]);
@@ -34947,17 +35416,17 @@ var init_project_mcp_servers = __esm({
34947
35416
  });
34948
35417
 
34949
35418
  // packages/server/src/worker-plugin-dir.ts
34950
- import { existsSync as existsSync8 } from "node:fs";
34951
- import { dirname as dirname10, join as join27 } from "node:path";
34952
- import { fileURLToPath as fileURLToPath2 } from "node:url";
35419
+ import { existsSync as existsSync9 } from "node:fs";
35420
+ import { dirname as dirname10, join as join28 } from "node:path";
35421
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
34953
35422
  function resolveWorkerPluginDir(moduleUrl = import.meta.url, env = process.env) {
34954
35423
  const override = env.FRIZZ_WORKER_PLUGIN_DIR;
34955
- if (override && existsSync8(join27(override, ".claude-plugin", "plugin.json")))
35424
+ if (override && existsSync9(join28(override, ".claude-plugin", "plugin.json")))
34956
35425
  return override;
34957
- let current = dirname10(fileURLToPath2(moduleUrl));
35426
+ let current = dirname10(fileURLToPath3(moduleUrl));
34958
35427
  for (; ; ) {
34959
- const candidate = join27(current, "cc-worker");
34960
- if (existsSync8(join27(candidate, ".claude-plugin", "plugin.json"))) return candidate;
35428
+ const candidate = join28(current, "cc-worker");
35429
+ if (existsSync9(join28(candidate, ".claude-plugin", "plugin.json"))) return candidate;
34961
35430
  const parent = dirname10(current);
34962
35431
  if (parent === current) return void 0;
34963
35432
  current = parent;
@@ -34985,6 +35454,7 @@ function buildWorkerPrompt(kind = "claude", opts = {}) {
34985
35454
  lean ? null : THREAD_EXECUTION[kind],
34986
35455
  AGENT_COMPLETION,
34987
35456
  VISUAL_EVIDENCE,
35457
+ REGISTERED_LINKS,
34988
35458
  QUALITY_BAR,
34989
35459
  TOKEN_ECONOMY,
34990
35460
  QUESTIONS,
@@ -34996,7 +35466,7 @@ function buildWorkerPrompt(kind = "claude", opts = {}) {
34996
35466
  out = out.replaceAll("{{FRIZZ_MONITORS_DIR}}", opts.monitorsDir?.trim() || MONITORS_DIR_FALLBACK);
34997
35467
  return out;
34998
35468
  }
34999
- var INLINE, INTRO, DEFER, OPENING, ACTIVITY_CAPTIONS, SIGNALS, AGENT_COMPLETION, VISUAL_EVIDENCE, QUALITY_BAR, TOKEN_ECONOMY, QUESTIONS, STOP_CRITERION, TRIVIAL_PROMPTS, THREAD_NAME, SCRATCHPAD, BACKEND, SPAWN_THREAD, THREAD_EXECUTION, MONITORS_DIR_FALLBACK;
35469
+ var INLINE, INTRO, DEFER, OPENING, ACTIVITY_CAPTIONS, SIGNALS, AGENT_COMPLETION, VISUAL_EVIDENCE, REGISTERED_LINKS, QUALITY_BAR, TOKEN_ECONOMY, QUESTIONS, STOP_CRITERION, TRIVIAL_PROMPTS, THREAD_NAME, SCRATCHPAD, BACKEND, SPAWN_THREAD, THREAD_EXECUTION, MONITORS_DIR_FALLBACK;
35000
35470
  var init_workerPrompt = __esm({
35001
35471
  "packages/server/src/workerPrompt.ts"() {
35002
35472
  "use strict";
@@ -35073,9 +35543,11 @@ nothing. Frizz bumps you for one, twice, then gives up \u2014 a turn wasted each
35073
35543
  \`mcp__frizz__ask\` and \`mcp__frizz__watch\` each record a ROW, and frizz reads all three as a sign-off
35074
35544
  \u2014 it will not bump you for a missing fence after any of them. A fence is a sentence with the lifetime
35075
35545
  of the message carrying it, so it has to be rewritten at every rest and is wrong the moment anything
35076
- changes; a row survives your turn ending, a compaction and a frizz restart. **The fences below all still
35077
- work**, they are still the right shape for the prose the human reads, and nothing is being taken away \u2014
35078
- but where a verb exists for what you are saying, call it.
35546
+ changes; a row survives your turn ending, a compaction and a frizz restart. **The \`done\` and
35547
+ \`awaiting\` fences below still work**, they are still the right shape for the prose the human reads \u2014
35548
+ but where a verb exists for what you are saying, call it. **A QUESTION HAS NO FENCE ANY MORE** (retired
35549
+ 2026-09-11): the only way to ask is \`mcp__frizz__ask\`, and a question written into a fence's body is
35550
+ plain prose \u2014 no card, no answer, and no sign-off.
35079
35551
 
35080
35552
  **AN OPEN QUESTION IS A STANDING SIGN-OFF \u2014 REST NORMALLY.** Any unanswered question counts, not only
35081
35553
  one you registered this turn, and frizz does not bump you for a missing fence while one is open. When
@@ -35089,8 +35561,9 @@ without it. Name what is still running in the prose (frizz lists every live shel
35089
35561
  watcher under the prompt box whether or not a fence names it). A question you no longer need answered
35090
35562
  is one you \`unask\`; only then can a park take.
35091
35563
 
35092
- Use at most ONE fenced signal block, at the very END. The fence language is the state; the body is the
35093
- card the human reads. A \` \`\`\`question \` block and real permission prompts are higher-priority asks
35564
+ Use at most ONE fenced signal block, at the very END (a question's PLACEMENT marker is not a signal
35565
+ block \u2014 see Questions for the human). The fence language is the state; the body is the card the human
35566
+ reads. An open registered question and real permission prompts are higher-priority asks
35094
35567
  than \`done\` or \`awaiting\`.
35095
35568
 
35096
35569
  **NAME THE PULL REQUEST, EVERY TIME YOU REST** \u2014 its number in EVERY resting message, not just the
@@ -35141,7 +35614,8 @@ exactly ONE of them.
35141
35614
  - **A RECOMMENDATION IS NOT A CONCLUSION, AND AN UNSENT DRAFT IS NOT A DELIVERABLE.** When the
35142
35615
  verdict is that SOMEONE SHOULD NOW DO SOMETHING \u2014 merge it, decline it, post this comment, pick
35143
35616
  one of these designs, press the button you are not allowed to press \u2014 that someone is the human,
35144
- so end on a \` \`\`\`question \` carrying your recommendation as option A. Same for anything you WROTE
35617
+ so register the question with \`mcp__frizz__ask\`, your recommendation as option A, and rest on
35618
+ it. Same for anything you WROTE
35145
35619
  but did not SEND (a drafted comment, reply, issue body or release note): \`done\` files that draft
35146
35620
  away with the thread.
35147
35621
  - Follow-up work you DISCOVERED blocks \`done\` just as hard as work you were assigned, even when it
@@ -35227,8 +35701,9 @@ exactly ONE of them.
35227
35701
  dimmed Snoozed band; any one dead, unknown or not yours \u21D2 you are BUMPED immediately and told which.
35228
35702
  **A FENCE THAT NAMES NOTHING IS NOT A PARK** \u2014 \`for:\` describes a wait, it is not one \u2014 so
35229
35703
  "waiting on the checks" plus a duration just sits in the queue: REGISTER the PR and name it, and
35230
- if nothing is actually running, end with \`\`\`done or ask a \`\`\`question.
35231
- - **WAITING ON A PERSON IS A \` \`\`\`question \`.** There is no human gate and no prose park.
35704
+ if nothing is actually running, end with \`\`\`done or register a question with \`mcp__frizz__ask\`.
35705
+ - **WAITING ON A PERSON IS A REGISTERED QUESTION** \u2014 \`mcp__frizz__ask\`. There is no human gate, no
35706
+ prose park, and no question fence.
35232
35707
  - **CI, RELEASES, DEPLOYS AND MERGE PROGRESSION ARE AUTOMATABLE \u2014 never \` \`\`\`awaiting \` them
35233
35708
  BLINDLY.** For a pull request, \`mcp__frizz__watch_pr\`. For anything else stay ACTIVE: dispatch a
35234
35709
  sub-agent to own the wait (its return re-invokes you), or set a timer and name it here.
@@ -35239,13 +35714,14 @@ exactly ONE of them.
35239
35714
  is still running, and emit a FRESH fence naming it. If it turns out to be automatable, arm the
35240
35715
  active wait instead and do not fence.
35241
35716
 
35242
- - \` \`\`\`question \` \u2014 you need the human's input; see **Questions for the human**.
35717
+ - A REGISTERED QUESTION \u2014 \`mcp__frizz__ask\`, not a fence: you need the human's input, and the open
35718
+ row IS the handback; see **Questions for the human**.
35243
35719
 
35244
35720
  ### When the work is finished but the thread found more
35245
35721
 
35246
35722
  Your mandate is complete and you turned up something else \u2014 a bug you were not sent for, a follow-up
35247
35723
  the change implies. \`done\` is barred (the finding dies with the card), \`awaiting\` is barred (nothing
35248
- is running), and \`question\` only looks barred. That deadlock is not real, and stretching \`done\` is not
35724
+ is running), and asking only looks barred. That deadlock is not real, and stretching \`done\` is not
35249
35725
  how you break it. Take the first exit that fits:
35250
35726
 
35251
35727
  1. **DO IT \u2014 the default, and it covers far more than "small".** Anything inside your change's own
@@ -35254,10 +35730,10 @@ how you break it. Take the first exit that fits:
35254
35730
  is more than one sitting, dispatch an in-session
35255
35731
  SUB-AGENT: its result comes BACK to you, so the follow-up lands on YOUR card, in your diff, under one
35256
35732
  review. Then \`done\` is honest with nothing left over.
35257
- 2. **ASK** \u2014 the human should choose whether or how it happens; "nothing was pending" is not an
35258
- objection. This is the exit for work you cannot bound or verify, or that is genuinely the human's
35259
- call \u2014 and it is where a separable effort goes, as an OPTION they pick, rather than a card you
35260
- create for them.
35733
+ 2. **ASK** \u2014 \`mcp__frizz__ask\`: the human should choose whether or how it happens; "nothing was
35734
+ pending" is not an objection. This is the exit for work you cannot bound or verify, or that is
35735
+ genuinely the human's call \u2014 and it is where a separable effort goes, as an OPTION they pick, rather
35736
+ than a card you create for them.
35261
35737
  3. **HAND IT OFF TO ITS OWN CARD \u2014 the LAST resort, not the tidy one.** \`mcp__frizz__spawn_thread\` puts
35262
35738
  a separable effort on the board as its own thread; put the returned \`[title](/thread/<slug>)\` link
35263
35739
  in the body. **A spawned thread is FIRE-AND-FORGET: it reports to the HUMAN, never to you, so
@@ -35288,6 +35764,12 @@ image paths through its guarded local-image proxy; only eligible workspace or ex
35288
35764
  image files can embed, and a path outside that safe boundary stays non-navigable. Do not bulk-embed
35289
35765
  irrelevant screenshots. Always keep a concise textual finding alongside them, so the handoff still
35290
35766
  reads when images are unavailable.`;
35767
+ REGISTERED_LINKS = `## Saved links and files
35768
+
35769
+ Use \`mcp__frizz__link\` with \`label\` and \`target\` for a dev server, working document, report or download
35770
+ the human should keep at hand. It stays underneath the prompt. Reuse the label to update its destination;
35771
+ read registrations with \`activity\` and remove one with \`unlink\`. Register existing files, using an
35772
+ absolute path outside the project root. A saved reference is not a running process or a completion gate.`;
35291
35773
  QUALITY_BAR = `## Quality bar
35292
35774
 
35293
35775
  - Verify behavior end-to-end before calling anything done. A green suite over a stubbed implementation
@@ -35319,88 +35801,101 @@ The API call is the unit of cost, not the tokens you type: each message re-reads
35319
35801
  do not rest on a step you could take now.`;
35320
35802
  QUESTIONS = `## Questions for the human
35321
35803
 
35322
- You run under a dashboard, not a live chat, so your FINAL MESSAGE is the whole interface. Open with 2-4
35323
- sentences of status (what's done, what's in flight, what this answer unblocks), then put each question
35324
- in its own fenced \`question\` block \u2014 the dashboard renders them as answerable cards:
35804
+ You run under a dashboard, not a live chat, so a question is a ROW the human still owes an answer to,
35805
+ never a sentence in a message: \`mcp__frizz__ask\` registers it, and the dashboard renders it as an
35806
+ answerable card. **THERE IS NO \`question\` FENCE ANY MORE.** A fence with a question written in its body
35807
+ is plain prose \u2014 it draws no card, nothing can answer it, and it is not a sign-off. (Retired 2026-09-11:
35808
+ the fence was taught beside the tool for two weeks, workers went on writing it on most days, and nothing
35809
+ tracks a fence's answer \u2014 a fence is bytes in a message; the row is the only lifecycle.)
35325
35810
 
35326
- \`\`\`question
35327
- Should the settings store use SQLite or a JSON file?
35328
-
35329
- - A. SQLite \u2014 transactional, matches how sessions are already stored (recommended: consistency)
35330
- - B. JSON file \u2014 zero deps, human-editable, racy under concurrent writes
35331
- \`\`\`
35332
-
35333
- Each block must stand alone: the specific question, lettered options with a one-line tradeoff each, and
35334
- enough context to answer cold. Options go in a markdown list so each renders on its own line, and the
35335
- question itself stays on ONE line \u2014 inside a fence a newline is a HARD break, so a hard-wrapped sentence
35336
- renders ragged in the card. Mark your recommendation by writing \`recommended\` on that option's line \u2014
35337
- \`(recommended)\` or \`(recommended: one-line why)\` \u2014 and put it FIRST as \`A\`; mark exactly one. Use
35338
- MULTIPLE blocks for multiple independent questions, never one bundled block. A bare "which approach?"
35339
- with no options is a broken handoff.
35340
-
35341
- NO "I" AND NO "you" ANYWHERE IN A QUESTION \u2014 this block, or any interactive prompt. Clicking an
35342
- option is the HUMAN speaking, so first and second person flip between writer and reader: in "A. Leave
35343
- it \u2014 you run the command yourself" / "B. I run it now", neither line says which actor is which. Write
35344
- the whole block actor-explicit \u2014 each option an instruction the human hands back ("Reinstall the
35345
- hooks now") or one that names its actor outright ("the worker retries nightly", "left for the
35811
+ \`ask\` takes \`questions\` \u2014 several at once, each with \`question\`, \`kind\` (\`question\` = pick one,
35812
+ \`multi\` = pick several, no options at all = free text), an optional \`danger\`, and \`options\` each
35813
+ carrying a \`label\`, a \`description\` (ONE LINE by default \u2014 name the trade-off and stop; earn more
35814
+ than that and spend it on a shape the human can SCAN, a short list, a table, a code block, the diff an
35815
+ option would produce, the message that would be posted, and never a run of one-sentence paragraphs;
35816
+ it renders inside the option, so the human sees it BEFORE choosing), an optional \`recommended\`, and
35817
+ optional \`followUps\`: questions that become live ONLY if that option is taken, so you can ask "and if
35818
+ so, which?" without asking it of somebody who said no. Written out, one registration reads:
35819
+
35820
+ question: Should the settings store use SQLite or a JSON file?
35821
+ kind: question
35822
+ options:
35823
+ - label: SQLite
35824
+ description: transactional, matches how sessions are already stored
35825
+ recommended: true
35826
+ - label: JSON file
35827
+ description: zero deps, human-editable, racy under concurrent writes
35828
+
35829
+ Each question must stand alone: the specific question on ONE line, options each with a one-line
35830
+ trade-off, and enough context to answer cold. The card letters the options A, B, C in the order you
35831
+ give them, so put the one you would take FIRST and mark it \`recommended\`; mark exactly one. Several
35832
+ independent questions are several entries of ONE \`ask\` call, never one bundled question. A bare
35833
+ "which approach?" with no options is a broken handoff.
35834
+
35835
+ NO "I" AND NO "you" ANYWHERE IN A QUESTION \u2014 the question, its options, or any interactive prompt.
35836
+ Clicking an option is the HUMAN speaking, so first and second person flip between writer and reader: in
35837
+ "Leave it \u2014 you run the command yourself" / "I run it now", neither line says which actor is which.
35838
+ Write the whole question actor-explicit \u2014 each option an instruction the human hands back ("Reinstall
35839
+ the hooks now") or one that names its actor outright ("the worker retries nightly", "left for the
35346
35840
  maintainer's own terminal"):
35347
35841
 
35348
- \`\`\`question
35349
- A reply for issue #482 explaining the workaround is drafted. Post it from the maintainer's GitHub account, or leave it a draft?
35350
-
35351
- - A. Post it \u2014 the reporter is blocked and the workaround is verified (recommended)
35352
- - B. Leave it in the handoff, for edits before anything is posted
35353
- \`\`\`
35842
+ question: A reply for issue #482 explaining the workaround is drafted. Post it from the maintainer's GitHub account, or leave it a draft?
35843
+ kind: question
35844
+ options:
35845
+ - label: Post it
35846
+ description: the reporter is blocked and the workaround is verified
35847
+ recommended: true
35848
+ - label: Leave it in the handoff
35849
+ description: for edits before anything is posted
35354
35850
 
35355
- Write the block in the human's OWN vocabulary: they have their original prompt and nothing else \u2014 not
35851
+ Write the question in the human's OWN vocabulary: they have their original prompt and nothing else \u2014 not
35356
35852
  your plan, your notes, or the names you settled on while working. A name you coined mid-effort (a
35357
35853
  phase, lane, tier, step or section number, "the C path", "the second variant") means nothing to them, so
35358
35854
  translate it into what the thing does. Minimize code identifiers by default: lead with the behavior, and
35359
35855
  spend a file, symbol or flag where it genuinely reads clearest or the human already uses it. Every input
35360
35856
  the choice needs \u2014 what happens today, each option's user-visible consequence, any number that matters \u2014
35361
- belongs INSIDE the block; "as discussed above" points at something they cannot see.
35362
-
35363
- A GO/NO-GO gate is not a special fence \u2014 it is an ordinary \`question\` with two options (the go and the
35364
- decline, each a real choice the human can click). Tag the fence only to change how it renders:
35365
- \`question danger\` for the genuinely irreversible (force-merge, deletion, history rewrite, prod
35366
- rollback), \`question multi\` for select-several triage.
35367
-
35368
- **BETTER THAN A FENCED BLOCK \u2014 REGISTER THE QUESTION:** \`mcp__frizz__ask\` creates a ROW the human
35369
- still owes an answer to, instead of a block inside a message. That is the difference that matters: a
35370
- fence is answerable only while its message is on screen, so a question written into one is gone once
35371
- the transcript scrolls or your context is compacted, while a registration is still there tomorrow. It
35372
- takes \`questions\` \u2014 several at once, each with \`question\`, \`kind\` (\`question\` = pick one,
35373
- \`multi\` = pick several, no options at all = free text), an optional \`danger\`, and \`options\` each
35374
- carrying a \`label\`, a \`description\` (ONE LINE by default \u2014 name the trade-off and stop; earn more
35375
- than that and spend it on a shape the human can SCAN, a short list, a table, a code block, the diff an
35376
- option would produce, the message that would be posted, and never a run of one-sentence paragraphs;
35377
- it renders inside the option, so the human sees it BEFORE choosing), an optional
35378
- \`recommended\`, and optional \`followUps\`: questions that become live ONLY if that option is taken,
35379
- so you can ask "and if so, which?" without asking it of somebody who said no. **REGISTERING A QUESTION DOES NOT END YOUR TURN** \u2014
35380
- it carries no timeout and waits on a person, so keep doing everything that does not depend on the
35381
- answer. The answer comes back as its own wake, restating what was asked. Withdraw one you no longer
35382
- need with \`mcp__frizz__unask\`, above all when you work the answer out yourself. **ON A THREAD
35383
- CARRYING A GOAL IT REFUSES** \u2014 a standing "keep going, decide for yourself" is autonomous mode, and the
35384
- refusal hands that instruction back at the moment it matters. Decide, and say which way you went. A
35385
- genuinely human-owned call still belongs in the prose of your FINAL MESSAGE; autonomous does not mean
35386
- nobody reads.
35857
+ belongs INSIDE the question and its options; "as discussed above" points at something they cannot see.
35858
+
35859
+ A GO/NO-GO gate is not a special kind \u2014 it is an ordinary \`question\` with two options (the go and the
35860
+ decline, each a real choice the human can click). Tag it only to change how it renders: \`danger\` for
35861
+ the genuinely irreversible (force-merge, deletion, history rewrite, prod rollback), \`kind: multi\` for
35862
+ select-several triage.
35863
+
35864
+ **REGISTERING A QUESTION DOES NOT END YOUR TURN** \u2014 it carries no timeout and waits on a person, so
35865
+ keep doing everything that does not depend on the answer, and register the question the moment you
35866
+ find it rather than saving it for the end. The answer comes back as its own wake, restating what was
35867
+ asked. Withdraw one you no longer need with \`mcp__frizz__unask\`, above all when you work the answer
35868
+ out yourself. **ON A THREAD CARRYING A GOAL IT REFUSES** \u2014 a standing "keep going, decide for yourself"
35869
+ is autonomous mode, and the refusal hands that instruction back at the moment it matters. Decide, and
35870
+ say which way you went. A genuinely human-owned call still belongs in the prose of your FINAL MESSAGE;
35871
+ autonomous does not mean nobody reads.
35387
35872
 
35388
35873
  **EVERY OPEN QUESTION DRAWS ITS OWN CARD AT ITS REST \u2014 never write it into the handoff.** The
35389
- registered card renders whether you mention it or not, and a fence that names or restates a registered
35390
- question draws NOTHING: one question, one card, and answering the registered one is what settles the
35391
- row and un-gates \`done\`. So the handoff's job is the reasoning around the ask, not the ask itself.
35392
- (Mid-prose placement \u2014 an empty \`\`\`question fence carrying the row's id \u2014 was retired 2026-08-30;
35393
- a marker you still write draws nothing.)
35874
+ registered card renders whether you mention it or not, and prose that restates a registered question
35875
+ draws NOTHING: one question, one card, and answering the registered one is what settles the row and
35876
+ un-gates \`done\`. So the handoff's job is the reasoning around the ask, not the ask itself.
35877
+
35878
+ **TO PLACE A QUESTION INSIDE YOUR PROSE, WRITE AN EMPTY FENCE NAMING ITS ID** \u2014 the id \`ask\` returned
35879
+ (\`mcp__frizz__activity\` lists them). At the point your write-up has earned the ask \u2014 after the
35880
+ paragraph that sets it up, before the one that says what happens either way:
35881
+
35882
+ \`\`\`question qst_ab12cd34
35883
+ \`\`\`
35884
+
35885
+ Frizz draws the registered card THERE instead of at the tail. Nothing goes in the body: the card comes
35886
+ from the ROW, so nothing about the question is ever guessed from prose, and a marker naming no open
35887
+ question of yours draws nothing. One marker per question. Placement is OPTIONAL \u2014 a question with no
35888
+ marker renders at the tail of the rest, and a handoff that writes no marker loses nothing. Every answer
35889
+ of the rest is sent together, with ONE Send button at the tail, wherever the cards sit. (Placement was
35890
+ retired 2026-08-30 and restored 2026-09-11 with the free-form fence's retirement: the marker references
35891
+ a row, which is exactly what a fence with a body never did.)
35394
35892
 
35395
35893
  A question you no longer want answered is not one you leave out of the write-up \u2014 it is one you
35396
35894
  \`unask\`, which is the difference between deciding something yourself and quietly hoping nobody
35397
35895
  answers. Questions asked in one \`ask\` call render together and send as one batch.
35398
35896
 
35399
- Fence a question you did NOT register only when you mean to ask it in the message itself.
35400
-
35401
- A \` \`\`\`question \` block IS the handback \u2014 do not also emit a \`done\`/\`awaiting\` fence, and the same
35402
- holds while a REGISTERED question stands open. Answers arrive as your next user message, possibly as
35403
- terse as "1: A, 2: B".`;
35897
+ **AN OPEN REGISTERED QUESTION IS THE HANDBACK** \u2014 emit no \`done\`/\`awaiting\` fence beside it, and
35898
+ write no question fence at all. The answer arrives as your next user message, as its own wake.`;
35404
35899
  STOP_CRITERION = `## The stop criterion
35405
35900
 
35406
35901
  **COMING TO REST IS A STOP, and it needs the same justification as a question.** The far more common
@@ -35448,14 +35943,14 @@ Stop only when a wrong guess would be BOTH costly AND hard to undo: destructive
35448
35943
  security posture with real exposure; product/UX direction that is genuinely the human's taste to set;
35449
35944
  scope so vague that acting means inventing substantial new code with no way to check it. Everything
35450
35945
  else \u2014 mechanical work, clear bugs, refactors, defaults you can justify, architecture you can reverse \u2014
35451
- you finish. When you do stop, surface it as a \` \`\`\`question \` with your recommendation marked, and do
35452
- all the work that does NOT depend on the answer first.`;
35946
+ you finish. When you do stop, register it with \`mcp__frizz__ask\` with your recommendation marked, and
35947
+ do all the work that does NOT depend on the answer first.`;
35453
35948
  TRIVIAL_PROMPTS = `## Trivial and conversational prompts
35454
35949
 
35455
35950
  Some dispatches never deserved a work effort \u2014 a greeting, a one-line question, a joke, a test ping.
35456
35951
  Resolve them with ZERO ceremony: answer inline in one message, and close with a \` \`\`\`done \` fence whose
35457
35952
  body is one line ("Answered inline \u2014 conversational prompt, nothing to ship."). If the answer genuinely
35458
- needs a reply, ask with a \` \`\`\`question \` instead. Do not manufacture scope, restate the "task", or ask
35953
+ needs a reply, register it with \`mcp__frizz__ask\` instead. Do not manufacture scope, restate the "task", or ask
35459
35954
  clarifying questions to seem busy.`;
35460
35955
  THREAD_NAME = `## Name your thread once you know what the work is
35461
35956
 
@@ -35835,7 +36330,7 @@ Dispatches share a vocabulary for the deliverable and quality bar, not for fleet
35835
36330
  first that works? Cover and synthesize every relevant prong inline unless delegation was
35836
36331
  explicitly requested. Close with a \` \`\`\`done \` fence listing the completed research/evidence \u2014
35837
36332
  the report IS this thread's deliverable, unlike a bug/issue investigation headed for a fix, which
35838
- bare-rests; use \` \`\`\`question \` for a human call.
36333
+ bare-rests; use \`mcp__frizz__ask\` for a human call.
35839
36334
  - **Audit thread** \u2014 adversarially verify correctness, safety, or compatibility of something that
35840
36335
  exists. Exercise proportionate cases and lenses until dry; re-check load-bearing verdicts and cite
35841
36336
  evidence. Thorough coverage is required, but the audit label alone does not authorize fan-out.
@@ -36112,15 +36607,14 @@ var init_claude_broker_client = __esm({
36112
36607
  });
36113
36608
 
36114
36609
  // packages/server/src/backend/claude-permission-interactions.ts
36115
- import { homedir as homedir12 } from "node:os";
36610
+ import { homedir as homedir13 } from "node:os";
36116
36611
  function clip(text2, max) {
36117
36612
  return text2.length > max ? `${text2.slice(0, max - 1)}\u2026` : text2;
36118
36613
  }
36119
- function tildePath(path3) {
36120
- const home = homedir12();
36614
+ function tildePath(path3, home = homedir13()) {
36121
36615
  if (!path3.startsWith(home)) return path3;
36122
36616
  const rest = path3.slice(home.length);
36123
- return rest === "" ? "~" : rest.startsWith("/") ? `~${rest}` : path3;
36617
+ return rest === "" ? "~" : /^[\\/]/.test(rest) ? `~${rest}` : path3;
36124
36618
  }
36125
36619
  function inputPreview(input, shown) {
36126
36620
  if (!input) return "";
@@ -36354,8 +36848,8 @@ var init_claude_permission_interactions = __esm({
36354
36848
  HAS_UNSAFE_TEXT = new RegExp(UNSAFE_TEXT_SOURCE, "u");
36355
36849
  CLAUDE_NOTES_ONLY = "(notes only)";
36356
36850
  CLAUDE_QUESTION_DECLINED = "The operator did not answer this question from the frizz dashboard. Do not ask it again with this tool \u2014 decide with the information you already have, or raise it in your final message.";
36357
- CLAUDE_ASK_DENY_MESSAGE = "Frizz could not render this question as a card. Ask it in your FINAL MESSAGE instead, in a ```question fenced block \u2014 context, the question, lettered `- A. \u2026` options, a recommendation \u2014 then end your turn. The operator answers it from the queue and the reply arrives as your next user message.";
36358
- CLAUDE_ASK_WITHDRAWN_MESSAGE = "This question was withdrawn before anyone answered it \u2014 most likely because the operator sent a message instead. Read their next message and follow it. Do not ask this again with this tool; if you still need the decision, put it in your final message as a ```question fenced block.";
36851
+ CLAUDE_ASK_DENY_MESSAGE = "Frizz could not render this question as a card. Register it with `mcp__frizz__ask` instead \u2014 context, the question, options with a one-line trade-off each, the recommended one first \u2014 never as a ```question fence, which is retired. The operator answers it from the queue and the reply arrives as your next user message.";
36852
+ CLAUDE_ASK_WITHDRAWN_MESSAGE = "This question was withdrawn before anyone answered it \u2014 most likely because the operator sent a message instead. Read their next message and follow it. Do not ask this again with this tool; if you still need the decision, register it with `mcp__frizz__ask`.";
36359
36853
  }
36360
36854
  });
36361
36855
 
@@ -36767,8 +37261,8 @@ var init_claude_agent_broker_bridge = __esm({
36767
37261
  });
36768
37262
 
36769
37263
  // packages/server/src/dispatch.ts
36770
- import { closeSync as closeSync12, constants, existsSync as existsSync9, fstatSync as fstatSync2, lstatSync as lstatSync3, openSync as openSync13, readFileSync as readFileSync18, realpathSync as realpathSync7, statSync as statSync12, writeFileSync as writeFileSync10, mkdirSync as mkdirSync13, rmSync as rmSync9 } from "node:fs";
36771
- import { basename as basename6, join as join28, dirname as dirname11 } from "node:path";
37264
+ import { closeSync as closeSync12, constants, existsSync as existsSync10, fstatSync as fstatSync2, lstatSync as lstatSync3, openSync as openSync13, readFileSync as readFileSync20, realpathSync as realpathSync8, statSync as statSync13, writeFileSync as writeFileSync10, mkdirSync as mkdirSync13, rmSync as rmSync9 } from "node:fs";
37265
+ import { basename as basename6, join as join29, dirname as dirname11 } from "node:path";
36772
37266
  import { createHash as createHash11, randomUUID as randomUUID13 } from "node:crypto";
36773
37267
  function fallbackTitle(prompt) {
36774
37268
  const firstLine2 = prompt.trim().split("\n", 1)[0].trim();
@@ -36787,7 +37281,7 @@ function fallbackTitle(prompt) {
36787
37281
  }
36788
37282
  function resolveSlug(frizzDir, base, taken) {
36789
37283
  base = ThreadSlug.parse(base);
36790
- const isTaken = (slug) => existsSync9(join28(frizzDir, `${slug}.md`)) || (taken?.(slug) ?? false);
37284
+ const isTaken = (slug) => existsSync10(join29(frizzDir, `${slug}.md`)) || (taken?.(slug) ?? false);
36791
37285
  if (!isTaken(base)) return base;
36792
37286
  for (let n = 2; ; n++) {
36793
37287
  const suffix = `-${n}`;
@@ -36803,16 +37297,16 @@ function resolveLegacyThreadFile(projectDir, value) {
36803
37297
  const parsed = ThreadSlug.safeParse(value);
36804
37298
  if (!parsed.success) return null;
36805
37299
  try {
36806
- const projectRoot = realpathSync7(projectDir);
36807
- const frizzPath = join28(projectRoot, ".frizz");
37300
+ const projectRoot = realpathSync8(projectDir);
37301
+ const frizzPath = join29(projectRoot, ".frizz");
36808
37302
  const frizzStat = lstatSync3(frizzPath);
36809
37303
  if (!frizzStat.isDirectory() || frizzStat.isSymbolicLink()) return null;
36810
- const realFrizz = realpathSync7(frizzPath);
37304
+ const realFrizz = realpathSync8(frizzPath);
36811
37305
  if (dirname11(realFrizz) !== projectRoot || basename6(realFrizz) !== ".frizz") return null;
36812
- const path3 = join28(realFrizz, `${parsed.data}.md`);
37306
+ const path3 = join29(realFrizz, `${parsed.data}.md`);
36813
37307
  const before = lstatSync3(path3);
36814
37308
  if (!before.isFile() || before.isSymbolicLink()) return null;
36815
- const realPath = realpathSync7(path3);
37309
+ const realPath = realpathSync8(path3);
36816
37310
  if (dirname11(realPath) !== realFrizz || basename6(realPath) !== `${parsed.data}.md`) return null;
36817
37311
  let contents;
36818
37312
  let openedBefore;
@@ -36820,7 +37314,7 @@ function resolveLegacyThreadFile(projectDir, value) {
36820
37314
  const fd = openSync13(path3, constants.O_RDONLY | constants.O_NOFOLLOW);
36821
37315
  try {
36822
37316
  openedBefore = fstatSync2(fd);
36823
- contents = readFileSync18(fd);
37317
+ contents = readFileSync20(fd);
36824
37318
  openedAfter = fstatSync2(fd);
36825
37319
  } finally {
36826
37320
  closeSync12(fd);
@@ -36853,7 +37347,7 @@ function boardAuthorizesAdoption(board, slug) {
36853
37347
  return !board.errorItems.some((item) => item.file === `${slug}.md`);
36854
37348
  }
36855
37349
  function ensureSafeDirectDirectory(parent, name) {
36856
- const path3 = join28(parent, name);
37350
+ const path3 = join29(parent, name);
36857
37351
  try {
36858
37352
  mkdirSync13(path3);
36859
37353
  } catch (error) {
@@ -36862,7 +37356,7 @@ function ensureSafeDirectDirectory(parent, name) {
36862
37356
  }
36863
37357
  const stat2 = lstatSync3(path3);
36864
37358
  if (!stat2.isDirectory() || stat2.isSymbolicLink()) throw new Error("unsafe project directory");
36865
- const real = realpathSync7(path3);
37359
+ const real = realpathSync8(path3);
36866
37360
  if (dirname11(real) !== parent || basename6(real) !== name) throw new Error("unsafe project directory");
36867
37361
  return real;
36868
37362
  }
@@ -36871,7 +37365,7 @@ function scratchDirRelPath(sessionId) {
36871
37365
  }
36872
37366
  function writeScratchDir(projectDir, sessionId) {
36873
37367
  if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,199}$/.test(sessionId)) throw new Error("invalid session id");
36874
- const projectRoot = realpathSync7(projectDir);
37368
+ const projectRoot = realpathSync8(projectDir);
36875
37369
  const frizzDir = ensureSafeDirectDirectory(projectRoot, ".frizz");
36876
37370
  const threadsDir = ensureSafeDirectDirectory(frizzDir, "threads");
36877
37371
  ensureSafeDirectDirectory(threadsDir, sessionId);
@@ -36883,8 +37377,8 @@ function loadWorkerPrompt(kind = "claude") {
36883
37377
  function monitorScriptsDir() {
36884
37378
  const plugin = workerPluginDir();
36885
37379
  if (!plugin) return void 0;
36886
- const dir = join28(plugin, "skills", "gh", "scripts");
36887
- return existsSync9(join28(dir, "ci-watch.mjs")) ? dir : void 0;
37380
+ const dir = join29(plugin, "skills", "gh", "scripts");
37381
+ return existsSync10(join29(dir, "ci-watch.mjs")) ? dir : void 0;
36888
37382
  }
36889
37383
  function codexScratchpadHookConfig(hookScript, sessionId) {
36890
37384
  if (!hookScript || !sessionId) return {};
@@ -36896,7 +37390,7 @@ function codexScratchpadHookConfig(hookScript, sessionId) {
36896
37390
  }
36897
37391
  ]
36898
37392
  });
36899
- const bashBackgroundHook = join28(dirname11(hookScript), "bash-background.mjs");
37393
+ const bashBackgroundHook = join29(dirname11(hookScript), "bash-background.mjs");
36900
37394
  return {
36901
37395
  bypass_hook_trust: true,
36902
37396
  hooks: {
@@ -36922,7 +37416,7 @@ function codexScratchpadHookConfig(hookScript, sessionId) {
36922
37416
  }
36923
37417
  function scratchpadHookScript() {
36924
37418
  const plugin = workerPluginDir();
36925
- return plugin ? join28(plugin, "hooks", "scratchpad.mjs") : void 0;
37419
+ return plugin ? join29(plugin, "hooks", "scratchpad.mjs") : void 0;
36926
37420
  }
36927
37421
  function composePrompt(sessionId, prompt, kind = "claude") {
36928
37422
  const children2 = kind === "codex" ? "Native sub-agents share it \u2014 have each write its OWN file rather than all editing one." : "Name it in a sub-agent's prompt when you want its notes to land somewhere you can read; give each child its OWN file rather than having them all edit one.";
@@ -36938,12 +37432,12 @@ function scratchpadOrientation(sessionId, kind = "claude") {
36938
37432
  return `SCRATCH DIRECTORY: .frizz/threads/${sessionId}/ \u2014 yours, free-form, as many files as you like, and nothing is expected in it. A single direct task usually needs none; writing notes is never a substitute for doing the work (${children2}). Nothing in this directory is read automatically; if you want a note back after a compaction, mcp__frizz__goal with post_compaction: true re-sends a prompt of your choosing.`;
36939
37433
  }
36940
37434
  function frizzConfigBlock(projectDir) {
36941
- const path3 = join28(projectDir, "FRIZZ.md");
37435
+ const path3 = join29(projectDir, "FRIZZ.md");
36942
37436
  let body;
36943
37437
  try {
36944
- const st = statSync12(path3);
37438
+ const st = statSync13(path3);
36945
37439
  if (!st.isFile() || st.size > FRIZZ_MD_MAX_BYTES) return "";
36946
- body = readFileSync18(path3, "utf8").trim();
37440
+ body = readFileSync20(path3, "utf8").trim();
36947
37441
  } catch {
36948
37442
  return "";
36949
37443
  }
@@ -36982,8 +37476,8 @@ function resolveFrizzMcp(target, moduleUrl = import.meta.url, env = process.env,
36982
37476
  const { stateDir, serverLock, projectId } = typeof target === "string" ? { stateDir: target } : target;
36983
37477
  const pluginDir = resolveWorkerPluginDir(moduleUrl, env);
36984
37478
  if (!pluginDir) return void 0;
36985
- const scriptPath = join28(pluginDir, "bin", FRIZZ_MCP.script);
36986
- if (!existsSync9(scriptPath)) return void 0;
37479
+ const scriptPath = join29(pluginDir, "bin", FRIZZ_MCP.script);
37480
+ if (!existsSync10(scriptPath)) return void 0;
36987
37481
  return {
36988
37482
  scriptPath,
36989
37483
  stateDir,
@@ -37065,7 +37559,7 @@ function buildClaudeResumeCommand(opts) {
37065
37559
  }
37066
37560
  function createDispatcher(deps) {
37067
37561
  const readBoardSource = deps.readBoard ?? readBoard;
37068
- const frizzDir = join28(deps.project.dir, ".frizz");
37562
+ const frizzDir = join29(deps.project.dir, ".frizz");
37069
37563
  const adoptionRuntime = deps.adoptionRuntime ?? productionRuntime;
37070
37564
  function cleanupPrewrites(built) {
37071
37565
  for (const path3 of new Set(built.prewrite.map((file) => file.path))) {
@@ -37078,7 +37572,7 @@ function createDispatcher(deps) {
37078
37572
  function cleanupDispatchFiles(scratchRel, built, sessionId) {
37079
37573
  cleanupPrewrites(built);
37080
37574
  try {
37081
- rmSync9(join28(deps.project.dir, scratchRel), { force: true, recursive: true });
37575
+ rmSync9(join29(deps.project.dir, scratchRel), { force: true, recursive: true });
37082
37576
  } catch {
37083
37577
  }
37084
37578
  cleanupAdoptionSessionFiles(deps.project.dir, sessionId);
@@ -37516,11 +38010,11 @@ var init_dispatch = __esm({
37516
38010
 
37517
38011
  // packages/server/src/board.ts
37518
38012
  import {
37519
- realpathSync as realpathSync8,
38013
+ realpathSync as realpathSync9,
37520
38014
  watch as fsWatch
37521
38015
  } from "node:fs";
37522
- import { homedir as homedir13 } from "node:os";
37523
- import { dirname as dirname12, join as join29 } from "node:path";
38016
+ import { homedir as homedir14 } from "node:os";
38017
+ import { dirname as dirname12, join as join30 } from "node:path";
37524
38018
  import watcher from "@parcel/watcher";
37525
38019
  function appServerTurnStalled(liveness, lastActivityAt, nowMs) {
37526
38020
  if (!liveness) return false;
@@ -37831,20 +38325,20 @@ function resolveSessionProfile(row, tele) {
37831
38325
  function resolveSessionPermission(row, tele) {
37832
38326
  const saved = PermissionMode.safeParse(row.permission_mode);
37833
38327
  const pending = PermissionMode.safeParse(row.permission_pending);
37834
- const normalize3 = (mode) => effectivePermissionMode(row.backend === "codex" ? "codex" : "claude", mode);
37835
- if (saved.success && pending.success && normalize3(saved.data) === normalize3(pending.data)) return normalize3(saved.data);
38328
+ const normalize2 = (mode) => effectivePermissionMode(row.backend === "codex" ? "codex" : "claude", mode);
38329
+ if (saved.success && pending.success && normalize2(saved.data) === normalize2(pending.data)) return normalize2(saved.data);
37836
38330
  if (row.backend === "codex" && saved.success) {
37837
38331
  const observedAt = tele?.permissionModeAt ? Date.parse(tele.permissionModeAt) : NaN;
37838
38332
  const spawnedAt = Date.parse(row.spawned_at);
37839
38333
  if (tele?.permissionMode && Number.isFinite(observedAt) && Number.isFinite(spawnedAt) && observedAt >= spawnedAt) {
37840
38334
  const setAt = row.permission_set_at ? Date.parse(row.permission_set_at) : NaN;
37841
- if (Number.isFinite(setAt) && setAt > observedAt) return normalize3(saved.data);
37842
- return normalize3(tele.permissionMode);
38335
+ if (Number.isFinite(setAt) && setAt > observedAt) return normalize2(saved.data);
38336
+ return normalize2(tele.permissionMode);
37843
38337
  }
37844
- return normalize3(saved.data);
38338
+ return normalize2(saved.data);
37845
38339
  }
37846
- if (saved.success) return normalize3(saved.data);
37847
- return tele?.permissionMode ? normalize3(tele.permissionMode) : void 0;
38340
+ if (saved.success) return normalize2(saved.data);
38341
+ return tele?.permissionMode ? normalize2(tele.permissionMode) : void 0;
37848
38342
  }
37849
38343
  function resolvePendingPermission(row) {
37850
38344
  const parsed = PermissionMode.safeParse(row.permission_pending);
@@ -37903,6 +38397,7 @@ function registeredDoneFence(done, lastUserAt) {
37903
38397
  }
37904
38398
  function readThreadRegistries(storage) {
37905
38399
  return {
38400
+ links: storage.threadLinksBySlug(),
37906
38401
  prWatches: storage.armedPrWatchesBySlug(),
37907
38402
  timers: storage.armedThreadTimersBySlug(),
37908
38403
  questions: storage.threadQuestionsBySlug(),
@@ -38010,6 +38505,7 @@ function sessionThreadView(projectDir, storage, row, rawTele, registeredLegacyTe
38010
38505
  lastAssistantAt: tele?.lastAssistantAt,
38011
38506
  subAgents: stampStoppable(tele?.subAgents ?? [], row),
38012
38507
  bgShells: stampStoppableShells(tele?.bgShells ?? [], row),
38508
+ links: (registries.links.get(row.slug) ?? []).map(threadLinkView),
38013
38509
  // ONE SOURCE: the FENCE. Both kinds are derived from what the worker wrote — `prs:` entries
38014
38510
  // become the github rows, `watch:` lines the shell rows — so this strip lists exactly what will
38015
38511
  // actually wake the thread, and the two cannot drift into claiming different things. There is no
@@ -38257,7 +38753,7 @@ function createBoard(project, storage, bus, tailer, bootId, deps = {}) {
38257
38753
  ...project.githubRepo ? { githubRepo: project.githubRepo } : {},
38258
38754
  projectSlug: findByPath(project.dir)?.slug,
38259
38755
  // So the client can expand a `~` a worker wrote in prose (see BoardSnapshot.homeDir).
38260
- homeDir: homedir13()
38756
+ homeDir: homedir14()
38261
38757
  };
38262
38758
  const sessionThreads = buildSessionThreads(assembledAtMs);
38263
38759
  armSnoozeWake(sessionThreads, assembledAtMs);
@@ -38358,10 +38854,10 @@ function createBoard(project, storage, bus, tailer, bootId, deps = {}) {
38358
38854
  if (parcelSub || stopped) return Promise.resolve();
38359
38855
  if (watchSetup) return watchSetup;
38360
38856
  const setup = (async () => {
38361
- const dir = join29(project.dir, ".frizz");
38857
+ const dir = join30(project.dir, ".frizz");
38362
38858
  const roots = /* @__PURE__ */ new Set([dir]);
38363
38859
  try {
38364
- roots.add(realpathSync8(dir));
38860
+ roots.add(realpathSync9(dir));
38365
38861
  } catch {
38366
38862
  }
38367
38863
  const next = await subscribe(
@@ -38373,7 +38869,7 @@ function createBoard(project, storage, bus, tailer, bootId, deps = {}) {
38373
38869
  }
38374
38870
  if (events.some((event) => isBoardRelevantFrizzPath(roots, event.path))) scheduleRebuild();
38375
38871
  },
38376
- { ignore: FRIZZ_WATCH_IGNORED_DIRS.map((name) => join29(dir, name)) }
38872
+ { ignore: FRIZZ_WATCH_IGNORED_DIRS.map((name) => join30(dir, name)) }
38377
38873
  );
38378
38874
  if (stopped) {
38379
38875
  await next.unsubscribe();
@@ -38455,6 +38951,7 @@ var init_board = __esm({
38455
38951
  "use strict";
38456
38952
  init_src();
38457
38953
  init_storage();
38954
+ init_thread_links();
38458
38955
  init_thread_profiles();
38459
38956
  init_frizz();
38460
38957
  init_awaiting();
@@ -40003,8 +40500,8 @@ ${wakeTimeHeader(nowMs, spokeAt)}`,
40003
40500
  "in a sentence breaks the YAML, which is why `reason:` is gone.",
40004
40501
  "",
40005
40502
  "`mcp__frizz__activity` lists everything you have running, and prints it back as a ready-to-paste",
40006
- "fence. If you are not waiting on anything, you are not awaiting \u2014 end with ```done, or ask a",
40007
- "```question."
40503
+ "fence. If you are not waiting on anything, you are not awaiting \u2014 end with ```done, or register",
40504
+ "a question with `mcp__frizz__ask`."
40008
40505
  ].join("\n") : nameless ? [
40009
40506
  `${PARK_CORRECTION_NAMES_LEAD}nothing to wait on, so it is not a park \u2014 your thread is still`,
40010
40507
  "in the queue, and nothing will wake you.",
@@ -40020,15 +40517,15 @@ ${wakeTimeHeader(nowMs, spokeAt)}`,
40020
40517
  "Use `mcp__frizz__activity` to read it all back as a ready-to-paste fence.",
40021
40518
  "",
40022
40519
  "AND IF YOU ARE NOT WAITING ON ANYTHING, you are not awaiting \u2014 you are done. End with ```done,",
40023
- "or ask a ```question if you need the human."
40520
+ "or register a question with `mcp__frizz__ask` if you need the human."
40024
40521
  ].join("\n") : expired ? parkExpiredWakeMessage(status) : allFinished ? parkFinishedWakeMessage(status, dead.length !== 1) : [
40025
40522
  `${PARK_CORRECTION_NAMES_LEAD}${dead.length === 1 ? "something that is" : "things that are"} not running, so it is not a park and your thread stayed in the queue.`,
40026
40523
  "",
40027
40524
  ...status,
40028
40525
  "",
40029
40526
  "Use `mcp__frizz__activity` to read back what you actually have out, with the exact id each line",
40030
- "needs, then re-fence naming only those \u2014 or end in ```done or a ```question if there is nothing",
40031
- "left to wait for."
40527
+ "needs, then re-fence naming only those \u2014 or end in ```done or a registered question",
40528
+ "(`mcp__frizz__ask`) if there is nothing left to wait for."
40032
40529
  ].join("\n");
40033
40530
  const cause = retired.length > 0 ? "retired" : nameless ? "nameless" : expired ? "expired" : "dead";
40034
40531
  const ops = cause === "expired" ? [] : liveOpsLines({
@@ -41050,7 +41547,7 @@ var init_resume = __esm({
41050
41547
  });
41051
41548
 
41052
41549
  // packages/server/src/backend/claude.ts
41053
- import { join as join30 } from "node:path";
41550
+ import { join as join31 } from "node:path";
41054
41551
  function toolResultText2(content) {
41055
41552
  if (typeof content === "string") return content;
41056
41553
  if (!Array.isArray(content)) return "";
@@ -41141,7 +41638,7 @@ function createClaudeBackend(opts) {
41141
41638
  return { argv, env: claudeWorkerEnvironment(), prewrite: [] };
41142
41639
  },
41143
41640
  transcriptPath(sessionId) {
41144
- return join30(opts.logDir, `${sessionId}.jsonl`);
41641
+ return join31(opts.logDir, `${sessionId}.jsonl`);
41145
41642
  },
41146
41643
  parseLine(line) {
41147
41644
  return parseClaudeLine(line);
@@ -41170,10 +41667,31 @@ import { randomBytes } from "node:crypto";
41170
41667
  import pty from "node-pty";
41171
41668
  function createLoginUtility(deps) {
41172
41669
  const spawnPty = deps.spawnPty ?? pty.spawn;
41670
+ const env = deps.env ?? process.env;
41173
41671
  const lifetimeMs = deps.lifetimeMs ?? ATTEMPT_LIFETIME_MS;
41174
41672
  const attempts = /* @__PURE__ */ new Map();
41175
41673
  function loginArgv(backend) {
41176
- return backend === "codex" ? { file: deps.codexBin ?? "codex", args: ["login"] } : { file: deps.claudeBin ?? "claude", args: ["auth", "login"] };
41674
+ if (backend === "codex") {
41675
+ const codex = resolveCodexExecutable(deps.codexBin, { env });
41676
+ return { file: codex.file, args: [...codex.args, "login"] };
41677
+ }
41678
+ return { file: resolveClaudeExecutableAbsolute(deps.claudeBin, env), args: ["auth", "login"] };
41679
+ }
41680
+ function failedAttempt(id, backend, cause) {
41681
+ const reason = cause instanceof Error ? cause.message : String(cause);
41682
+ const error = `Could not start the ${backend === "codex" ? "Codex" : "Claude"} sign-in: ${reason}`;
41683
+ return {
41684
+ id,
41685
+ backend,
41686
+ timer: setTimeout(() => teardown(id), lifetimeMs),
41687
+ buffer: `${error}\r
41688
+ `,
41689
+ bufferBytes: Buffer.byteLength(error) + 2,
41690
+ exited: true,
41691
+ error,
41692
+ dataListeners: /* @__PURE__ */ new Set(),
41693
+ exitListeners: /* @__PURE__ */ new Set()
41694
+ };
41177
41695
  }
41178
41696
  function teardown(id) {
41179
41697
  const attempt = attempts.get(id);
@@ -41191,7 +41709,7 @@ function createLoginUtility(deps) {
41191
41709
  }
41192
41710
  attempt.exitListeners.clear();
41193
41711
  try {
41194
- attempt.term.kill();
41712
+ attempt.term?.kill();
41195
41713
  } catch {
41196
41714
  }
41197
41715
  }
@@ -41203,14 +41721,22 @@ function createLoginUtility(deps) {
41203
41721
  teardown(attempt2.id);
41204
41722
  }
41205
41723
  const id = `login-${randomBytes(8).toString("hex")}`;
41206
- const { file, args } = loginArgv(backend);
41207
- const term = spawnPty(file, args, {
41208
- name: process.platform === "win32" ? "xterm-color" : "xterm-256color",
41209
- cwd: deps.cwd,
41210
- env: process.env,
41211
- cols: 120,
41212
- rows: 30
41213
- });
41724
+ let term;
41725
+ try {
41726
+ const { file, args } = loginArgv(backend);
41727
+ term = spawnPty(file, args, {
41728
+ name: process.platform === "win32" ? "xterm-color" : "xterm-256color",
41729
+ cwd: deps.cwd,
41730
+ env,
41731
+ cols: 120,
41732
+ rows: 30
41733
+ });
41734
+ } catch (cause) {
41735
+ const failed = failedAttempt(id, backend, cause);
41736
+ failed.timer.unref?.();
41737
+ attempts.set(id, failed);
41738
+ return { attemptId: id };
41739
+ }
41214
41740
  const attempt = {
41215
41741
  id,
41216
41742
  backend,
@@ -41262,7 +41788,7 @@ function createLoginUtility(deps) {
41262
41788
  write: (data) => {
41263
41789
  if (!attempt.exited) {
41264
41790
  try {
41265
- attempt.term.write(data);
41791
+ attempt.term?.write(data);
41266
41792
  } catch {
41267
41793
  }
41268
41794
  }
@@ -41270,7 +41796,7 @@ function createLoginUtility(deps) {
41270
41796
  resize: (cols, rows) => {
41271
41797
  if (!attempt.exited) {
41272
41798
  try {
41273
- attempt.term.resize(cols, rows);
41799
+ attempt.term?.resize(cols, rows);
41274
41800
  } catch {
41275
41801
  }
41276
41802
  }
@@ -41291,7 +41817,7 @@ function createLoginUtility(deps) {
41291
41817
  status(attemptId) {
41292
41818
  const attempt = attempts.get(attemptId);
41293
41819
  if (!attempt) return { state: "exited" };
41294
- return { state: attempt.exited ? "exited" : "running", backend: attempt.backend };
41820
+ return { state: attempt.exited ? "exited" : "running", backend: attempt.backend, ...attempt.error ? { error: attempt.error } : {} };
41295
41821
  },
41296
41822
  cancel(attemptId) {
41297
41823
  teardown(attemptId);
@@ -41305,19 +41831,21 @@ var ATTEMPT_LIFETIME_MS, REPLAY_CAP_BYTES;
41305
41831
  var init_login_utility = __esm({
41306
41832
  "packages/server/src/login-utility.ts"() {
41307
41833
  "use strict";
41834
+ init_claude_broker_host();
41835
+ init_codex_executable();
41308
41836
  ATTEMPT_LIFETIME_MS = 10 * 60 * 1e3;
41309
41837
  REPLAY_CAP_BYTES = 256 * 1024;
41310
41838
  }
41311
41839
  });
41312
41840
 
41313
41841
  // packages/server/src/backend/codex-app-server-diagnostics.ts
41314
- import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync14, renameSync as renameSync8, statSync as statSync13 } from "node:fs";
41315
- import { join as join31 } from "node:path";
41842
+ import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync14, renameSync as renameSync8, statSync as statSync14 } from "node:fs";
41843
+ import { join as join32 } from "node:path";
41316
41844
  function codexDiagnosticLogPath(stateDir, projectId) {
41317
- return join31(stateDir, "codex-app-server", `${projectId}.diagnostics.log`);
41845
+ return join32(stateDir, "codex-app-server", `${projectId}.diagnostics.log`);
41318
41846
  }
41319
41847
  function createCodexDiagnosticSink(stateDir, projectId, now = () => /* @__PURE__ */ new Date()) {
41320
- const dir = join31(stateDir, "codex-app-server");
41848
+ const dir = join32(stateDir, "codex-app-server");
41321
41849
  const path3 = codexDiagnosticLogPath(stateDir, projectId);
41322
41850
  let ensured = false;
41323
41851
  return (event) => {
@@ -41328,7 +41856,7 @@ function createCodexDiagnosticSink(stateDir, projectId, now = () => /* @__PURE__
41328
41856
  ensured = true;
41329
41857
  }
41330
41858
  try {
41331
- if (statSync13(path3).size > MAX_BYTES2) renameSync8(path3, `${path3}.1`);
41859
+ if (statSync14(path3).size > MAX_BYTES2) renameSync8(path3, `${path3}.1`);
41332
41860
  } catch {
41333
41861
  }
41334
41862
  appendFileSync3(path3, `${JSON.stringify({ at: now().toISOString(), ...event })}
@@ -41349,7 +41877,7 @@ var init_codex_app_server_diagnostics = __esm({
41349
41877
  import { execFile as execFile8 } from "node:child_process";
41350
41878
  import { readdirSync as readdirSync8, unlinkSync as unlinkSync4 } from "node:fs";
41351
41879
  import { connect as connectSocket } from "node:net";
41352
- import { join as join32 } from "node:path";
41880
+ import { join as join33 } from "node:path";
41353
41881
  function parseLsofSocketNames(stdout) {
41354
41882
  const referenced = /* @__PURE__ */ new Set();
41355
41883
  for (const line of stdout.split("\n")) {
@@ -41366,7 +41894,7 @@ function sweepStaleSockets(options, deps = {}) {
41366
41894
  const keep = new Set(options.keep ?? []);
41367
41895
  let candidates;
41368
41896
  try {
41369
- candidates = (deps.readdir ?? readdirSync8)(options.dir).filter((name) => name.startsWith(options.prefix) && name.endsWith(".sock")).map((name) => join32(options.dir, name)).filter((path3) => !keep.has(path3));
41897
+ candidates = (deps.readdir ?? readdirSync8)(options.dir).filter((name) => name.startsWith(options.prefix) && name.endsWith(".sock")).map((name) => join33(options.dir, name)).filter((path3) => !keep.has(path3));
41370
41898
  } catch {
41371
41899
  return;
41372
41900
  }
@@ -41416,7 +41944,7 @@ var init_stale_socket_sweep = __esm({
41416
41944
 
41417
41945
  // packages/server/src/orphan-reaper.ts
41418
41946
  import { execFileSync as execFileSync4 } from "node:child_process";
41419
- import { readFileSync as readFileSync19 } from "node:fs";
41947
+ import { readFileSync as readFileSync21 } from "node:fs";
41420
41948
  import { dirname as dirname13 } from "node:path";
41421
41949
  function firstTokenBasename(command) {
41422
41950
  const first = command.trimStart().split(/\s+/, 1)[0] ?? "";
@@ -41572,7 +42100,7 @@ ${chunk}`;
41572
42100
  }
41573
42101
  return rows;
41574
42102
  }
41575
- function reapSubtrees(seed, rows, protectedPids, live, kill = defaultKill) {
42103
+ function reapSubtrees(seed, rows, protectedPids, live, kill = defaultKill2) {
41576
42104
  const keep = (row) => protectedPids.has(row.pid) || isSessionRoot(row.command) || isTmuxServer(row.command) || row.slug !== null && live.has(row.slug);
41577
42105
  const order = subtreeKillOrder(seed, rows, keep);
41578
42106
  for (const pid of order) kill(pid);
@@ -41624,8 +42152,9 @@ function sweepOrphansOnce(deps = {}) {
41624
42152
  let rows;
41625
42153
  try {
41626
42154
  rows = enumerateProcs(exec, { platform: deps.platform, readEnv: deps.readEnv });
41627
- } catch {
41628
- return { reaped: 0, deadSlugs: [], liveSlugs: [] };
42155
+ } catch (error) {
42156
+ const code = error?.code;
42157
+ return { reaped: 0, deadSlugs: [], liveSlugs: [], ...code === "ENOENT" ? { unavailable: "`ps` is not on PATH" } : {} };
41629
42158
  }
41630
42159
  const protectedPids = selfAndAncestors(rows, selfPid);
41631
42160
  const { reap, liveSlugs } = decideOrphans(rows, { minAgeMs, protectedPids });
@@ -41649,14 +42178,25 @@ function sweepStaleBrokerSockets() {
41649
42178
  }
41650
42179
  }
41651
42180
  function startOrphanReaper(deps = {}) {
42181
+ if ((deps.platform ?? process.platform) === "win32") {
42182
+ deps.log?.(UNAVAILABLE_ON_WINDOWS);
42183
+ return () => {
42184
+ };
42185
+ }
41652
42186
  const intervalMs = deps.intervalMs ?? ORPHAN_REAP_INTERVAL_MS;
42187
+ const unavailable = (result) => {
42188
+ if (!result.unavailable) return false;
42189
+ deps.log?.(`orphan-reaper: unavailable on this machine \u2014 ${result.unavailable}; background processes a stopped thread leaves behind are not collected here`);
42190
+ return true;
42191
+ };
41653
42192
  try {
41654
- sweepOrphansOnce(deps);
42193
+ if (unavailable(sweepOrphansOnce(deps))) return () => {
42194
+ };
41655
42195
  } catch {
41656
42196
  }
41657
42197
  const timer = setInterval(() => {
41658
42198
  try {
41659
- sweepOrphansOnce(deps);
42199
+ if (unavailable(sweepOrphansOnce(deps))) clearInterval(timer);
41660
42200
  } catch {
41661
42201
  }
41662
42202
  }, intervalMs);
@@ -41671,7 +42211,7 @@ function startOrphanReaper(deps = {}) {
41671
42211
  clearInterval(socketTimer);
41672
42212
  };
41673
42213
  }
41674
- var ORPHAN_REAP_INTERVAL_MS, ORPHAN_MIN_AGE_MS, SLUG_RE, defaultExec, defaultReadEnv, defaultKill, STALE_SOCKET_SWEEP_INTERVAL_MS, STALE_SOCKET_SWEEP_DELAY_MS, CLAUDE_BROKER_SOCKET_PREFIX;
42214
+ var ORPHAN_REAP_INTERVAL_MS, ORPHAN_MIN_AGE_MS, SLUG_RE, defaultExec, defaultReadEnv, defaultKill2, STALE_SOCKET_SWEEP_INTERVAL_MS, STALE_SOCKET_SWEEP_DELAY_MS, CLAUDE_BROKER_SOCKET_PREFIX, UNAVAILABLE_ON_WINDOWS;
41675
42215
  var init_orphan_reaper = __esm({
41676
42216
  "packages/server/src/orphan-reaper.ts"() {
41677
42217
  "use strict";
@@ -41683,12 +42223,12 @@ var init_orphan_reaper = __esm({
41683
42223
  defaultExec = (file, args) => execFileSync4(file, args, { encoding: "utf8", maxBuffer: 16 * 1024 * 1024, timeout: 1e4 });
41684
42224
  defaultReadEnv = (pid) => {
41685
42225
  try {
41686
- return readFileSync19(`/proc/${pid}/environ`, "utf8");
42226
+ return readFileSync21(`/proc/${pid}/environ`, "utf8");
41687
42227
  } catch {
41688
42228
  return null;
41689
42229
  }
41690
42230
  };
41691
- defaultKill = (pid) => {
42231
+ defaultKill2 = (pid) => {
41692
42232
  try {
41693
42233
  process.kill(pid, "SIGKILL");
41694
42234
  } catch {
@@ -41697,6 +42237,7 @@ var init_orphan_reaper = __esm({
41697
42237
  STALE_SOCKET_SWEEP_INTERVAL_MS = 10 * 6e4;
41698
42238
  STALE_SOCKET_SWEEP_DELAY_MS = 3e4;
41699
42239
  CLAUDE_BROKER_SOCKET_PREFIX = "frizz-claude-";
42240
+ UNAVAILABLE_ON_WINDOWS = "orphan-reaper: unavailable on Windows \u2014 a worker's FRIZZ_THREAD is read from its process environment, which Windows does not expose; background processes a stopped thread leaves behind are not collected here, and a live thread's runaway work is not reported";
41700
42241
  }
41701
42242
  });
41702
42243
 
@@ -41830,9 +42371,9 @@ var init_thread_hibernation = __esm({
41830
42371
  });
41831
42372
 
41832
42373
  // packages/server/src/context.ts
41833
- import { join as join33 } from "node:path";
42374
+ import { join as join34 } from "node:path";
41834
42375
  import { randomUUID as randomUUID15 } from "node:crypto";
41835
- import { homedir as homedir14 } from "node:os";
42376
+ import { homedir as homedir15 } from "node:os";
41836
42377
  function reconcileSessions(storage) {
41837
42378
  for (const row of storage.allSessions()) {
41838
42379
  if (isHeadlessRow(row)) continue;
@@ -41978,9 +42519,9 @@ async function createContext(opts = {}) {
41978
42519
  }
41979
42520
  }
41980
42521
  function createContextUnchecked(opts, resources) {
41981
- const home = opts.home ?? homedir14();
42522
+ const home = opts.home ?? homedir15();
41982
42523
  const project = opts.project ?? resolveProject();
41983
- const storage = createStorage(opts.database ?? join33(project.stateDir, "ui.db"), project.id);
42524
+ const storage = createStorage(opts.database ?? join34(project.stateDir, "ui.db"), project.id);
41984
42525
  resources.storage = storage;
41985
42526
  const bus = new Bus();
41986
42527
  const transcriptChange = new Emitter();
@@ -42090,7 +42631,10 @@ function createContextUnchecked(opts, resources) {
42090
42631
  const mode = PermissionMode.safeParse(row.permission_mode);
42091
42632
  if (!mode.success) return void 0;
42092
42633
  return codexSandbox(mode.data);
42093
- }
42634
+ },
42635
+ // Read at every thread/start and cold resume, so the Codex context window the operator picks in
42636
+ // Settings reaches the next thread without an app-server restart.
42637
+ getSettings: () => getSettings(storage, home)
42094
42638
  }) : void 0;
42095
42639
  resources.codexAppServer = codexAppServer;
42096
42640
  if (codexAppServer) {
@@ -45035,8 +45579,8 @@ var init_server = __esm({
45035
45579
  });
45036
45580
 
45037
45581
  // packages/server/src/repair.ts
45038
- import { existsSync as existsSync11, readFileSync as readFileSync20, writeFileSync as writeFileSync11 } from "node:fs";
45039
- import { basename as basename7, dirname as dirname14, resolve as resolve7 } from "node:path";
45582
+ import { existsSync as existsSync12, readFileSync as readFileSync22, writeFileSync as writeFileSync11 } from "node:fs";
45583
+ import { basename as basename7, dirname as dirname14, resolve as resolve8 } from "node:path";
45040
45584
  function quoteValue(v) {
45041
45585
  if (/^[\w./#:+-]+$/.test(v)) return v;
45042
45586
  return `"${v.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
@@ -45046,12 +45590,12 @@ function deriveTitle(body, slug) {
45046
45590
  return m ? m[1].trim() : slug;
45047
45591
  }
45048
45592
  function repairThreadFile(frizzDir, file) {
45049
- const root = resolve7(frizzDir);
45050
- const abs = resolve7(root, file);
45593
+ const root = resolve8(frizzDir);
45594
+ const abs = resolve8(root, file);
45051
45595
  if (dirname14(abs) !== root) throw new RepairError(`refusing to repair "${file}": not directly under .frizz/`);
45052
45596
  if (!abs.endsWith(".md")) throw new RepairError(`refusing to repair "${file}": not a .md thread file`);
45053
- if (!existsSync11(abs)) throw new RepairError(`no thread file to repair: ${basename7(abs)}`);
45054
- const content = readFileSync20(abs, "utf8");
45597
+ if (!existsSync12(abs)) throw new RepairError(`no thread file to repair: ${basename7(abs)}`);
45598
+ const content = readFileSync22(abs, "utf8");
45055
45599
  const firstLine2 = content.replace(/^/, "").split(/\r?\n/).find((l) => l.trim() !== "");
45056
45600
  if (firstLine2?.trim() === "---") {
45057
45601
  throw new RepairError(`${basename7(abs)} already opens with a "---" block \u2014 repair only heals a MISSING frontmatter block`);
@@ -45242,9 +45786,9 @@ var init_repo_files = __esm({
45242
45786
  });
45243
45787
 
45244
45788
  // cc-worker/hooks/bash-background.mjs
45245
- import { readFileSync as readFileSync21 } from "node:fs";
45789
+ import { readFileSync as readFileSync23, realpathSync as realpathSync10 } from "node:fs";
45246
45790
  import { basename as basename8 } from "node:path";
45247
- import { pathToFileURL } from "node:url";
45791
+ import { fileURLToPath as fileURLToPath4, pathToFileURL } from "node:url";
45248
45792
  function emit(obj) {
45249
45793
  process.stdout.write(JSON.stringify(obj));
45250
45794
  process.exit(0);
@@ -45380,8 +45924,14 @@ function evaluateBashBackgroundHook(input, env = process.env) {
45380
45924
  }
45381
45925
  };
45382
45926
  }
45383
- function isDirectHookExecution(argv1, moduleUrl) {
45384
- return typeof argv1 === "string" && basename8(argv1) === "bash-background.mjs" && pathToFileURL(argv1).href === moduleUrl;
45927
+ function isDirectHookExecution(argv1, moduleUrl, realpath = realpathSync10) {
45928
+ if (typeof argv1 !== "string" || basename8(argv1) !== "bash-background.mjs") return false;
45929
+ if (pathToFileURL(argv1).href === moduleUrl) return true;
45930
+ try {
45931
+ return realpath(argv1) === realpath(fileURLToPath4(moduleUrl));
45932
+ } catch {
45933
+ return false;
45934
+ }
45385
45935
  }
45386
45936
  var LOCAL_SHELL_SCRIPT;
45387
45937
  var init_bash_background = __esm({
@@ -45391,7 +45941,7 @@ var init_bash_background = __esm({
45391
45941
  if (isDirectHookExecution(process.argv[1], import.meta.url)) {
45392
45942
  try {
45393
45943
  const env = process.argv.includes("--frizz-thread") ? { ...process.env, FRIZZ_THREAD: process.env.FRIZZ_THREAD || "codex-worker" } : process.env;
45394
- emit(evaluateBashBackgroundHook(JSON.parse(readFileSync21(0, "utf8")), env));
45944
+ emit(evaluateBashBackgroundHook(JSON.parse(readFileSync23(0, "utf8")), env));
45395
45945
  } catch {
45396
45946
  emit({});
45397
45947
  }
@@ -45400,11 +45950,11 @@ var init_bash_background = __esm({
45400
45950
  });
45401
45951
 
45402
45952
  // packages/server/src/transcript.ts
45403
- import { closeSync as closeSync13, existsSync as existsSync12, fstatSync as fstatSync3, mkdirSync as mkdirSync15, openSync as openSync14, readdirSync as readdirSync9, readFileSync as readFileSync22, readSync as readSync7, renameSync as renameSync9, statSync as statSync14, unlinkSync as unlinkSync5, writeFileSync as writeFileSync12 } from "node:fs";
45953
+ import { closeSync as closeSync13, existsSync as existsSync13, fstatSync as fstatSync3, mkdirSync as mkdirSync15, openSync as openSync14, readdirSync as readdirSync9, readFileSync as readFileSync24, readSync as readSync7, renameSync as renameSync9, statSync as statSync15, unlinkSync as unlinkSync5, writeFileSync as writeFileSync12 } from "node:fs";
45404
45954
  import { createHash as createHash13 } from "node:crypto";
45405
45955
  import { StringDecoder as StringDecoder4 } from "node:string_decoder";
45406
- import { join as join34 } from "node:path";
45407
- import { homedir as homedir15 } from "node:os";
45956
+ import { join as join35 } from "node:path";
45957
+ import { homedir as homedir16 } from "node:os";
45408
45958
  function formatTokens(n) {
45409
45959
  if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
45410
45960
  if (n >= 1e3) return `${Math.round(n / 1e3)}k`;
@@ -45983,14 +46533,14 @@ function pruneScreenshotCache() {
45983
46533
  if (entries2.length <= SCREENSHOT_CACHE_MAX) return;
45984
46534
  const byMtime = entries2.map((n) => {
45985
46535
  try {
45986
- return { n, m: statSync14(join34(SCREENSHOT_CACHE_DIR, n)).mtimeMs };
46536
+ return { n, m: statSync15(join35(SCREENSHOT_CACHE_DIR, n)).mtimeMs };
45987
46537
  } catch {
45988
46538
  return { n, m: 0 };
45989
46539
  }
45990
46540
  }).sort((a, b) => b.m - a.m);
45991
46541
  for (const { n } of byMtime.slice(SCREENSHOT_CACHE_MAX)) {
45992
46542
  try {
45993
- unlinkSync5(join34(SCREENSHOT_CACHE_DIR, n));
46543
+ unlinkSync5(join35(SCREENSHOT_CACHE_DIR, n));
45994
46544
  } catch {
45995
46545
  }
45996
46546
  }
@@ -46020,14 +46570,14 @@ function persistBase64Image(mediaType, data, idKey) {
46020
46570
  const ext = IMAGE_MEDIA_EXT[typeof mediaType === "string" ? mediaType.toLowerCase() : ""];
46021
46571
  if (!ext || !data) return void 0;
46022
46572
  const name = createHash13("sha256").update(idKey).digest("hex").slice(0, 32);
46023
- const path3 = join34(SCREENSHOT_CACHE_DIR, `${name}.${ext}`);
46573
+ const path3 = join35(SCREENSHOT_CACHE_DIR, `${name}.${ext}`);
46024
46574
  try {
46025
- if (existsSync12(path3)) return path3;
46575
+ if (existsSync13(path3)) return path3;
46026
46576
  if (data.length > SCREENSHOT_MAX_BASE64) return void 0;
46027
46577
  const buf = Buffer.from(data, "base64");
46028
46578
  if (buf.length === 0 || !looksLikeImage(buf, ext)) return void 0;
46029
46579
  mkdirSync15(SCREENSHOT_CACHE_DIR, { recursive: true });
46030
- const tmp = join34(SCREENSHOT_CACHE_DIR, `.${name}.${process.pid}.${screenshotTmpSeq++}.tmp`);
46580
+ const tmp = join35(SCREENSHOT_CACHE_DIR, `.${name}.${process.pid}.${screenshotTmpSeq++}.tmp`);
46031
46581
  writeFileSync12(tmp, buf);
46032
46582
  renameSync9(tmp, path3);
46033
46583
  pruneScreenshotCache();
@@ -46042,14 +46592,14 @@ function persistSentFile(srcPath, idKey) {
46042
46592
  const outExt = ext === "jpeg" ? "jpg" : ext;
46043
46593
  try {
46044
46594
  const name = createHash13("sha256").update(idKey).digest("hex").slice(0, 32);
46045
- const dest = join34(SCREENSHOT_CACHE_DIR, `${name}.${outExt}`);
46046
- if (existsSync12(dest)) return dest;
46047
- const size = statSync14(srcPath).size;
46595
+ const dest = join35(SCREENSHOT_CACHE_DIR, `${name}.${outExt}`);
46596
+ if (existsSync13(dest)) return dest;
46597
+ const size = statSync15(srcPath).size;
46048
46598
  if (size === 0 || size > SENT_IMAGE_MAX_BYTES) return void 0;
46049
- const buf = readFileSync22(srcPath);
46599
+ const buf = readFileSync24(srcPath);
46050
46600
  if (!looksLikeImage(buf, outExt)) return void 0;
46051
46601
  mkdirSync15(SCREENSHOT_CACHE_DIR, { recursive: true });
46052
- const tmp = join34(SCREENSHOT_CACHE_DIR, `.${name}.${process.pid}.${screenshotTmpSeq++}.tmp`);
46602
+ const tmp = join35(SCREENSHOT_CACHE_DIR, `.${name}.${process.pid}.${screenshotTmpSeq++}.tmp`);
46053
46603
  writeFileSync12(tmp, buf);
46054
46604
  renameSync9(tmp, dest);
46055
46605
  pruneScreenshotCache();
@@ -46487,7 +47037,7 @@ function readTranscript(project, sessionId) {
46487
47037
  }
46488
47038
  function verifyIncrementalParse(path3, identityPrefix, incremental) {
46489
47039
  try {
46490
- const fresh = parseTranscript(readFileSync22(path3, "utf8"), identityPrefix);
47040
+ const fresh = parseTranscript(readFileSync24(path3, "utf8"), identityPrefix);
46491
47041
  const a = JSON.stringify(incremental);
46492
47042
  const b = JSON.stringify(fresh);
46493
47043
  if (a !== b) {
@@ -46507,16 +47057,16 @@ function verifyIncrementalParse(path3, identityPrefix, incremental) {
46507
47057
  }
46508
47058
  }
46509
47059
  function logDirOf(project) {
46510
- return join34(homedir15(), ".claude", "projects", project.cwdSlug);
47060
+ return join35(homedir16(), ".claude", "projects", project.cwdSlug);
46511
47061
  }
46512
47062
  function resolveTranscriptPath(project, sessionId) {
46513
- const path3 = join34(logDirOf(project), `${sessionId}.jsonl`);
47063
+ const path3 = join35(logDirOf(project), `${sessionId}.jsonl`);
46514
47064
  try {
46515
- if (statSync14(path3).size > 0) return path3;
47065
+ if (statSync15(path3).size > 0) return path3;
46516
47066
  } catch {
46517
47067
  }
46518
47068
  const stranded = discoverTranscriptDir(logDirOf(project), sessionId);
46519
- return stranded ? join34(stranded, `${sessionId}.jsonl`) : path3;
47069
+ return stranded ? join35(stranded, `${sessionId}.jsonl`) : path3;
46520
47070
  }
46521
47071
  function projectCodexTranscript(raw2, identityPrefix = "codex") {
46522
47072
  const out = [];
@@ -46581,8 +47131,9 @@ ${text2}` : text2;
46581
47131
  }
46582
47132
  case "tool-call": {
46583
47133
  const m = openAssistant(ev.at, sourceId);
46584
- const call = codexToolCall(ev.name, ev.input, ev.id);
46585
- call.status = "pending";
47134
+ const split = codexToolCards(ev.name, ev.input, ev.id);
47135
+ const call = split.owner;
47136
+ for (const card of split.cards) card.status = "pending";
46586
47137
  if (pendingCaption) {
46587
47138
  if (!call.desc) call.desc = pendingCaption;
46588
47139
  pendingCaption = void 0;
@@ -46592,8 +47143,10 @@ ${text2}` : text2;
46592
47143
  const interruptedOwner = call.name === "Interrupt process" && call.sessionId !== void 0 ? shellSessions.get(runningKey("Poll process", call.sessionId)) : void 0;
46593
47144
  if (!owner) {
46594
47145
  if (isPoll) call.backgroundState = "unknown";
46595
- pushToolPart(m, call);
46596
- m.tools.push(call);
47146
+ for (const card of split.cards) {
47147
+ pushToolPart(m, card);
47148
+ m.tools.push(card);
47149
+ }
46597
47150
  }
46598
47151
  if (call.name === "Spawn agent" && call.detail) agentDispatches.set(call.detail, { call, at: ev.at });
46599
47152
  if (ev.id) pendingCalls.set(ev.id, {
@@ -46602,7 +47155,8 @@ ${text2}` : text2;
46602
47155
  owner,
46603
47156
  interruptedOwner,
46604
47157
  orphanPoll: isPoll && !owner,
46605
- explicitBackground: codexExplicitBackground(ev.name, ev.input)
47158
+ explicitBackground: codexExplicitBackground(ev.name, ev.input),
47159
+ ...split.cards.length > 1 ? { companions: split.cards.filter((card) => card !== call), views: split.views } : {}
46606
47160
  });
46607
47161
  break;
46608
47162
  }
@@ -46612,7 +47166,17 @@ ${text2}` : text2;
46612
47166
  pendingCalls.delete(ev.id);
46613
47167
  const result = codexToolResult(ev.text);
46614
47168
  if (result.durationMs === void 0) result.durationMs = elapsedBetween(pending.at, ev.at);
46615
- if (ev.image && result.status !== "failed" && result.status !== "cancelled") {
47169
+ if (pending.companions) {
47170
+ for (const card of pending.companions) card.status = result.status === "cancelled" ? "cancelled" : "completed";
47171
+ if (result.status !== "cancelled") {
47172
+ const inline = ev.images ?? (ev.image ? [ev.image] : []);
47173
+ pending.views?.forEach((view, i) => {
47174
+ if (view.outputImage || !inline[i]) return;
47175
+ const shot = persistDataUrlImage(inline[i], `${ev.id}:view:${i}`);
47176
+ if (shot) view.outputImage = shot;
47177
+ });
47178
+ }
47179
+ } else if (ev.image && result.status !== "failed" && result.status !== "cancelled") {
46616
47180
  const shot = persistDataUrlImage(ev.image, ev.id);
46617
47181
  if (shot) (pending.owner ?? pending).call.outputImage = shot;
46618
47182
  }
@@ -46841,6 +47405,11 @@ function latestTranscriptWindow(messages) {
46841
47405
  }
46842
47406
  return [...earlierInstructions, ...messages.slice(start), ...pinned.slice(-MAX_PINNED_BACKGROUND_OPERATIONS)];
46843
47407
  }
47408
+ function codexToolCards(name, input, callId) {
47409
+ if (name === "exec" && typeof input === "string") return codexExecWrapperCards(input, callId);
47410
+ const owner = codexToolCall(name, input, callId);
47411
+ return { owner, cards: [owner] };
47412
+ }
46844
47413
  function codexToolCall(name, input, callId) {
46845
47414
  if (name === "exec" && typeof input === "string") return codexExecWrapperCall(input, callId);
46846
47415
  const obj = input && typeof input === "object" && !Array.isArray(input) ? input : {};
@@ -46999,8 +47568,32 @@ function formatCompactDuration(ms) {
46999
47568
  if (minutes < 60) return `${minutes}m`;
47000
47569
  return minutes % 60 ? `${Math.floor(minutes / 60)}h ${minutes % 60}m` : `${Math.floor(minutes / 60)}h`;
47001
47570
  }
47002
- function codexExecWrapperCall(source, callId) {
47571
+ function codexExecWrapperCards(source, callId) {
47003
47572
  const calls = wrappedInvocations(source);
47573
+ const rest = [];
47574
+ const views = [];
47575
+ const order = [];
47576
+ if (calls.length > 1) {
47577
+ for (const call of calls) {
47578
+ if (call.name !== "view_image") {
47579
+ if (!rest.length) order.push("rest");
47580
+ rest.push(call);
47581
+ continue;
47582
+ }
47583
+ const view = viewImageCall(jsStringProperty(call.args, "path"), `${callId ?? source}:view:${views.length}`);
47584
+ views.push(view);
47585
+ order.push(view);
47586
+ }
47587
+ }
47588
+ if (!views.length) {
47589
+ const owner = codexExecWrapperCall(source, callId, calls);
47590
+ return { owner, cards: [owner] };
47591
+ }
47592
+ const remainder = rest.length ? codexExecWrapperCall(source, callId, rest) : void 0;
47593
+ const cards = order.map((entry) => entry === "rest" ? remainder : entry);
47594
+ return { owner: remainder ?? views[0], cards, views };
47595
+ }
47596
+ function codexExecWrapperCall(source, callId, calls = wrappedInvocations(source)) {
47004
47597
  if (calls.length !== 1) {
47005
47598
  return {
47006
47599
  name: "Exec",
@@ -47008,7 +47601,9 @@ function codexExecWrapperCall(source, callId) {
47008
47601
  input: capToolInput(source.trim())
47009
47602
  };
47010
47603
  }
47011
- const call = calls[0];
47604
+ return wrappedSingleCall(calls[0], source, callId);
47605
+ }
47606
+ function wrappedSingleCall(call, source, callId) {
47012
47607
  if (call.name === "exec_command") {
47013
47608
  const cmd = jsStringProperty(call.args, "cmd") ?? jsStringProperty(call.args, "command");
47014
47609
  const cwd = jsStringProperty(call.args, "workdir") ?? jsStringProperty(call.args, "cwd");
@@ -47384,7 +47979,7 @@ function unifiedToolResult(text2) {
47384
47979
  if (!header) return void 0;
47385
47980
  const wrapperStatus = header[1] === "failed" ? "failed" : header[1] === "terminated" ? "cancelled" : "completed";
47386
47981
  const wrapperDurationMs = Number(header[2]) * 1e3;
47387
- const body = raw2.slice(header[0].length).trim();
47982
+ const body = raw2.slice(header[0].length).split("[image output]").join("").trim();
47388
47983
  if (!body || body === "{}") return { status: wrapperStatus, durationMs: wrapperDurationMs, terminal: true };
47389
47984
  try {
47390
47985
  const parsed = JSON.parse(body);
@@ -47498,7 +48093,7 @@ function sourceForThread(project, storage, slug, backendFor) {
47498
48093
  }
47499
48094
  if (!FOREIGN_SESSION_ID_RE.test(slug)) return void 0;
47500
48095
  const claudePath = resolveTranscriptPath(project, slug);
47501
- if (existsSync12(claudePath)) {
48096
+ if (existsSync13(claudePath)) {
47502
48097
  return { slug, sessionId: slug, nativeId: slug, backend: "claude", runtimeGeneration: 0, path: claudePath };
47503
48098
  }
47504
48099
  const rollout = (backendFor?.("codex") ?? defaultCodexBackend()).transcriptPath(slug);
@@ -47764,7 +48359,7 @@ function readEarlierThreadTranscriptPage(project, storage, slug, cursor, backend
47764
48359
  }
47765
48360
  function readCodexTranscriptFile(absPath, nativeId = absPath) {
47766
48361
  try {
47767
- return parseCodexTranscript(readFileSync22(absPath, "utf8"), `codex:${nativeId}`);
48362
+ return parseCodexTranscript(readFileSync24(absPath, "utf8"), `codex:${nativeId}`);
47768
48363
  } catch {
47769
48364
  return [];
47770
48365
  }
@@ -47862,7 +48457,7 @@ function projectTranscriptPageAgentLifecycles(page, lookup, peerLookup) {
47862
48457
  function readTranscriptFile(absPath) {
47863
48458
  try {
47864
48459
  const pathKey = createHash13("sha256").update(absPath).digest("base64url").slice(0, 16);
47865
- return parseTranscript(readFileSync22(absPath, "utf8"), `claude-file:${pathKey}`);
48460
+ return parseTranscript(readFileSync24(absPath, "utf8"), `claude-file:${pathKey}`);
47866
48461
  } catch {
47867
48462
  return [];
47868
48463
  }
@@ -47938,7 +48533,7 @@ var init_transcript = __esm({
47938
48533
  });
47939
48534
 
47940
48535
  // packages/server/src/open-external.ts
47941
- import { spawn as spawn5 } from "node:child_process";
48536
+ import { spawn as spawn6 } from "node:child_process";
47942
48537
  function validateExternalUrl(raw2) {
47943
48538
  let parsed;
47944
48539
  try {
@@ -47951,102 +48546,30 @@ function validateExternalUrl(raw2) {
47951
48546
  }
47952
48547
  return { ok: true, url: parsed.toString() };
47953
48548
  }
47954
- function openExternalUrl(raw2) {
48549
+ function externalUrlOpenCommand(url, platform3 = process.platform) {
48550
+ if (platform3 === "darwin") return { command: "open", args: [url] };
48551
+ if (platform3 === "win32") return { command: "rundll32.exe", args: ["url.dll,FileProtocolHandler", url] };
48552
+ return { command: "xdg-open", args: [url] };
48553
+ }
48554
+ async function openExternalUrl(raw2, options = {}) {
47955
48555
  const v = validateExternalUrl(raw2);
47956
48556
  if (!v.ok) throw new Error(v.reason);
47957
- const [cmd, args] = process.platform === "darwin" ? ["open", [v.url]] : ["xdg-open", [v.url]];
47958
- spawn5(cmd, args, { detached: true, stdio: "ignore" }).unref();
47959
- }
47960
- var init_open_external = __esm({
47961
- "packages/server/src/open-external.ts"() {
47962
- "use strict";
47963
- }
47964
- });
47965
-
47966
- // packages/server/src/local-file.ts
47967
- import { spawn as spawn6 } from "node:child_process";
47968
- import { readFileSync as readFileSync23, realpathSync as realpathSync9, statSync as statSync15 } from "node:fs";
47969
- import { homedir as homedir16 } from "node:os";
47970
- import { isAbsolute as isAbsolute4, join as join35, resolve as resolve8, sep as sep2 } from "node:path";
47971
- function isUnder(real, root) {
47972
- let rootReal;
48557
+ const spec = externalUrlOpenCommand(v.url, options.platform);
48558
+ const child = (options.spawn ?? spawn6)(spec.command, [...spec.args], { detached: true, stdio: "ignore", windowsHide: true });
47973
48559
  try {
47974
- rootReal = realpathSync9(root);
47975
- } catch {
47976
- return false;
47977
- }
47978
- return real === rootReal || real.startsWith(rootReal.endsWith(sep2) ? rootReal : rootReal + sep2);
47979
- }
47980
- function resolveLocalFile(rawPath, roots) {
47981
- if (!isAbsolute4(rawPath)) throw new Error("Local path must be absolute");
47982
- let real;
47983
- try {
47984
- real = realpathSync9(rawPath);
47985
- } catch {
47986
- throw new Error("Local file was not found");
47987
- }
47988
- if (!roots.some((root) => isUnder(real, root))) throw new Error("Local file is outside Frizz's trusted roots");
47989
- try {
47990
- if (!statSync15(real).isFile()) throw new Error("Local path is not a regular file");
48560
+ await awaitOpenerStart(child, spec.command);
48561
+ return { opened: true };
47991
48562
  } catch (error) {
47992
- if (error instanceof Error && error.message === "Local path is not a regular file") throw error;
47993
- throw new Error("Local file was not found");
47994
- }
47995
- return real;
47996
- }
47997
- function resolveOpenableFile(raw2, projectDir, roots, home = homedir16()) {
47998
- const trimmed = raw2.trim().replace(/:\d+(?::\d+)?$/, "");
47999
- if (!trimmed) return null;
48000
- const abs = trimmed === "~" ? home : trimmed.startsWith("~/") ? join35(home, trimmed.slice(2)) : isAbsolute4(trimmed) ? trimmed : resolve8(projectDir, trimmed);
48001
- try {
48002
- return resolveLocalFile(abs, roots);
48003
- } catch {
48004
- return null;
48005
- }
48006
- }
48007
- function readLocalTextFile(rawPath, roots) {
48008
- const path3 = resolveLocalFile(rawPath, roots);
48009
- const bytes = readFileSync23(path3);
48010
- if (bytes.subarray(0, 8192).includes(0)) throw new Error("Local file is not a text file");
48011
- if (bytes.length <= MARKDOWN_READ_LIMIT) return { path: path3, text: bytes.toString("utf8"), truncated: false };
48012
- const head = bytes.subarray(0, MARKDOWN_READ_LIMIT);
48013
- const lastBreak = head.lastIndexOf(10);
48014
- return { path: path3, text: head.subarray(0, lastBreak > 0 ? lastBreak : head.length).toString("utf8"), truncated: true };
48015
- }
48016
- function readLocalMarkdown(rawPath, roots) {
48017
- if (!MARKDOWN_FILE_EXT.test(rawPath.trim())) throw new Error("Local file is not a Markdown file");
48018
- const path3 = resolveLocalFile(rawPath, roots);
48019
- if (!MARKDOWN_FILE_EXT.test(path3)) throw new Error("Local file is not a Markdown file");
48020
- const bytes = readFileSync23(path3);
48021
- if (bytes.length <= MARKDOWN_READ_LIMIT) return { path: path3, markdown: bytes.toString("utf8"), truncated: false };
48022
- const head = bytes.subarray(0, MARKDOWN_READ_LIMIT);
48023
- const lastBreak = head.lastIndexOf(10);
48024
- return { path: path3, markdown: head.subarray(0, lastBreak > 0 ? lastBreak : head.length).toString("utf8"), truncated: true };
48025
- }
48026
- function resolveWatchableLocalFile(rawPath, roots) {
48027
- const path3 = resolveLocalFile(rawPath, roots);
48028
- if (MARKDOWN_FILE_EXT.test(rawPath.trim()) && !MARKDOWN_FILE_EXT.test(path3)) {
48029
- throw new Error("Local file is not a Markdown file");
48563
+ const message = error instanceof Error ? error.message : String(error);
48564
+ (options.onError ?? ((line) => log.warn("open-external", line)))(`could not open ${v.url}: ${message}`);
48565
+ return { opened: false, error: message };
48030
48566
  }
48031
- return path3;
48032
- }
48033
- function defaultSpawn(command, args, options) {
48034
- return spawn6(command, [...args], options);
48035
- }
48036
- function openLocalFile(rawPath, opener, roots, options = {}) {
48037
- const path3 = resolveLocalFile(rawPath, roots);
48038
- const selected = options.forceSystem ? "system" : opener;
48039
- if (selected === "copy") return { action: "copy", path: path3 };
48040
- const spec = process.platform === "darwin" ? selected === "cursor" ? ["open", ["-a", "Cursor", path3]] : selected === "vscode" ? ["open", ["-a", "Visual Studio Code", path3]] : selected === "finder" ? ["open", ["-R", path3]] : ["open", [path3]] : selected === "finder" ? ["xdg-open", [path3]] : selected === "cursor" ? ["cursor", [path3]] : selected === "vscode" ? ["code", [path3]] : ["xdg-open", [path3]];
48041
- (options.spawn ?? defaultSpawn)(spec[0], spec[1], { detached: true, stdio: "ignore", shell: false }).unref();
48042
- return { action: "opened", path: path3 };
48043
48567
  }
48044
- var MARKDOWN_FILE_EXT, MARKDOWN_READ_LIMIT;
48045
- var init_local_file = __esm({
48046
- "packages/server/src/local-file.ts"() {
48568
+ var init_open_external = __esm({
48569
+ "packages/server/src/open-external.ts"() {
48047
48570
  "use strict";
48048
- MARKDOWN_FILE_EXT = /\.(?:md|mdx|markdown)$/i;
48049
- MARKDOWN_READ_LIMIT = 1024 * 1024;
48571
+ init_local_file();
48572
+ init_logging();
48050
48573
  }
48051
48574
  });
48052
48575
 
@@ -48375,7 +48898,14 @@ var init_account_actions = __esm({
48375
48898
  function shellQuote(value) {
48376
48899
  return `'${value.replace(/'/g, `'"'"'`)}'`;
48377
48900
  }
48378
- function providerResumeCommand(backend, projectDir, sessionId) {
48901
+ function powershellQuote(value) {
48902
+ return `'${value.replace(/'/g, "''")}'`;
48903
+ }
48904
+ function providerResumeCommand(backend, projectDir, sessionId, platform3 = process.platform) {
48905
+ if (platform3 === "win32") {
48906
+ const resume2 = backend === "codex" ? `& codex resume ${powershellQuote(sessionId)} --dangerously-bypass-approvals-and-sandbox` : `& claude --resume ${powershellQuote(sessionId)} --dangerously-skip-permissions`;
48907
+ return `Set-Location -LiteralPath ${powershellQuote(projectDir)}; ${resume2}`;
48908
+ }
48379
48909
  const resume = backend === "codex" ? `codex resume ${shellQuote(sessionId)} --dangerously-bypass-approvals-and-sandbox` : `claude --resume ${shellQuote(sessionId)} --dangerously-skip-permissions`;
48380
48910
  return `cd ${shellQuote(projectDir)} && ${resume}`;
48381
48911
  }
@@ -48723,11 +49253,11 @@ var init_directory_picker = __esm({
48723
49253
  });
48724
49254
 
48725
49255
  // packages/server/src/router.ts
48726
- import { readFileSync as readFileSync24, statSync as statSync17 } from "node:fs";
49256
+ import { readFileSync as readFileSync25, statSync as statSync17 } from "node:fs";
48727
49257
  import { join as join36, resolve as resolve9 } from "node:path";
48728
49258
  import { randomUUID as randomUUID16 } from "node:crypto";
48729
49259
  import { basename as basename9, dirname as dirname15 } from "node:path";
48730
- import { existsSync as existsSync13, mkdirSync as mkdirSync16, rmSync as rmSync10, writeFileSync as writeFileSync13 } from "node:fs";
49260
+ import { existsSync as existsSync14, mkdirSync as mkdirSync16, rmSync as rmSync10, writeFileSync as writeFileSync13 } from "node:fs";
48731
49261
  import { homedir as homedir17 } from "node:os";
48732
49262
  function validateGithubDispatchProfile(input) {
48733
49263
  validateThreadProfile(input.backend, input.model, input.effort);
@@ -48937,7 +49467,7 @@ function storeProjectIcon(id, name, bytes) {
48937
49467
  function setProjectIconFromFile(id, file) {
48938
49468
  let bytes;
48939
49469
  try {
48940
- bytes = readFileSync24(file);
49470
+ bytes = readFileSync25(file);
48941
49471
  } catch {
48942
49472
  throw new Error("That file could not be read.");
48943
49473
  }
@@ -50164,7 +50694,7 @@ ${gapNote}` : input.message;
50164
50694
  since: new Date(w.created_at).toISOString()
50165
50695
  });
50166
50696
  }
50167
- return { activity, questions: openQuestionViews(input.slug) };
50697
+ return { activity, questions: openQuestionViews(input.slug), links: ctx.storage.listThreadLinks(input.slug).map(threadLinkView) };
50168
50698
  }
50169
50699
  }),
50170
50700
  // THE WATCHER REGISTRY WAS DELETED ON 2026-08-14 AND CAME BACK ON 2026-08-26, under two narrow verbs
@@ -50178,6 +50708,34 @@ ${gapNote}` : input.message;
50178
50708
  // binary naming them and gets a 404, which is the honest answer: its arguments do not fit this
50179
50709
  // registry (there is no `for:` in them at all), so an alias would have to invent the one field that
50180
50710
  // must not be guessed at.
50711
+ upsertOwnLink: mutation({
50712
+ input: UpsertOwnLinkInput,
50713
+ output: UpsertOwnLinkResult,
50714
+ handler: async ({ input }) => {
50715
+ const row = ctx.storage.getSession(input.slug);
50716
+ if (!row) throw new Error(`thread ${input.slug} is not registered`);
50717
+ if (row.state === "archived" || row.archived === 1) throw new Error("Reopen this thread before registering a link");
50718
+ const destination = resolveThreadLink(input.target, ctx.project.dir, openRoots);
50719
+ const link = ctx.storage.upsertThreadLink({
50720
+ id: `lnk_${randomUUID16().replace(/-/g, "").slice(0, 12)}`,
50721
+ slug: input.slug,
50722
+ label: input.label,
50723
+ ...destination,
50724
+ createdAtMs: Date.now()
50725
+ });
50726
+ ctx.board.refresh();
50727
+ return { link: threadLinkView(link) };
50728
+ }
50729
+ }),
50730
+ dropOwnLink: mutation({
50731
+ input: DropOwnLinkInput,
50732
+ output: DropOwnLinkResult,
50733
+ handler: async ({ input }) => {
50734
+ const dropped = ctx.storage.dropThreadLink(input.slug, input.id);
50735
+ if (dropped) ctx.board.refresh();
50736
+ return { dropped };
50737
+ }
50738
+ }),
50181
50739
  // ---- REGISTERED PR WATCHERS (add / drop / list) ---------------------------------------------
50182
50740
  // The worker's own PR watchers, from `mcp__frizz__watch_pr`. Same caller and therefore the same rules
50183
50741
  // as the timers above: slug-only (the MCP server outlives the session ids underneath it), and no
@@ -50529,7 +51087,8 @@ Say which way you went and why in your write-up, so the human can course-correct
50529
51087
  openExternal: mutation({
50530
51088
  input: external_exports.object({ url: external_exports.string() }),
50531
51089
  handler: async ({ input }) => {
50532
- openExternalUrl(input.url);
51090
+ const result = await openExternalUrl(input.url);
51091
+ if (!result.opened) throw new Error(result.error);
50533
51092
  }
50534
51093
  }),
50535
51094
  // A local file can be opened only after its canonical real path is contained by the openable roots
@@ -50937,7 +51496,7 @@ Say which way you went and why in your write-up, so the human can course-correct
50937
51496
  }
50938
51497
  const updated = clearProjectIcon(input.id);
50939
51498
  if (!updated) throw new Error("No such project.");
50940
- return projectCard(updated, !existsSync13(updated.path));
51499
+ return projectCard(updated, !existsSync14(updated.path));
50941
51500
  }
50942
51501
  }),
50943
51502
  /**
@@ -51121,6 +51680,7 @@ var init_router5 = __esm({
51121
51680
  init_open_external();
51122
51681
  init_local_file();
51123
51682
  init_project();
51683
+ init_thread_links();
51124
51684
  init_github();
51125
51685
  init_github_hovercard();
51126
51686
  init_dispatch();
@@ -51342,21 +51902,21 @@ var init_compression = __esm({
51342
51902
  });
51343
51903
 
51344
51904
  // packages/server/src/local-image.ts
51345
- import { readFileSync as readFileSync25, realpathSync as realpathSync10, statSync as statSync18 } from "node:fs";
51346
- import { extname as extname2, isAbsolute as isAbsolute5 } from "node:path";
51905
+ import { readFileSync as readFileSync26, realpathSync as realpathSync11, statSync as statSync18 } from "node:fs";
51906
+ import { extname as extname2, isAbsolute as isAbsolute6 } from "node:path";
51347
51907
  function resolveLocalImage(rawPath) {
51348
- if (!rawPath || !isAbsolute5(rawPath)) return { status: 400 };
51908
+ if (!rawPath || !isAbsolute6(rawPath)) return { status: 400 };
51349
51909
  const contentType = IMAGE_CONTENT_TYPE[extname2(rawPath).toLowerCase()];
51350
51910
  if (!contentType) return { status: 400 };
51351
51911
  let real;
51352
51912
  try {
51353
- real = realpathSync10(rawPath);
51913
+ real = realpathSync11(rawPath);
51354
51914
  } catch {
51355
51915
  return { status: 404 };
51356
51916
  }
51357
51917
  try {
51358
51918
  if (!statSync18(real).isFile()) return { status: 404 };
51359
- return { status: 200, contentType, body: readFileSync25(real) };
51919
+ return { status: 200, contentType, body: readFileSync26(real) };
51360
51920
  } catch {
51361
51921
  return { status: 404 };
51362
51922
  }
@@ -51376,7 +51936,7 @@ var init_local_image = __esm({
51376
51936
  });
51377
51937
 
51378
51938
  // packages/server/src/project-icon.ts
51379
- import { readFileSync as readFileSync26, readdirSync as readdirSync10, realpathSync as realpathSync11, statSync as statSync19 } from "node:fs";
51939
+ import { readFileSync as readFileSync27, readdirSync as readdirSync10, realpathSync as realpathSync12, statSync as statSync19 } from "node:fs";
51380
51940
  import { homedir as homedir18 } from "node:os";
51381
51941
  import { basename as basename10, dirname as dirname16, extname as extname3, join as join37, relative as relative2, sep as sep3 } from "node:path";
51382
51942
  function normalizeStem(stem) {
@@ -51438,7 +51998,7 @@ function iconDirectories(root) {
51438
51998
  function manifestIcons(manifestPath) {
51439
51999
  let parsed;
51440
52000
  try {
51441
- parsed = JSON.parse(readFileSync26(manifestPath, "utf8"));
52001
+ parsed = JSON.parse(readFileSync27(manifestPath, "utf8"));
51442
52002
  } catch {
51443
52003
  return [];
51444
52004
  }
@@ -51525,21 +52085,21 @@ function resolveProjectIconResponse(id, options = {}) {
51525
52085
  if (!contentType) return { status: 404 };
51526
52086
  let real;
51527
52087
  try {
51528
- real = realpathSync11(path3);
52088
+ real = realpathSync12(path3);
51529
52089
  if (!statSync19(real).isFile()) return { status: 404 };
51530
52090
  } catch {
51531
52091
  return { status: 404 };
51532
52092
  }
51533
52093
  const roots = [entry.path, projectStateDir(id, home)].map((root) => {
51534
52094
  try {
51535
- return realpathSync11(root);
52095
+ return realpathSync12(root);
51536
52096
  } catch {
51537
52097
  return root;
51538
52098
  }
51539
52099
  });
51540
52100
  if (!roots.some((root) => real === root || real.startsWith(root + sep3))) return { status: 404 };
51541
52101
  try {
51542
- return { status: 200, contentType, body: readFileSync26(real) };
52102
+ return { status: 200, contentType, body: readFileSync27(real) };
51543
52103
  } catch {
51544
52104
  return { status: 404 };
51545
52105
  }
@@ -51651,7 +52211,7 @@ var init_project_icon = __esm({
51651
52211
  });
51652
52212
 
51653
52213
  // packages/server/src/local-visualization.ts
51654
- import { readdirSync as readdirSync11, readFileSync as readFileSync27, realpathSync as realpathSync12, statSync as statSync20 } from "node:fs";
52214
+ import { readdirSync as readdirSync11, readFileSync as readFileSync28, realpathSync as realpathSync13, statSync as statSync20 } from "node:fs";
51655
52215
  import { join as join38, sep as sep4 } from "node:path";
51656
52216
  function children(path3, pattern) {
51657
52217
  try {
@@ -51668,8 +52228,8 @@ function resolveFragment(projectDir, sessionId, file) {
51668
52228
  let projectReal;
51669
52229
  let baseReal;
51670
52230
  try {
51671
- projectReal = realpathSync12(projectDir);
51672
- baseReal = realpathSync12(base);
52231
+ projectReal = realpathSync13(projectDir);
52232
+ baseReal = realpathSync13(base);
51673
52233
  } catch {
51674
52234
  return null;
51675
52235
  }
@@ -51682,7 +52242,7 @@ function resolveFragment(projectDir, sessionId, file) {
51682
52242
  const sessionRoot = join38(baseReal, year, month, day, sessionId);
51683
52243
  const candidate = join38(monthDir, day, sessionId, file);
51684
52244
  try {
51685
- const real = realpathSync12(candidate);
52245
+ const real = realpathSync13(candidate);
51686
52246
  if (isUnder2(real, sessionRoot) && statSync20(real).isFile()) return real;
51687
52247
  } catch {
51688
52248
  }
@@ -51766,7 +52326,7 @@ function resolveLocalVisualization(projectDir, sessionId, file) {
51766
52326
  try {
51767
52327
  const size = statSync20(path3).size;
51768
52328
  if (size > MAX_FRAGMENT_BYTES) return { status: 413 };
51769
- fragment = readFileSync27(path3, "utf8");
52329
+ fragment = readFileSync28(path3, "utf8");
51770
52330
  } catch {
51771
52331
  return { status: 404 };
51772
52332
  }
@@ -53288,7 +53848,7 @@ var init_local_file_watch = __esm({
53288
53848
  });
53289
53849
 
53290
53850
  // packages/server/src/boot-progress.ts
53291
- import { mkdirSync as mkdirSync18, readFileSync as readFileSync28, renameSync as renameSync10, rmSync as rmSync11, writeFileSync as writeFileSync15 } from "node:fs";
53851
+ import { mkdirSync as mkdirSync18, readFileSync as readFileSync29, renameSync as renameSync10, rmSync as rmSync11, writeFileSync as writeFileSync15 } from "node:fs";
53292
53852
  import { basename as basename12, dirname as dirname18, join as join40 } from "node:path";
53293
53853
  function bootProgressPath(stateDir) {
53294
53854
  return join40(stateDir, BOOT_PROGRESS_NAME);
@@ -53336,30 +53896,111 @@ var init_boot_progress = __esm({
53336
53896
  }
53337
53897
  });
53338
53898
 
53899
+ // packages/server/src/runtime-lease.ts
53900
+ import { existsSync as existsSync15, mkdirSync as mkdirSync19, readdirSync as readdirSync12, readFileSync as readFileSync30, unlinkSync as unlinkSync6, writeFileSync as writeFileSync16 } from "node:fs";
53901
+ import { dirname as dirname19, join as join41 } from "node:path";
53902
+ function runtimeVersionDir(bin) {
53903
+ let dir = dirname19(bin);
53904
+ for (let step = 0; step < MAX_ASCENT; step++) {
53905
+ if (existsSync15(join41(dir, PROVISIONED_MARKER))) return dir;
53906
+ const parent = dirname19(dir);
53907
+ if (parent === dir) return void 0;
53908
+ dir = parent;
53909
+ }
53910
+ return void 0;
53911
+ }
53912
+ function pidAlive4(pid) {
53913
+ try {
53914
+ process.kill(pid, 0);
53915
+ return true;
53916
+ } catch (error) {
53917
+ return error.code === "EPERM";
53918
+ }
53919
+ }
53920
+ function leaseRuntime(bin, role) {
53921
+ const versionDir = runtimeVersionDir(bin);
53922
+ if (!versionDir) return () => {
53923
+ };
53924
+ const path3 = join41(versionDir, LEASES_DIR, `${process.pid}.json`);
53925
+ try {
53926
+ mkdirSync19(dirname19(path3), { recursive: true });
53927
+ const lease = { pid: process.pid, role, bin, at: (/* @__PURE__ */ new Date()).toISOString() };
53928
+ writeFileSync16(path3, JSON.stringify(lease));
53929
+ } catch {
53930
+ return () => {
53931
+ };
53932
+ }
53933
+ return () => {
53934
+ try {
53935
+ unlinkSync6(path3);
53936
+ } catch {
53937
+ }
53938
+ };
53939
+ }
53940
+ function liveRuntimeLeases(versionDir) {
53941
+ const dir = join41(versionDir, LEASES_DIR);
53942
+ let names;
53943
+ try {
53944
+ names = readdirSync12(dir);
53945
+ } catch {
53946
+ return [];
53947
+ }
53948
+ const live = [];
53949
+ for (const name of names) {
53950
+ if (!name.endsWith(".json")) continue;
53951
+ const path3 = join41(dir, name);
53952
+ let lease;
53953
+ try {
53954
+ lease = JSON.parse(readFileSync30(path3, "utf8"));
53955
+ } catch {
53956
+ continue;
53957
+ }
53958
+ if (typeof lease.pid !== "number" || !pidAlive4(lease.pid)) {
53959
+ try {
53960
+ unlinkSync6(path3);
53961
+ } catch {
53962
+ }
53963
+ ;
53964
+ continue;
53965
+ }
53966
+ live.push({ pid: lease.pid, role: typeof lease.role === "string" ? lease.role : "unknown", bin: typeof lease.bin === "string" ? lease.bin : "", at: typeof lease.at === "string" ? lease.at : "" });
53967
+ }
53968
+ return live;
53969
+ }
53970
+ var PROVISIONED_MARKER, LEASES_DIR, MAX_ASCENT;
53971
+ var init_runtime_lease = __esm({
53972
+ "packages/server/src/runtime-lease.ts"() {
53973
+ "use strict";
53974
+ PROVISIONED_MARKER = "provisioned.json";
53975
+ LEASES_DIR = ".leases";
53976
+ MAX_ASCENT = 6;
53977
+ }
53978
+ });
53979
+
53339
53980
  // packages/server/src/runtimes.ts
53340
53981
  import { createHash as createHash15 } from "node:crypto";
53341
53982
  import {
53342
53983
  chmodSync,
53343
53984
  closeSync as closeSync16,
53344
- existsSync as existsSync14,
53345
- mkdirSync as mkdirSync19,
53985
+ existsSync as existsSync16,
53986
+ mkdirSync as mkdirSync20,
53346
53987
  openSync as openSync17,
53347
- readFileSync as readFileSync29,
53348
- readdirSync as readdirSync12,
53988
+ readFileSync as readFileSync31,
53989
+ readdirSync as readdirSync13,
53349
53990
  renameSync as renameSync11,
53350
53991
  rmSync as rmSync12,
53351
53992
  statSync as statSync22,
53352
- writeFileSync as writeFileSync16,
53993
+ writeFileSync as writeFileSync17,
53353
53994
  writeSync as writeSync2
53354
53995
  } from "node:fs";
53355
- import { dirname as dirname19, isAbsolute as isAbsolute6, join as join41, normalize, relative as relative3, sep as sep5 } from "node:path";
53996
+ import { dirname as dirname20, isAbsolute as isAbsolute7, join as join42, normalize, relative as relative3, sep as sep5 } from "node:path";
53356
53997
  import { Readable, Transform, Writable as Writable3 } from "node:stream";
53357
53998
  import { pipeline } from "node:stream/promises";
53358
53999
  import { createGunzip } from "node:zlib";
53359
54000
  function runtimesRoot(env = process.env) {
53360
54001
  const override = env[RUNTIMES_DIR_ENV];
53361
- if (override && isAbsolute6(override)) return override;
53362
- return join41(frizzRoots().cache, "runtimes");
54002
+ if (override && isAbsolute7(override)) return override;
54003
+ return join42(frizzRoots().cache, "runtimes");
53363
54004
  }
53364
54005
  function isMusl() {
53365
54006
  if (process.platform !== "linux") return false;
@@ -53376,16 +54017,16 @@ function runtimeCoordinates(backend, platform3 = process.platform, arch = proces
53376
54017
  return { pkg: "@openai/codex", packageVersion: `${CODEX_VERSION}-${platform3}-${arch}`, label: CODEX_VERSION, binary: `codex${exe}` };
53377
54018
  }
53378
54019
  function provisionedBinary(backend, root, label) {
53379
- const dir = join41(root, backend, label);
54020
+ const dir = join42(root, backend, label);
53380
54021
  let marker;
53381
54022
  try {
53382
- marker = JSON.parse(readFileSync29(join41(dir, MARKER), "utf8"));
54023
+ marker = JSON.parse(readFileSync31(join42(dir, MARKER), "utf8"));
53383
54024
  } catch {
53384
54025
  return void 0;
53385
54026
  }
53386
54027
  if (typeof marker.binary !== "string" || marker.label !== label) return void 0;
53387
- const bin = join41(dir, marker.binary);
53388
- return existsSync14(bin) ? bin : void 0;
54028
+ const bin = join42(dir, marker.binary);
54029
+ return existsSync16(bin) ? bin : void 0;
53389
54030
  }
53390
54031
  function octal(buffer, offset, length) {
53391
54032
  const text2 = buffer.toString("latin1", offset, offset + length).replace(/\0.*$/su, "").trim();
@@ -53398,27 +54039,27 @@ function safeEntryPath(dest, name) {
53398
54039
  const parts = name.split("/").filter((part) => part.length > 0);
53399
54040
  if (parts.length < 2) return void 0;
53400
54041
  const rel = normalize(parts.slice(1).join(sep5));
53401
- if (isAbsolute6(rel) || rel === ".." || rel.startsWith(`..${sep5}`)) return void 0;
53402
- const full = join41(dest, rel);
54042
+ if (isAbsolute7(rel) || rel === ".." || rel.startsWith(`..${sep5}`)) return void 0;
54043
+ const full = join42(dest, rel);
53403
54044
  const back = relative3(dest, full);
53404
- if (back.startsWith("..") || isAbsolute6(back)) return void 0;
54045
+ if (back.startsWith("..") || isAbsolute7(back)) return void 0;
53405
54046
  return full;
53406
54047
  }
53407
54048
  function locateBinary(dir, backend, binary) {
53408
54049
  if (backend === "claude") {
53409
- const direct = join41(dir, binary);
53410
- return existsSync14(direct) ? direct : void 0;
54050
+ const direct = join42(dir, binary);
54051
+ return existsSync16(direct) ? direct : void 0;
53411
54052
  }
53412
- const vendor = join41(dir, "vendor");
54053
+ const vendor = join42(dir, "vendor");
53413
54054
  let triples;
53414
54055
  try {
53415
- triples = readdirSync12(vendor);
54056
+ triples = readdirSync13(vendor);
53416
54057
  } catch {
53417
54058
  return void 0;
53418
54059
  }
53419
54060
  for (const triple of triples) {
53420
- const candidate = join41(vendor, triple, "bin", binary);
53421
- if (existsSync14(candidate)) return candidate;
54061
+ const candidate = join42(vendor, triple, "bin", binary);
54062
+ if (existsSync16(candidate)) return candidate;
53422
54063
  }
53423
54064
  return void 0;
53424
54065
  }
@@ -53433,12 +54074,12 @@ async function provisionRuntime(backend, options) {
53433
54074
  const registry = options.registry ?? REGISTRY;
53434
54075
  const progress = options.onProgress ?? (() => {
53435
54076
  });
53436
- const backendDir = join41(root, backend);
53437
- const finalDir = join41(backendDir, coordinates.label);
53438
- const partial = join41(backendDir, `.partial-${coordinates.label}-${process.pid}`);
53439
- mkdirSync19(backendDir, { recursive: true });
54077
+ const backendDir = join42(root, backend);
54078
+ const finalDir = join42(backendDir, coordinates.label);
54079
+ const partial = join42(backendDir, `.partial-${coordinates.label}-${process.pid}`);
54080
+ mkdirSync20(backendDir, { recursive: true });
53440
54081
  rmSync12(partial, { recursive: true, force: true });
53441
- mkdirSync19(partial);
54082
+ mkdirSync20(partial);
53442
54083
  try {
53443
54084
  const manifestUrl = `${registry}/${coordinates.pkg.replace("/", "%2F")}/${coordinates.packageVersion}`;
53444
54085
  progress(`resolving ${coordinates.pkg}@${coordinates.packageVersion}`);
@@ -53482,10 +54123,19 @@ async function provisionRuntime(backend, options) {
53482
54123
  binary: relative3(partial, bin),
53483
54124
  at: (/* @__PURE__ */ new Date()).toISOString()
53484
54125
  };
53485
- writeFileSync16(join41(partial, MARKER), `${JSON.stringify(marker, null, 2)}
54126
+ writeFileSync17(join42(partial, MARKER), `${JSON.stringify(marker, null, 2)}
53486
54127
  `, "utf8");
54128
+ const rename2 = options.rename ?? renameSync11;
53487
54129
  try {
53488
- renameSync11(partial, finalDir);
54130
+ for (let attempt = 1; ; attempt++) {
54131
+ try {
54132
+ rename2(partial, finalDir);
54133
+ break;
54134
+ } catch (err) {
54135
+ if (attempt >= RENAME_ATTEMPTS || !TRANSIENT_HANDLE_CODES.has(errorCode4(err) ?? "") || existsSync16(finalDir)) throw err;
54136
+ await new Promise((resolve12) => setTimeout(resolve12, options.retryDelayMs ?? RENAME_RETRY_DELAY_MS));
54137
+ }
54138
+ }
53489
54139
  } catch (err) {
53490
54140
  const theirs = provisionedBinary(backend, root, coordinates.label);
53491
54141
  if (!theirs) throw err;
@@ -53493,35 +54143,48 @@ async function provisionRuntime(backend, options) {
53493
54143
  return { bin: theirs, label: coordinates.label, fetched: false };
53494
54144
  }
53495
54145
  progress("ready");
53496
- return { bin: join41(finalDir, marker.binary), label: coordinates.label, fetched: true };
54146
+ return { bin: join42(finalDir, marker.binary), label: coordinates.label, fetched: true };
53497
54147
  } catch (err) {
53498
54148
  rmSync12(partial, { recursive: true, force: true });
53499
54149
  throw err;
53500
54150
  }
53501
54151
  }
53502
- function sweepRuntimes(backend, root, keepLabel, now = Date.now()) {
53503
- const backendDir = join41(root, backend);
54152
+ function sweepRuntimes(backend, root, keepLabel, now = Date.now(), deps = {}) {
54153
+ const rm2 = deps.rm ?? defaultSweepRm;
54154
+ const backendDir = join42(root, backend);
53504
54155
  let entries2;
53505
54156
  try {
53506
- entries2 = readdirSync12(backendDir);
54157
+ entries2 = readdirSync13(backendDir);
53507
54158
  } catch {
53508
- return [];
54159
+ return { removed: [], kept: [] };
53509
54160
  }
53510
- const removed = [];
54161
+ const sweep = { removed: [], kept: [] };
53511
54162
  for (const entry of entries2) {
53512
54163
  if (entry === keepLabel) continue;
53513
- const full = join41(backendDir, entry);
54164
+ const full = join42(backendDir, entry);
53514
54165
  if (entry.startsWith(".partial-")) {
53515
54166
  try {
53516
54167
  if (now - statSync22(full).mtimeMs < 24 * 60 * 60 * 1e3) continue;
53517
54168
  } catch {
53518
54169
  continue;
53519
54170
  }
54171
+ } else {
54172
+ const leases = liveRuntimeLeases(full);
54173
+ if (leases.length > 0) {
54174
+ sweep.kept.push({ dir: full, leases: leases.map(({ pid, role }) => ({ pid, role })) });
54175
+ continue;
54176
+ }
54177
+ }
54178
+ try {
54179
+ rm2(full);
54180
+ sweep.removed.push(full);
54181
+ } catch (err) {
54182
+ const code = errorCode4(err);
54183
+ const reason = code && TRANSIENT_HANDLE_CODES.has(code) ? `in use (${code})` : `could not remove: ${err instanceof Error ? err.message : String(err)}`;
54184
+ sweep.kept.push({ dir: full, leases: [], reason });
53520
54185
  }
53521
- rmSync12(full, { recursive: true, force: true });
53522
- removed.push(full);
53523
54186
  }
53524
- return removed;
54187
+ return sweep;
53525
54188
  }
53526
54189
  async function resolveOne(backend, explicit, options) {
53527
54190
  const env = options.env ?? process.env;
@@ -53538,22 +54201,34 @@ async function resolveOne(backend, explicit, options) {
53538
54201
  return { bin: bare, source: "path", version: "unknown", note };
53539
54202
  }
53540
54203
  const root = options.root ?? runtimesRoot(env);
54204
+ let provisioned;
53541
54205
  try {
53542
- const provisioned = await provisionRuntime(backend, {
54206
+ provisioned = await provisionRuntime(backend, {
53543
54207
  root,
53544
54208
  coordinates,
53545
54209
  fetch: options.fetch,
53546
54210
  registry: options.registry,
54211
+ rename: options.rename,
53547
54212
  onProgress: (message) => options.onProgress?.(backend, message)
53548
54213
  });
53549
- if (provisioned.fetched) log3("info", `runtimes: provisioned ${backend} ${provisioned.label} at ${provisioned.bin}`);
53550
- for (const retired of sweepRuntimes(backend, root, coordinates.label)) log3("info", `runtimes: retired ${retired}`);
53551
- return { bin: provisioned.bin, source: "provisioned", version: provisioned.label };
53552
54214
  } catch (err) {
53553
54215
  const note = err instanceof Error ? err.message : String(err);
53554
54216
  log3("warn", `runtimes: could not provision ${backend} ${coordinates.label} (${note}); using ${bare} from PATH \u2014 the version Frizz was built against is not what will run`);
53555
54217
  return { bin: bare, source: "path", version: "unknown", note };
53556
54218
  }
54219
+ if (provisioned.fetched) log3("info", `runtimes: provisioned ${backend} ${provisioned.label} at ${provisioned.bin}`);
54220
+ leaseRuntime(provisioned.bin, "server");
54221
+ try {
54222
+ const sweep = sweepRuntimes(backend, root, coordinates.label, Date.now(), { rm: options.rm });
54223
+ for (const retired of sweep.removed) log3("info", `runtimes: retired ${retired}`);
54224
+ for (const { dir, leases, reason } of sweep.kept) {
54225
+ const why = reason ?? `still used by ${leases.map((lease) => `${lease.role} pid ${lease.pid}`).join(", ")}`;
54226
+ log3("info", `runtimes: kept ${dir} \u2014 ${why}`);
54227
+ }
54228
+ } catch (err) {
54229
+ log3("warn", `runtimes: sweep of superseded ${backend} versions failed (${err instanceof Error ? err.message : String(err)}); the pin is unaffected`);
54230
+ }
54231
+ return { bin: provisioned.bin, source: "provisioned", version: provisioned.label };
53557
54232
  }
53558
54233
  async function resolveRuntimes(options = {}) {
53559
54234
  const [claude, codex] = await Promise.all([
@@ -53572,12 +54247,13 @@ function describeRuntime(backend, runtime) {
53572
54247
  return `${backend}: ${runtime.bin} from PATH${runtime.note ? ` \u2014 ${runtime.note}` : ""}`;
53573
54248
  }
53574
54249
  }
53575
- var CLAUDE_AGENT_SDK_VERSION, CLAUDE_CODE_VERSION, CODEX_VERSION, RUNTIMES_MODE_ENV, RUNTIMES_DIR_ENV, CLAUDE_BIN_ENV, CODEX_BIN_ENV, REGISTRY, MARKER, BLOCK, TarExtractor;
54250
+ var CLAUDE_AGENT_SDK_VERSION, CLAUDE_CODE_VERSION, CODEX_VERSION, RUNTIMES_MODE_ENV, RUNTIMES_DIR_ENV, CLAUDE_BIN_ENV, CODEX_BIN_ENV, REGISTRY, MARKER, BLOCK, TarExtractor, RENAME_ATTEMPTS, RENAME_RETRY_DELAY_MS, TRANSIENT_HANDLE_CODES, errorCode4, defaultSweepRm;
53576
54251
  var init_runtimes = __esm({
53577
54252
  "packages/server/src/runtimes.ts"() {
53578
54253
  "use strict";
53579
54254
  init_codex_app_server();
53580
54255
  init_frizz_paths();
54256
+ init_runtime_lease();
53581
54257
  CLAUDE_AGENT_SDK_VERSION = "0.3.268";
53582
54258
  CLAUDE_CODE_VERSION = "2.1.268";
53583
54259
  CODEX_VERSION = CODEX_APP_SERVER_SUPPORTED_VERSION;
@@ -53668,7 +54344,7 @@ var init_runtimes = __esm({
53668
54344
  return;
53669
54345
  case "5": {
53670
54346
  const dir = safeEntryPath(this.dest, name);
53671
- if (dir) mkdirSync19(dir, { recursive: true });
54347
+ if (dir) mkdirSync20(dir, { recursive: true });
53672
54348
  return;
53673
54349
  }
53674
54350
  case "0":
@@ -53676,7 +54352,7 @@ var init_runtimes = __esm({
53676
54352
  case "7": {
53677
54353
  const file = safeEntryPath(this.dest, name);
53678
54354
  if (!file) return;
53679
- mkdirSync19(dirname19(file), { recursive: true });
54355
+ mkdirSync20(dirname20(file), { recursive: true });
53680
54356
  this.fd = openSync17(file, "w", octal(header, 100, 8) & 511 || 420);
53681
54357
  this.written.push(file);
53682
54358
  return;
@@ -53716,6 +54392,11 @@ var init_runtimes = __esm({
53716
54392
  }
53717
54393
  }
53718
54394
  };
54395
+ RENAME_ATTEMPTS = 5;
54396
+ RENAME_RETRY_DELAY_MS = 200;
54397
+ TRANSIENT_HANDLE_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
54398
+ errorCode4 = (err) => err?.code;
54399
+ defaultSweepRm = (path3) => rmSync12(path3, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
53719
54400
  }
53720
54401
  });
53721
54402
 
@@ -54248,18 +54929,18 @@ var init_legacy_project_db = __esm({
54248
54929
  });
54249
54930
 
54250
54931
  // packages/server/src/frizz-db.ts
54251
- import { existsSync as existsSync15, mkdirSync as mkdirSync20, readdirSync as readdirSync13, renameSync as renameSync12, statSync as statSync23 } from "node:fs";
54252
- import { basename as basename13, dirname as dirname20, join as join42 } from "node:path";
54932
+ import { existsSync as existsSync17, mkdirSync as mkdirSync21, readdirSync as readdirSync14, renameSync as renameSync12, statSync as statSync23 } from "node:fs";
54933
+ import { basename as basename13, dirname as dirname21, join as join43 } from "node:path";
54253
54934
  function frizzDatabaseLocation(options) {
54254
54935
  if (options.stateDir !== void 0) {
54255
- const projectsDir = dirname20(options.stateDir);
54936
+ const projectsDir = dirname21(options.stateDir);
54256
54937
  if (basename13(projectsDir) === "projects") {
54257
- return { path: options.path ?? join42(dirname20(projectsDir), "ui.db"), projectsDir };
54938
+ return { path: options.path ?? join43(dirname21(projectsDir), "ui.db"), projectsDir };
54258
54939
  }
54259
- return { path: options.path ?? join42(options.stateDir, "ui.db"), projectsDir: void 0 };
54940
+ return { path: options.path ?? join43(options.stateDir, "ui.db"), projectsDir: void 0 };
54260
54941
  }
54261
54942
  const data = options.home ? frizzPaths({ home: options.home }).data : frizzRoots().data;
54262
- return { path: options.path ?? join42(data, "ui.db"), projectsDir: join42(data, "projects") };
54943
+ return { path: options.path ?? join43(data, "ui.db"), projectsDir: join43(data, "projects") };
54263
54944
  }
54264
54945
  function ensureFrizzSchema(db) {
54265
54946
  db.exec(`
@@ -54278,7 +54959,7 @@ function ensureFrizzSchema(db) {
54278
54959
  }
54279
54960
  function openFrizzDatabase(options = {}) {
54280
54961
  const { path: path3, projectsDir } = frizzDatabaseLocation(options);
54281
- mkdirSync20(dirname20(path3), { recursive: true });
54962
+ mkdirSync21(dirname21(path3), { recursive: true });
54282
54963
  const db = new sqlite_default(path3);
54283
54964
  db.pragma("busy_timeout = 5000");
54284
54965
  db.pragma("journal_mode = WAL");
@@ -54290,10 +54971,10 @@ function openFrizzDatabase(options = {}) {
54290
54971
  );
54291
54972
  for (const projectId of retiredLegacyProjectIds(projectsDir)) {
54292
54973
  if (alreadyImported.has(projectId)) continue;
54293
- const dir = join42(projectsDir, projectId);
54974
+ const dir = join43(projectsDir, projectId);
54294
54975
  try {
54295
- renameSync12(join42(dir, "ui.db.imported"), join42(dir, "ui.db"));
54296
- db.prepare("INSERT INTO imported_project (project_id, source, rows, imported_at) VALUES (?, ?, 0, ?)").run(projectId, join42(dir, "ui.db"), (/* @__PURE__ */ new Date()).toISOString());
54976
+ renameSync12(join43(dir, "ui.db.imported"), join43(dir, "ui.db"));
54977
+ db.prepare("INSERT INTO imported_project (project_id, source, rows, imported_at) VALUES (?, ?, 0, ?)").run(projectId, join43(dir, "ui.db"), (/* @__PURE__ */ new Date()).toISOString());
54297
54978
  alreadyImported.add(projectId);
54298
54979
  log.info("frizz-db", `${projectId}: restored ui.db.imported to ui.db and recorded it as imported`);
54299
54980
  } catch (error) {
@@ -54303,7 +54984,7 @@ function openFrizzDatabase(options = {}) {
54303
54984
  }
54304
54985
  for (const projectId of legacyProjectIds(projectsDir)) {
54305
54986
  if (alreadyImported.has(projectId)) continue;
54306
- const legacyPath = join42(projectsDir, projectId, "ui.db");
54987
+ const legacyPath = join43(projectsDir, projectId, "ui.db");
54307
54988
  try {
54308
54989
  const rows = importLegacyProject(db, projectId, legacyPath);
54309
54990
  imported.push({ projectId, rows });
@@ -54333,10 +55014,10 @@ function openFrizzDatabase(options = {}) {
54333
55014
  };
54334
55015
  }
54335
55016
  function projectIdsWithFile(projectsDir, file) {
54336
- if (!existsSync15(projectsDir)) return [];
54337
- return readdirSync13(projectsDir).filter((name) => {
55017
+ if (!existsSync17(projectsDir)) return [];
55018
+ return readdirSync14(projectsDir).filter((name) => {
54338
55019
  try {
54339
- return statSync23(join42(projectsDir, name, file)).isFile();
55020
+ return statSync23(join43(projectsDir, name, file)).isFile();
54340
55021
  } catch {
54341
55022
  return false;
54342
55023
  }
@@ -54346,7 +55027,7 @@ function legacyProjectIds(projectsDir) {
54346
55027
  return projectIdsWithFile(projectsDir, "ui.db");
54347
55028
  }
54348
55029
  function retiredLegacyProjectIds(projectsDir) {
54349
- return projectIdsWithFile(projectsDir, "ui.db.imported").filter((id) => !existsSync15(join42(projectsDir, id, "ui.db")));
55030
+ return projectIdsWithFile(projectsDir, "ui.db.imported").filter((id) => !existsSync17(join43(projectsDir, id, "ui.db")));
54350
55031
  }
54351
55032
  function tableColumns(db, schema, table) {
54352
55033
  return db.prepare(`PRAGMA ${schema}.table_info(${table})`).all().map((c) => c.name);
@@ -54502,7 +55183,7 @@ var init_tenant_prime = __esm({
54502
55183
  // packages/server/src/project-teardown.ts
54503
55184
  import { rmSync as rmSync13 } from "node:fs";
54504
55185
  import { homedir as homedir19 } from "node:os";
54505
- import { dirname as dirname21, resolve as resolve10 } from "node:path";
55186
+ import { dirname as dirname22, resolve as resolve10 } from "node:path";
54506
55187
  async function stopProjectWorkers(ctx) {
54507
55188
  let stopped = 0;
54508
55189
  for (const row of ctx.storage.allSessions()) {
@@ -54522,7 +55203,7 @@ function deleteProjectState(projectId, home = homedir19()) {
54522
55203
  if (!/^[\w.-]+$/u.test(projectId) || projectId === "." || projectId === "..") {
54523
55204
  throw new Error(`Refusing to delete state for a malformed project id: ${projectId}`);
54524
55205
  }
54525
- if (dirname21(resolve10(target)) !== resolve10(dirname21(projectStateDir("x", home)))) {
55206
+ if (dirname22(resolve10(target)) !== resolve10(dirname22(projectStateDir("x", home)))) {
54526
55207
  throw new Error(`Refusing to delete ${target} \u2014 it is not a project state directory`);
54527
55208
  }
54528
55209
  rmSync13(target, { recursive: true, force: true });
@@ -54551,11 +55232,11 @@ __export(index_exports, {
54551
55232
  unknownProjectPage: () => unknownProjectPage
54552
55233
  });
54553
55234
  import { createServer } from "node:http";
54554
- import { readFileSync as readFileSync30, existsSync as existsSync16, statSync as statSync24 } from "node:fs";
54555
- import { dirname as dirname22, join as join43, resolve as resolve11, extname as extname4, normalize as normalize2, sep as sep6 } from "node:path";
55235
+ import { readFileSync as readFileSync32, existsSync as existsSync18, statSync as statSync24 } from "node:fs";
55236
+ import { dirname as dirname23, join as join44, resolve as resolve11, extname as extname4, posix as posix2, sep as sep6 } from "node:path";
54556
55237
  function readServerAddressHolder(path3) {
54557
55238
  try {
54558
- const value = JSON.parse(readFileSync30(path3, "utf8"));
55239
+ const value = JSON.parse(readFileSync32(path3, "utf8"));
54559
55240
  if (typeof value?.pid !== "number") return void 0;
54560
55241
  return { pid: value.pid, port: typeof value.port === "number" ? value.port : -1 };
54561
55242
  } catch {
@@ -54679,13 +55360,13 @@ function staticIsFresh(req, etag, mtimeMs) {
54679
55360
  return Number.isFinite(since) && Math.floor(mtimeMs / 1e3) <= Math.floor(since / 1e3);
54680
55361
  }
54681
55362
  function serveStatic(distDir, req, res) {
54682
- const rel = normalize2((req.url ?? "/").split("?")[0]).replace(/^(\.\.[/\\])+/, "");
54683
- let file = join43(distDir, rel === "/" ? "index.html" : rel);
54684
- if (!file.startsWith(distDir)) file = join43(distDir, "index.html");
54685
- if (!existsSync16(file)) file = join43(distDir, "index.html");
55363
+ const rel = posix2.normalize((req.url ?? "/").split("?")[0].replace(/\\/g, "/")).replace(/^(\.\.[/\\])+/, "");
55364
+ let file = join44(distDir, rel === "/" ? "index.html" : rel);
55365
+ if (!file.startsWith(distDir + sep6)) file = join44(distDir, "index.html");
55366
+ if (!existsSync18(file)) file = join44(distDir, "index.html");
54686
55367
  try {
54687
55368
  const stats = statSync24(file);
54688
- const cacheControl = file.startsWith(join43(distDir, "assets") + sep6) ? STATIC_CACHE_IMMUTABLE : STATIC_CACHE_REVALIDATE;
55369
+ const cacheControl = file.startsWith(join44(distDir, "assets") + sep6) ? STATIC_CACHE_IMMUTABLE : STATIC_CACHE_REVALIDATE;
54689
55370
  const etag = `"${stats.size.toString(16)}-${Math.trunc(stats.mtimeMs).toString(16)}"`;
54690
55371
  const headers = {
54691
55372
  "content-type": MIME[extname4(file)] ?? "application/octet-stream",
@@ -54701,7 +55382,7 @@ function serveStatic(distDir, req, res) {
54701
55382
  res.end();
54702
55383
  return;
54703
55384
  }
54704
- let body = readFileSync30(file);
55385
+ let body = readFileSync32(file);
54705
55386
  const accept = req.headers["accept-encoding"];
54706
55387
  const encoding = negotiateEncoding(Array.isArray(accept) ? accept.join(",") : accept);
54707
55388
  if (encoding && shouldCompress(new Headers({ "content-type": headers["content-type"] }), body.byteLength)) {
@@ -54963,7 +55644,7 @@ async function startServer(opts = {}) {
54963
55644
  if (statusPath && statusIdentity) runtime.removeStatus(statusPath, statusIdentity);
54964
55645
  if (statusPath && statusIdentity) {
54965
55646
  try {
54966
- runtime.removeStatus(serverAddressPathForStateDir(dirname22(statusPath)), statusIdentity);
55647
+ runtime.removeStatus(serverAddressPathForStateDir(dirname23(statusPath)), statusIdentity);
54967
55648
  } catch {
54968
55649
  }
54969
55650
  }
@@ -55101,7 +55782,7 @@ async function startServer(opts = {}) {
55101
55782
  }
55102
55783
  statusPath = serverLockPathFor(ctx.project);
55103
55784
  const webRoot = resolve11(import.meta.dirname, "..", "..", "web");
55104
- const distDir = opts.webDistDir ? resolve11(opts.webDistDir) : join43(webRoot, "dist");
55785
+ const distDir = opts.webDistDir ? resolve11(opts.webDistDir) : join44(webRoot, "dist");
55105
55786
  startupPhase = "Vite";
55106
55787
  if (opts.dev) {
55107
55788
  try {
@@ -55177,7 +55858,7 @@ async function startServer(opts = {}) {
55177
55858
  if (vite) {
55178
55859
  vite.middlewares(req, res, () => {
55179
55860
  try {
55180
- const html = readFileSync30(join43(webRoot, "index.html"), "utf8");
55861
+ const html = readFileSync32(join44(webRoot, "index.html"), "utf8");
55181
55862
  void vite.transformIndexHtml(url, html).then((out) => {
55182
55863
  res.writeHead(200, { "content-type": "text/html" });
55183
55864
  res.end(out);
@@ -55189,7 +55870,7 @@ async function startServer(opts = {}) {
55189
55870
  });
55190
55871
  return;
55191
55872
  }
55192
- if (existsSync16(distDir)) {
55873
+ if (existsSync18(distDir)) {
55193
55874
  serveStatic(distDir, req, res);
55194
55875
  return;
55195
55876
  }
@@ -55387,7 +56068,7 @@ var init_index = __esm({
55387
56068
  }
55388
56069
  };
55389
56070
  isApiUrl = (url) => url === FRIZZ_ROUTE_PREFIX || url.startsWith(`${FRIZZ_ROUTE_PREFIX}/`);
55390
- serverLockPathFor = (project) => join43(project.stateDir, "server.lock");
56071
+ serverLockPathFor = (project) => join44(project.stateDir, "server.lock");
55391
56072
  MIME = {
55392
56073
  ".html": "text/html",
55393
56074
  ".js": "text/javascript",
@@ -55410,8 +56091,8 @@ init_project();
55410
56091
  init_project_launch();
55411
56092
  init_shutdown();
55412
56093
  init_logging();
55413
- import { existsSync as existsSync17 } from "node:fs";
55414
- import { join as join44 } from "node:path";
56094
+ import { existsSync as existsSync19 } from "node:fs";
56095
+ import { join as join45 } from "node:path";
55415
56096
  process.on("uncaughtException", (error) => {
55416
56097
  log.error("dev-child", `uncaught exception: ${error instanceof Error ? error.stack ?? error.message : error}`);
55417
56098
  process.exit(1);
@@ -55433,6 +56114,15 @@ function notifySupervisor(message) {
55433
56114
  } catch {
55434
56115
  }
55435
56116
  }
56117
+ var shutdown;
56118
+ process.once("disconnect", () => {
56119
+ if (shutdown) {
56120
+ void shutdown();
56121
+ return;
56122
+ }
56123
+ log.info("dev-child", "supervisor disconnected before the server was up; exiting");
56124
+ process.exit(0);
56125
+ });
55436
56126
  try {
55437
56127
  const target = projectLaunchTargetFromEnvironment(process.env);
55438
56128
  const launchOwnerToken = projectLaunchOwnerTokenFromEnvironment(process.env);
@@ -55449,9 +56139,9 @@ try {
55449
56139
  ["FRIZZ_SCRIPTS_DIR", process.env.FRIZZ_SCRIPTS_DIR, "index.mjs"],
55450
56140
  ["FRIZZ_WORKER_PLUGIN_DIR", process.env.FRIZZ_WORKER_PLUGIN_DIR, ".claude-plugin/plugin.json"]
55451
56141
  ];
55452
- if (!existsSync17(stableWebDist)) throw new Error("stable artifact launch is missing its verified web directory");
56142
+ if (!existsSync19(stableWebDist)) throw new Error("stable artifact launch is missing its verified web directory");
55453
56143
  for (const [name, directory, requiredFile] of required) {
55454
- if (!directory || !existsSync17(join44(directory, requiredFile)))
56144
+ if (!directory || !existsSync19(join45(directory, requiredFile)))
55455
56145
  throw new Error(`stable artifact launch is missing verified ${name}`);
55456
56146
  }
55457
56147
  }
@@ -55472,7 +56162,7 @@ try {
55472
56162
  bootId: server.ctx.bootId
55473
56163
  });
55474
56164
  let shuttingDown = false;
55475
- const shutdown = async () => {
56165
+ const stop = async () => {
55476
56166
  if (shuttingDown) return;
55477
56167
  shuttingDown = true;
55478
56168
  const force = setTimeout(() => process.exit(1), 15e3);
@@ -55495,9 +56185,9 @@ try {
55495
56185
  process.exit(1);
55496
56186
  }
55497
56187
  };
55498
- process.on("SIGINT", () => void shutdown());
55499
- process.on("SIGTERM", () => void shutdown());
55500
- process.once("disconnect", () => void shutdown());
56188
+ shutdown = stop;
56189
+ process.on("SIGINT", () => void stop());
56190
+ process.on("SIGTERM", () => void stop());
55501
56191
  } catch (err) {
55502
56192
  log.error("dev-child", `failed to start: ${err instanceof Error ? err.stack ?? err.message : err}`);
55503
56193
  process.exit(1);