frizz 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/claude-agent-broker.js +141 -12
  2. package/dist/dev-child.js +876 -337
  3. package/dist/frizz.js +81 -19
  4. package/package.json +1 -1
  5. package/runtime/cc-worker/DECISIONS.md +11 -0
  6. package/runtime/cc-worker/bin/frizz-mcp.mjs +58 -24
  7. package/web-dist/assets/{TerminalPane-CNFymHXK.js → TerminalPane-VbmxsjxK.js} +1 -1
  8. package/web-dist/assets/{abnfDiagram-VRR7QNED-DNBrcJfV.js → abnfDiagram-VRR7QNED-CBCkSzJg.js} +1 -1
  9. package/web-dist/assets/architecture-TIHT7OUA-DGkzyd6b.js +1 -0
  10. package/web-dist/assets/{architectureDiagram-ZJ3FMSHR-SXVydqly.js → architectureDiagram-ZJ3FMSHR-Rd44A8-r.js} +1 -1
  11. package/web-dist/assets/{blockDiagram-677ZJIJ3-CgI1GSdx.js → blockDiagram-677ZJIJ3-C8vX06UX.js} +1 -1
  12. package/web-dist/assets/{c4Diagram-LMCZKHZV-KDmJm2tG.js → c4Diagram-LMCZKHZV-CMMjtui-.js} +1 -1
  13. package/web-dist/assets/channel-BvYE_fQ0.js +1 -0
  14. package/web-dist/assets/{chunk-32BRIVSS-j3eOjq4Z.js → chunk-32BRIVSS-DtsFPb8y.js} +1 -1
  15. package/web-dist/assets/{chunk-52WLFC77-DZHfa78H.js → chunk-52WLFC77-35RvKhG9.js} +1 -1
  16. package/web-dist/assets/{chunk-C7G6YPKG-CiB5uo24.js → chunk-C7G6YPKG-Dusr0gOw.js} +1 -1
  17. package/web-dist/assets/{chunk-EX3LRPZG-B35Inyjz.js → chunk-EX3LRPZG-BadaF-x2.js} +1 -1
  18. package/web-dist/assets/{chunk-FWX5IMBZ-D2sRvM5U.js → chunk-FWX5IMBZ-CpQ7yEDx.js} +2 -2
  19. package/web-dist/assets/{chunk-HOUHSVGY-D247PDv6.js → chunk-HOUHSVGY-CxygFxWC.js} +1 -1
  20. package/web-dist/assets/{chunk-ICXQ74PX-Bu3Tb6Ec.js → chunk-ICXQ74PX-YC4-v0mV.js} +1 -1
  21. package/web-dist/assets/{chunk-MOJQB5TN-C_lQy4on.js → chunk-MOJQB5TN-CTgO9Z8o.js} +1 -1
  22. package/web-dist/assets/{chunk-OGEWGWER-DU5ZBedR.js → chunk-OGEWGWER-D87J1JIz.js} +1 -1
  23. package/web-dist/assets/{chunk-PUDLZKDR-DWRT7ztS.js → chunk-PUDLZKDR-CVB9kmxS.js} +1 -1
  24. package/web-dist/assets/{chunk-Q4XR5HBZ-CCX4AK_n.js → chunk-Q4XR5HBZ-B2a88YVr.js} +1 -1
  25. package/web-dist/assets/{chunk-V7JOEXUC-VbixbkDG.js → chunk-V7JOEXUC-DoFngcpl.js} +1 -1
  26. package/web-dist/assets/{chunk-VAUOI2AC-PJvChxMA.js → chunk-VAUOI2AC-X71d5AYZ.js} +1 -1
  27. package/web-dist/assets/{chunk-VR4S4FIN-BjsMfxj8.js → chunk-VR4S4FIN-39jPx_ES.js} +1 -1
  28. package/web-dist/assets/{chunk-WYO6CB5R-Bcb97r3w.js → chunk-WYO6CB5R-CnCFbrQ7.js} +1 -1
  29. package/web-dist/assets/{chunk-ZGVPDNZ5-CT2sZlhT.js → chunk-ZGVPDNZ5-B7wPa-pc.js} +1 -1
  30. package/web-dist/assets/classDiagram-OUVF2IWQ-BmoT0Xoa.js +1 -0
  31. package/web-dist/assets/classDiagram-v2-EOCWNBFH-BmoT0Xoa.js +1 -0
  32. package/web-dist/assets/{cynefin-VYW2F7L2-dUpO4u0w.js → cynefin-VYW2F7L2-C4GE-6ZJ.js} +1 -1
  33. package/web-dist/assets/{cynefinDiagram-TSTJHNR4-BQZ3UIsx.js → cynefinDiagram-TSTJHNR4-DM7vNSjQ.js} +1 -1
  34. package/web-dist/assets/{dagre-VKFMJZFB-K-oO8w51.js → dagre-VKFMJZFB-CBRFVbWb.js} +1 -1
  35. package/web-dist/assets/{diagram-FQU43EPY-Cr47GJ_S.js → diagram-FQU43EPY-D3t9pHH6.js} +1 -1
  36. package/web-dist/assets/{diagram-G47NLZAW-BbWzSkoZ.js → diagram-G47NLZAW-CBjFvnAL.js} +1 -1
  37. package/web-dist/assets/{diagram-NH7WQ7WH-BGJJBgGM.js → diagram-NH7WQ7WH-DAI3KCBU.js} +1 -1
  38. package/web-dist/assets/{diagram-OA4YK3LP-2W7aunEN.js → diagram-OA4YK3LP-C82PfYbV.js} +1 -1
  39. package/web-dist/assets/{diagram-WEI45ONY-D-4ITFI6.js → diagram-WEI45ONY-fOJDDfoI.js} +1 -1
  40. package/web-dist/assets/{ebnfDiagram-CCIWWBDH-CxUcx103.js → ebnfDiagram-CCIWWBDH-B5h_VbLA.js} +1 -1
  41. package/web-dist/assets/{erDiagram-Q63AITRT-Cf_5SWAE.js → erDiagram-Q63AITRT-BPje_yvH.js} +1 -1
  42. package/web-dist/assets/eventmodeling-45OFAUF4-Bqo9jfNt.js +1 -0
  43. package/web-dist/assets/flowDiagram-23GEKE2U-BwssDy7H.js +1 -0
  44. package/web-dist/assets/{ganttDiagram-NO4QXBWP-CVZI8Ywa.js → ganttDiagram-NO4QXBWP-sBrAv0Zq.js} +1 -1
  45. package/web-dist/assets/{gitGraph-TEB2WS4Q-BsiGpR9l.js → gitGraph-TEB2WS4Q-DRA1mvOm.js} +1 -1
  46. package/web-dist/assets/{gitGraphDiagram-IHSO6WYX-DVym_4l1.js → gitGraphDiagram-IHSO6WYX-DBMOi0IS.js} +1 -1
  47. package/web-dist/assets/index-C_iC5T6e.css +1 -0
  48. package/web-dist/assets/index-YBqvcp8M.js +506 -0
  49. package/web-dist/assets/{info-DKCQHKI2-DYcsKFv-.js → info-DKCQHKI2-faVLVx1A.js} +1 -1
  50. package/web-dist/assets/{infoDiagram-FWYZ7A6U-hjVxlrTT.js → infoDiagram-FWYZ7A6U-BST26f54.js} +1 -1
  51. package/web-dist/assets/{ishikawaDiagram-FXEZZL3T-B0DKOPSS.js → ishikawaDiagram-FXEZZL3T-AQe4w48R.js} +1 -1
  52. package/web-dist/assets/{journeyDiagram-5HDEW3XC-Cohibc5O.js → journeyDiagram-5HDEW3XC-DHYMyWnH.js} +1 -1
  53. package/web-dist/assets/{kanban-definition-HUTT4EX6-_6Zr31vM.js → kanban-definition-HUTT4EX6-DRjdUC1w.js} +1 -1
  54. package/web-dist/assets/{line-B14EZI6M.js → line-DsDFTOBE.js} +1 -1
  55. package/web-dist/assets/{mermaid-parser.core-DHkc6cqw.js → mermaid-parser.core-BQmWWHcM.js} +3 -3
  56. package/web-dist/assets/{mermaid.core-DuTvdsKf.js → mermaid.core-DyyZgnTM.js} +3 -3
  57. package/web-dist/assets/{mindmap-definition-LN4V7U3C-DPRF9pW2.js → mindmap-definition-LN4V7U3C-DKFwErMl.js} +1 -1
  58. package/web-dist/assets/{packet-7NZHBO7P-Ci8oCcGM.js → packet-7NZHBO7P-Dc2mNrwB.js} +1 -1
  59. package/web-dist/assets/{pegDiagram-2B236MQR-Cr3ctC6e.js → pegDiagram-2B236MQR-CApGxts7.js} +1 -1
  60. package/web-dist/assets/{pie-RZYD4A2V-EHgA307y.js → pie-RZYD4A2V-420IM2Cy.js} +1 -1
  61. package/web-dist/assets/{pieDiagram-ENE6RG2P-UW6Bmu6F.js → pieDiagram-ENE6RG2P-B9AabvqO.js} +1 -1
  62. package/web-dist/assets/{quadrantDiagram-ABIIQ3AL-D6tAsRj9.js → quadrantDiagram-ABIIQ3AL-KDtGo5xp.js} +1 -1
  63. package/web-dist/assets/{radar-I7S5WNFK-CAQqojcd.js → radar-I7S5WNFK-r00cL8de.js} +1 -1
  64. package/web-dist/assets/{railroad-3IZDKUUU-BZsAOTrm.js → railroad-3IZDKUUU-u3-ja2m8.js} +1 -1
  65. package/web-dist/assets/railroad-abnf-AHOZXSZD-BIa3zJb3.js +1 -0
  66. package/web-dist/assets/railroad-ebnf-EBAXGLYW-Dhv_ZRtc.js +1 -0
  67. package/web-dist/assets/railroad-peg-LSFZ7HO6-BSnPbaOy.js +1 -0
  68. package/web-dist/assets/{railroadDiagram-RFXS5EU6-bSpff55H.js → railroadDiagram-RFXS5EU6-PXQXCI8m.js} +1 -1
  69. package/web-dist/assets/{requirementDiagram-TGXJPOKE-DIAYvXoB.js → requirementDiagram-TGXJPOKE-CsigfYcM.js} +1 -1
  70. package/web-dist/assets/{sankeyDiagram-HTMAVEWB-DEnsSz3F.js → sankeyDiagram-HTMAVEWB-CZTeOkfq.js} +1 -1
  71. package/web-dist/assets/{sequenceDiagram-DBY2YBRQ-DsG7eb9R.js → sequenceDiagram-DBY2YBRQ-BVnCAk_N.js} +1 -1
  72. package/web-dist/assets/{stateDiagram-2N3HPSRC-DT3XNwjo.js → stateDiagram-2N3HPSRC-ChCJipZW.js} +1 -1
  73. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-BopBzYGR.js +1 -0
  74. package/web-dist/assets/{swimlanes-5IMT3BWC-B5hZwgRo.js → swimlanes-5IMT3BWC-DHR9HD5O.js} +1 -1
  75. package/web-dist/assets/swimlanesDiagram-G3AALYLV-B3z_1cGb.js +8 -0
  76. package/web-dist/assets/{timeline-definition-FHXFAJF6-BVTsjkfu.js → timeline-definition-FHXFAJF6-BZl68jCj.js} +1 -1
  77. package/web-dist/assets/{treeView-QDETBFTQ-M6HmP4bB.js → treeView-QDETBFTQ-CcWDnN_I.js} +1 -1
  78. package/web-dist/assets/{treemap-6X3UGDF4-C60TppfC.js → treemap-6X3UGDF4-CoPTx3oa.js} +1 -1
  79. package/web-dist/assets/{vennDiagram-L72KCM5P-Dv1y3g4S.js → vennDiagram-L72KCM5P-lrsO-4o9.js} +1 -1
  80. package/web-dist/assets/{wardley-OPB4EBWU-D0lMJMWt.js → wardley-OPB4EBWU-hA_IMYdR.js} +1 -1
  81. package/web-dist/assets/{wardleyDiagram-EHGQE667-B1E5NtZY.js → wardleyDiagram-EHGQE667-6T7WmjKn.js} +1 -1
  82. package/web-dist/assets/{xychartDiagram-FW5EYKEG-oA91YOvh.js → xychartDiagram-FW5EYKEG-BPYtf6cM.js} +1 -1
  83. package/web-dist/index.html +2 -2
  84. package/web-dist/assets/architecture-TIHT7OUA-BSUDKbro.js +0 -1
  85. package/web-dist/assets/channel-DaCOnTxr.js +0 -1
  86. package/web-dist/assets/classDiagram-OUVF2IWQ-BlylbZXV.js +0 -1
  87. package/web-dist/assets/classDiagram-v2-EOCWNBFH-BlylbZXV.js +0 -1
  88. package/web-dist/assets/eventmodeling-45OFAUF4-DxKQnZNV.js +0 -1
  89. package/web-dist/assets/flowDiagram-23GEKE2U-Cdz9YM2Z.js +0 -1
  90. package/web-dist/assets/index-CYzJK7u0.js +0 -497
  91. package/web-dist/assets/index-CovJDCWc.css +0 -1
  92. package/web-dist/assets/railroad-abnf-AHOZXSZD-_2rSHfPz.js +0 -1
  93. package/web-dist/assets/railroad-ebnf-EBAXGLYW-xM3eAMFz.js +0 -1
  94. package/web-dist/assets/railroad-peg-LSFZ7HO6-cZlBUsED.js +0 -1
  95. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-C06v0Ltu.js +0 -1
  96. package/web-dist/assets/swimlanesDiagram-G3AALYLV-CXyN7ZyO.js +0 -8
package/dist/dev-child.js CHANGED
@@ -727,6 +727,20 @@ function ensureProjectIdFile(root, home = homedir3(), seed) {
727
727
  function hasAny(dir, names) {
728
728
  return names.some((name) => existsSync2(join4(dir, name)));
729
729
  }
730
+ function isHomeDirectory(dir, home = homedir3()) {
731
+ const canonical = (value) => {
732
+ try {
733
+ return realpathSync2(resolve3(value));
734
+ } catch {
735
+ return resolve3(value);
736
+ }
737
+ };
738
+ try {
739
+ return canonical(dir) === canonical(home);
740
+ } catch {
741
+ return false;
742
+ }
743
+ }
730
744
  function existingProjectId(dir) {
731
745
  const fromFile = readProjectIdFile(dir);
732
746
  if (fromFile) return fromFile;
@@ -736,6 +750,9 @@ function existingProjectId(dir) {
736
750
  return void 0;
737
751
  }
738
752
  }
753
+ function hasProjectMarker(dir) {
754
+ return hasAny(dir, REPO_MARKERS) || hasAny(dir, PROJECT_MARKERS);
755
+ }
739
756
  function discoverProjectRoot(cwd = process.cwd(), home = homedir3()) {
740
757
  let dir;
741
758
  try {
@@ -754,6 +771,11 @@ function discoverProjectRoot(cwd = process.cwd(), home = homedir3()) {
754
771
  }
755
772
  return dir;
756
773
  }
774
+ function chosenProjectRoot(chosen, home = homedir3()) {
775
+ const dir = resolve3(chosen);
776
+ const discovered = discoverProjectRoot(dir, home);
777
+ return discovered === dir || hasProjectMarker(discovered) ? discovered : dir;
778
+ }
757
779
  var FRIZZ_DIR, ID_FILE, SELF_IGNORE, REPO_MARKERS, PROJECT_MARKERS;
758
780
  var init_project_root = __esm({
759
781
  "packages/server/src/project-root.ts"() {
@@ -13053,12 +13075,12 @@ function parseAwaitingYaml(text2) {
13053
13075
  function isAwaitingItemKind(kind) {
13054
13076
  return AWAITING_ITEM_KINDS.includes(kind);
13055
13077
  }
13056
- function parseAwaitingDuration(value) {
13078
+ function parseAwaitingDurationRaw(value) {
13057
13079
  const m = AWAITING_DURATION_RE.exec(value.trim());
13058
13080
  if (!m) return null;
13059
13081
  const ms = Number(m[1]) * DURATION_UNIT_MS[m[2]];
13060
13082
  if (!Number.isFinite(ms) || ms <= 0) return null;
13061
- return Math.min(ms, AWAITING_FOR_MAX_MS);
13083
+ return ms;
13062
13084
  }
13063
13085
  function isInterruptMarker(text2) {
13064
13086
  return INTERRUPT_MARKERS.includes(text2.trim());
@@ -13383,7 +13405,7 @@ function parseGithubWakeSteer(text2) {
13383
13405
  function frizzRoute(path) {
13384
13406
  return `${FRIZZ_ROUTE_PREFIX}${path.startsWith("/") ? path : `/${path}`}`;
13385
13407
  }
13386
- 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, 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_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, ASK_MAX_PER_CALL, ASK_MAX_OPEN, 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, 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, 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, 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_FAILED, GithubStatus, GithubRefCard, GithubRefPreviewInput, GithubRefPreviewResult, GithubItem, GithubListInput, GithubListResult, GithubBatchInput, GithubBatchResult, BoardMeta, ServerEvent, TranscriptEdit, TranscriptTodo, TranscriptToolCall, TranscriptPart, TranscriptMessage, TranscriptPageCursor, EditedFile, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketClientMsg, ProjectCard, PROJECT_ICON_EXTENSIONS, PROJECT_ICON_MAX_BASE64_CHARS, DirectoryPickResult, ThreadLocation, FRIZZ_ROUTE_PREFIX, DEFAULT_PORT, threadIdentityName;
13408
+ 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, 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, 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, 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_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;
13387
13409
  var init_src = __esm({
13388
13410
  "packages/shared/src/index.ts"() {
13389
13411
  "use strict";
@@ -13663,8 +13685,13 @@ var init_src = __esm({
13663
13685
  subAgentCount: external_exports.number().default(0),
13664
13686
  // total live sub-agents (≥ subAgents.length)
13665
13687
  bgShells: external_exports.array(CompletionHoldOp).default([]),
13666
- bgShellCount: external_exports.number().default(0)
13688
+ bgShellCount: external_exports.number().default(0),
13667
13689
  // total live background shells (≥ bgShells.length)
13690
+ // The worker is DEAD and its recorded turn never ended — cut off by a reboot, a signal or a crash
13691
+ // mid-tool-call (router.cutOffHold). Nothing is running, so nothing will be killed; the hold exists
13692
+ // because the thread is not finished and Done would say it was. Optional rather than defaulted so it
13693
+ // is absent (⇒ false) on every hold that is not this one, and a pre-change client reads them unchanged.
13694
+ cutOff: external_exports.boolean().optional()
13668
13695
  });
13669
13696
  AskOption = external_exports.object({
13670
13697
  label: external_exports.string(),
@@ -13867,7 +13894,8 @@ var init_src = __esm({
13867
13894
  * known, so a timer row always carries this. */
13868
13895
  timer: external_exports.object({ fireAt: external_exports.string(), prompt: external_exports.string() }).strict().optional()
13869
13896
  }).strict();
13870
- PR_WATCH_DEFAULT_FOR_MS = 6 * 60 * 60 * 1e3;
13897
+ PR_WATCH_FOR_MAX_MS = 365 * 24 * 60 * 60 * 1e3;
13898
+ PR_WATCH_DEFAULT_FOR_MS = 30 * 24 * 60 * 60 * 1e3;
13871
13899
  PR_WATCH_MAX_ARMED = 32;
13872
13900
  PrWatchView = external_exports.object({
13873
13901
  id: external_exports.string(),
@@ -13883,13 +13911,18 @@ var init_src = __esm({
13883
13911
  /** `owner/repo#123` or a PR URL. Parsed server-side; an unparseable ref is refused rather than stored,
13884
13912
  * because a watcher that can never fire is worse than none — the worker rests believing it is covered. */
13885
13913
  target: external_exports.string().trim().min(1).max(200),
13886
- /** How long to watch, as a DURATION (`30m`, `2h`, `3d` — parseAwaitingDuration).
13914
+ /** How long to watch, as a DURATION (`2h`, `3d`, `180d` — parseAwaitingDurationRaw, capped at
13915
+ * PR_WATCH_FOR_MAX_MS).
13887
13916
  *
13888
13917
  * A PR nobody ever reviews would otherwise be polled forever, and a thread parked on it would wait
13889
13918
  * forever with it — the same unbounded wait the awaiting fence's `for:` closes, one level down. A
13890
13919
  * duration rather than an instant for the same reason it is one there: it cannot be written in the
13891
13920
  * past (maintainer 2026-08-15, asking for it explicitly).
13892
13921
  *
13922
+ * BOUNDED IS NOT THE SAME AS SHORT, and conflating them is what made this field a noise source. The
13923
+ * bound exists so a forgotten watcher stops polling eventually, which a year satisfies exactly as
13924
+ * well as a day — and a day spent it against every external PR, which does not move on that clock.
13925
+ *
13893
13926
  * REQUIRED BY THE TOOL, OPTIONAL ON THE WIRE, and the asymmetry is deliberate. A worker's MCP server
13894
13927
  * outlives every frizz restart, so a session dispatched before this existed still holds a binary that
13895
13928
  * cannot send it — and making the RPC reject that would break `watch_pr` outright for every thread
@@ -13904,6 +13937,12 @@ var init_src = __esm({
13904
13937
  /** True when this exact PR was ALREADY watched by this thread, so the call registered nothing new.
13905
13938
  * Re-registering after a compaction is the common case, and a duplicate would double every wake. */
13906
13939
  alreadyArmed: external_exports.boolean(),
13940
+ /** When this watcher runs out. Read back so the worker sees the duration it ACTUALLY got rather than
13941
+ * the one it asked for — on a re-registration that is the original expiry, which the call left alone. */
13942
+ expiresAt: external_exports.string(),
13943
+ /** The `for:` the worker wrote, present ONLY when it exceeded the ceiling and was capped. A clamp
13944
+ * nobody is told about is a worker resting on a year of coverage it does not have. */
13945
+ clampedFrom: external_exports.string().optional(),
13907
13946
  watches: external_exports.array(PrWatchView)
13908
13947
  }).strict();
13909
13948
  DropOwnPrWatchInput = external_exports.object({
@@ -13930,7 +13969,11 @@ var init_src = __esm({
13930
13969
  * that is both the stronger check and the one that catches the real mistake: naming a sub-agent under
13931
13970
  * `kind: "shell"`, which is what put two sub-agents under a "Background shells" heading on 2026-08-26. */
13932
13971
  target: external_exports.string().trim().min(1).max(200),
13933
- /** REQUIRED, and a DURATION (`30m`, `2h`, `3d` — parseAwaitingDuration, capped at 24h).
13972
+ /** REQUIRED, and a DURATION (`30m`, `2h`, `3d` — parseAwaitingDurationRaw, capped at 24h).
13973
+ *
13974
+ * STILL A DAY, where a PR watcher now gets a year: this names a shell or a sub-agent, which lives
13975
+ * inside the session that launched it, so a wait standing longer than a day is one whose target is
13976
+ * almost certainly already gone.
13934
13977
  *
13935
13978
  * No default and no wire-level optionality, unlike `AddOwnPrWatchInput.for`: that field is optional
13936
13979
  * only to keep working for sessions dispatched before it existed, and this RPC has no such sessions.
@@ -13955,6 +13998,10 @@ var init_src = __esm({
13955
13998
  /** True when this exact (kind, target) was already watched, so the call registered nothing new and the
13956
13999
  * existing expiry stands. Re-registering after a wake or a compaction is the common, correct case. */
13957
14000
  alreadyArmed: external_exports.boolean(),
14001
+ /** The `for:` the worker wrote, present ONLY when it exceeded the ceiling and was capped — the twin of
14002
+ * `AddOwnPrWatchResult.clampedFrom`, for the same reason: a silent clamp is a worker resting on
14003
+ * coverage it does not have. */
14004
+ clampedFrom: external_exports.string().optional(),
13958
14005
  watches: external_exports.array(OwnWatchView)
13959
14006
  }).strict();
13960
14007
  DropOwnWatchInput = external_exports.object({
@@ -13968,25 +14015,38 @@ var init_src = __esm({
13968
14015
  OWN_WATCH_MAX_ARMED = 24;
13969
14016
  AskQuestionKind = external_exports.enum(["question", "multi"]);
13970
14017
  ASK_MAX_DEPTH = 3;
13971
- ASK_MAX_PER_CALL = 4;
13972
- ASK_MAX_OPEN = 12;
13973
14018
  AskedOptionSchema = external_exports.lazy(() => external_exports.object({
13974
- label: external_exports.string().trim().min(1).max(120),
13975
- description: external_exports.string().trim().max(400).optional(),
14019
+ // 400, not the 120 it launched with (2026-09-03): a label is a chip line, but "Post the drafted
14020
+ // comment as written" plus its qualifier runs long, and the fence has no cap at all. The answer's
14021
+ // `chosen` carries labels, so its per-item cap matches this one.
14022
+ label: external_exports.string().trim().min(1).max(400),
14023
+ // 20000, not the 4000 the retired `preview` had (2026-09-03): a description is allowed to BE the rich
14024
+ // body — a diff, a drafted comment, a table — and a real diff exceeds 4000.
14025
+ description: external_exports.string().trim().max(2e4).optional(),
13976
14026
  recommended: external_exports.boolean().optional(),
13977
14027
  preview: external_exports.string().max(4e3).optional(),
13978
- followUps: external_exports.array(AskedQuestionSchema).max(ASK_MAX_PER_CALL).optional()
14028
+ // No count cap, like `options` (2026-09-03); the tree is bounded by ASK_MAX_DEPTH instead.
14029
+ followUps: external_exports.array(AskedQuestionSchema).optional()
13979
14030
  }).strict());
13980
14031
  AskedQuestionSchema = external_exports.lazy(() => external_exports.object({
13981
- question: external_exports.string().trim().min(1).max(600),
14032
+ // 4000, not the 600 it launched with (2026-09-03): this field is the whole card context — everything
14033
+ // the human needs to answer cold — and a question that carries its own evidence needs the room. The
14034
+ // answer restates it, so QuestionAnswerSchema's cap matches.
14035
+ question: external_exports.string().trim().min(1).max(4e3),
13982
14036
  header: external_exports.string().trim().max(24).optional(),
13983
14037
  kind: AskQuestionKind,
13984
14038
  danger: external_exports.boolean().optional(),
13985
- options: external_exports.array(AskedOptionSchema).max(8).optional()
14039
+ // UNBOUNDED, deliberately. This carried `.max(8)` from launch until 2026-09-03, when the maintainer
14040
+ // asked for the cap to go ("allow arbitrary numbers of options"): a `multi` over a long list — which
14041
+ // gates to run, which of twenty findings to act on — is a real shape, and the card letters past 26
14042
+ // (`AA.`) already. The count is the worker's to choose; the answer's `chosen` is unbounded to match.
14043
+ options: external_exports.array(AskedOptionSchema).optional()
13986
14044
  }).strict());
13987
14045
  AskInput = external_exports.object({
13988
14046
  slug: ThreadSlug,
13989
- questions: external_exports.array(AskedQuestionSchema).min(1).max(ASK_MAX_PER_CALL)
14047
+ // No cap on the count (four until 2026-09-03): the tool's own text says "several at once is one
14048
+ // call — register them together", and a cap here told a worker with six to batch and then refused it.
14049
+ questions: external_exports.array(AskedQuestionSchema).min(1)
13990
14050
  }).strict();
13991
14051
  RegisteredQuestionView = external_exports.object({
13992
14052
  /** Minted by frizz. The worker never chose it, which is why an answer RESTATES the question text —
@@ -14010,17 +14070,19 @@ var init_src = __esm({
14010
14070
  }).strict();
14011
14071
  QuestionAnswerSchema = external_exports.lazy(() => external_exports.object({
14012
14072
  questionId: external_exports.string().min(1).max(64),
14013
- question: external_exports.string().min(1).max(600),
14014
- chosen: external_exports.array(external_exports.string().max(200)).max(8),
14073
+ question: external_exports.string().min(1).max(4e3),
14074
+ // No count cap: a `multi` may carry any number of options (see AskedQuestionSchema), and the human
14075
+ // may pick every one of them. The per-item cap is the option label's.
14076
+ chosen: external_exports.array(external_exports.string().max(400)),
14015
14077
  text: external_exports.string().max(8e3).optional(),
14016
- followUps: external_exports.array(QuestionAnswerSchema).max(ASK_MAX_PER_CALL).optional()
14078
+ followUps: external_exports.array(QuestionAnswerSchema).optional()
14017
14079
  }).strict());
14018
14080
  AnswerQuestionsInput = external_exports.object({
14019
14081
  slug: ThreadSlug,
14020
14082
  /** SUBMITTED AS A UNIT. The card sends whatever was answered in one call, because a per-question send
14021
14083
  * would half-wake a turn: the worker would come back to a payload it cannot act on and would have to
14022
14084
  * ask again for the rest. */
14023
- answers: external_exports.array(QuestionAnswerSchema).min(1).max(ASK_MAX_OPEN)
14085
+ answers: external_exports.array(QuestionAnswerSchema).min(1)
14024
14086
  }).strict();
14025
14087
  AnswerQuestionsResult = external_exports.object({
14026
14088
  /** The ids that were open and are now answered. An id that was already settled is silently absent
@@ -14030,7 +14092,7 @@ var init_src = __esm({
14030
14092
  }).strict();
14031
14093
  DismissQuestionsInput = external_exports.object({
14032
14094
  slug: ThreadSlug,
14033
- ids: external_exports.array(external_exports.string().min(1).max(64)).min(1).max(ASK_MAX_OPEN)
14095
+ ids: external_exports.array(external_exports.string().min(1).max(64)).min(1)
14034
14096
  }).strict();
14035
14097
  DismissQuestionsResult = external_exports.object({
14036
14098
  dismissed: external_exports.array(external_exports.string()),
@@ -14213,6 +14275,11 @@ var init_src = __esm({
14213
14275
  // AUTO-bump (the scheduler resumes the agent with exactly this text) rather than a reminder, which is
14214
14276
  // the distinction the held row's tooltip renders. Absent ⇒ the card merely re-surfaces.
14215
14277
  snoozePrompt: external_exports.string().optional(),
14278
+ // The instant the human PINNED this thread (absent = not pinned). A pinned thread leaves the rail's
14279
+ // band system entirely — Rested/Active/Snoozed/Done — and holds the pinned band at the very top, in
14280
+ // this instant's order. Lifecycle metadata like the snooze: written only by the pin/unpin verb, and
14281
+ // it outranks every derived state (a pinned thread that finishes stays pinned).
14282
+ pinnedAt: external_exports.string().optional(),
14216
14283
  /** The EVENT snooze on the resting card is armed for this exact rest — the human has said "hide this
14217
14284
  * until something reports". Distinct from `snoozedUntil`, which is a wall-clock park on the whole
14218
14285
  * thread: this one has no deadline and clears itself when the thread comes to a NEW rest.
@@ -14458,6 +14525,12 @@ var init_src = __esm({
14458
14525
  // Code caps to the model's real window. Optional so an old blob parses; defaultSettings pins 500_000
14459
14526
  // (maintainer 2026-08-26: "a default compaction window of 500k by default").
14460
14527
  autoCompactWindow: external_exports.number().int().positive().optional(),
14528
+ // The prompt-cache tier a NEW Claude worker writes to. "auto" leaves the CLI's own choice (1h on a
14529
+ // subscription); "5m" and "1h" reach the worker as CLAUDE_CODE_PROMPT_CACHE_TTL (and the sub-agent
14530
+ // twin). The 1h tier bills a cache write at 2x input against 1.25x for 5m; on 2026-09-03 cache
14531
+ // writes were 51% of a day's spend while the entries were invalidated every 15–30 minutes anyway
14532
+ // (see claudePromptCacheEnv). Optional so an old blob parses; defaultSettings pins "auto".
14533
+ promptCacheTtl: external_exports.enum(["auto", "5m", "1h"]).optional(),
14461
14534
  // The GitHub batch-dispatch prompt template (the picker's per-item worker prompt). Optional: when
14462
14535
  // unset OR blank the server falls back to its exported DEFAULT_GITHUB_PROMPT. Substitution tokens
14463
14536
  // the server fills: {repo} {n} {title} {url} {labels} {body}. The leading `THREAD: <slug>` tag is
@@ -14596,6 +14669,11 @@ var init_src = __esm({
14596
14669
  // can never leave an armed prompt behind.
14597
14670
  prompt: SnoozePrompt.nullable().optional()
14598
14671
  }).strict();
14672
+ SetThreadPinnedInput = external_exports.object({
14673
+ slug: ThreadSlug,
14674
+ sessionId: external_exports.string().min(1),
14675
+ pinned: external_exports.boolean()
14676
+ }).strict();
14599
14677
  SetThreadRecurringPromptInput = external_exports.object({
14600
14678
  slug: ThreadSlug,
14601
14679
  sessionId: external_exports.string().min(1),
@@ -15287,10 +15365,16 @@ ${DISPATCH_TASK_BANNER}
15287
15365
  cursor: TranscriptPageCursor
15288
15366
  }).strict();
15289
15367
  SocketTranscriptSlug = ThreadSlug;
15290
- SocketClientMsg = external_exports.discriminatedUnion("t", [
15368
+ SocketFilePath = external_exports.string().min(1).max(4096);
15369
+ SocketTranscriptClientMsg = external_exports.discriminatedUnion("t", [
15291
15370
  external_exports.object({ t: external_exports.literal("sub"), topic: external_exports.literal("transcript"), slug: SocketTranscriptSlug }).strict(),
15292
15371
  external_exports.object({ t: external_exports.literal("unsub"), topic: external_exports.literal("transcript"), slug: SocketTranscriptSlug }).strict()
15293
15372
  ]);
15373
+ SocketFileClientMsg = external_exports.discriminatedUnion("t", [
15374
+ external_exports.object({ t: external_exports.literal("sub"), topic: external_exports.literal("file"), path: SocketFilePath }).strict(),
15375
+ external_exports.object({ t: external_exports.literal("unsub"), topic: external_exports.literal("file"), path: SocketFilePath }).strict()
15376
+ ]);
15377
+ SocketClientMsg = external_exports.union([SocketTranscriptClientMsg, SocketFileClientMsg]);
15294
15378
  ProjectCard = external_exports.object({
15295
15379
  id: external_exports.string(),
15296
15380
  slug: external_exports.string(),
@@ -16818,7 +16902,7 @@ function createShutdownBarrier(options) {
16818
16902
  void drainPromise.catch(() => void 0);
16819
16903
  return drainPromise;
16820
16904
  };
16821
- const close = () => {
16905
+ const close2 = () => {
16822
16906
  if (closePromise) return closePromise;
16823
16907
  closePromise = deadline(beginDrain(), options.timeoutMs).catch((error) => {
16824
16908
  if (error instanceof ShutdownTimeoutError) {
@@ -16832,7 +16916,7 @@ function createShutdownBarrier(options) {
16832
16916
  get closing() {
16833
16917
  return closing;
16834
16918
  },
16835
- close,
16919
+ close: close2,
16836
16920
  whenDrained: beginDrain
16837
16921
  };
16838
16922
  }
@@ -16912,13 +16996,13 @@ function linkLatest(dir, target) {
16912
16996
  }
16913
16997
  }
16914
16998
  function pruneRunLogs(dir, keep = RETAINED_RUNS, days = RETAINED_DAYS, now = Date.now()) {
16915
- let entries;
16999
+ let entries2;
16916
17000
  try {
16917
- entries = readdirSync2(dir).filter((name) => name.endsWith(".log") && name !== "latest.log");
17001
+ entries2 = readdirSync2(dir).filter((name) => name.endsWith(".log") && name !== "latest.log");
16918
17002
  } catch {
16919
17003
  return;
16920
17004
  }
16921
- const dated = entries.map((name) => {
17005
+ const dated = entries2.map((name) => {
16922
17006
  try {
16923
17007
  return { name, at: statSync3(join8(dir, name)).mtimeMs };
16924
17008
  } catch {
@@ -18549,6 +18633,12 @@ function slugMintedFromTitle(slug, title) {
18549
18633
  }
18550
18634
  function ensureStorageSchema(db) {
18551
18635
  db.exec(STORAGE_SCHEMA);
18636
+ for (const column of ["pinned_at TEXT"]) {
18637
+ try {
18638
+ db.exec(`ALTER TABLE session ADD COLUMN ${column}`);
18639
+ } catch {
18640
+ }
18641
+ }
18552
18642
  }
18553
18643
  function createStorage(source, projectId) {
18554
18644
  const owned = typeof source === "string";
@@ -18813,6 +18903,7 @@ function createStorage(source, projectId) {
18813
18903
  WHERE project_id = @project_id AND slug = ? AND session_id = ? AND runtime_generation = ?
18814
18904
  `);
18815
18905
  const snoozedUntilStmt = scope.prepare("UPDATE session SET snoozed_until = ?, snooze_prompt = ? WHERE project_id = @project_id AND slug = ?");
18906
+ const pinnedAtStmt = scope.prepare("UPDATE session SET pinned_at = ? WHERE project_id = @project_id AND slug = ?");
18816
18907
  const snoozedUntilIfCurrentStmt = scope.prepare(`
18817
18908
  UPDATE session SET snoozed_until = ?
18818
18909
  WHERE project_id = @project_id AND slug = ? AND session_id = ? AND runtime_generation = ?
@@ -18995,7 +19086,7 @@ function createStorage(source, projectId) {
18995
19086
  WHERE project_id = @project_id AND id = ? AND state = 'open'
18996
19087
  `);
18997
19088
  const undeliveredSettlementsStmt = scope.prepare(
18998
- "SELECT * FROM thread_question WHERE project_id = @project_id AND state IN ('answered', 'dismissed') AND delivered = 0 ORDER BY settled_at, id"
19089
+ "SELECT * FROM thread_question WHERE project_id = @project_id AND state IN ('answered', 'dismissed') AND delivered = 0 ORDER BY asked_at, rowid"
18999
19090
  );
19000
19091
  const markSettlementDeliveredStmt = scope.prepare(
19001
19092
  "UPDATE thread_question SET delivered = 1 WHERE project_id = @project_id AND id = ? AND delivered = 0"
@@ -19168,6 +19259,7 @@ function createStorage(source, projectId) {
19168
19259
  SET model = ?, effort = ?, profile_revision = profile_revision + 1
19169
19260
  WHERE project_id = @project_id AND slug = ? AND session_id = ? AND runtime_generation = ?
19170
19261
  AND runtime_control IS NULL AND profile_pending_model IS NULL AND profile_pending_effort IS NULL
19262
+ AND (profile_set_at IS NULL OR backend = 'codex')
19171
19263
  AND (model IS NOT ? OR effort IS NOT ?)
19172
19264
  `);
19173
19265
  const beginRuntimeGenerationStmt = scope.prepare(`
@@ -19498,6 +19590,7 @@ function createStorage(source, projectId) {
19498
19590
  // never be written without a deadline to fire it.
19499
19591
  setSnoozedUntil: (slug, until, prompt = null) => void snoozedUntilStmt.run(until, until === null ? null : prompt, slug),
19500
19592
  setSnoozedUntilIfCurrent: (slug, sessionId, generation, until) => snoozedUntilIfCurrentStmt.run(until, slug, sessionId, generation).changes === 1,
19593
+ setPinnedAt: (slug, at) => void pinnedAtStmt.run(at, slug),
19501
19594
  setBgSnoozeRestedAtIfCurrent: (slug, sessionId, generation, restedAt) => bgSnoozeRestedAtIfCurrentStmt.run(restedAt, slug, sessionId, generation).changes === 1,
19502
19595
  setRecurringPromptIfCurrent: (slug, sessionId, generation, write) => recurringStmt.run(...recurringArgs(write), slug, sessionId, generation).changes === 1,
19503
19596
  setRecurringPromptBySlug: (slug, write) => recurringBySlugStmt.run(...recurringArgs(write), slug).changes === 1,
@@ -19505,7 +19598,7 @@ function createStorage(source, projectId) {
19505
19598
  resetSignoffNudges: (slug) => void resetNudgesStmt.run(slug),
19506
19599
  countParkBump: (slug, anchor) => void countParkBumpStmt.run(anchor, slug),
19507
19600
  resetParkBumps: (slug) => void resetParkBumpsStmt.run(slug),
19508
- armPrWatch: (watch) => void armPrWatchStmt.run(watch),
19601
+ armPrWatch: (watch2) => void armPrWatchStmt.run(watch2),
19509
19602
  listPrWatches: (slug, opts) => (opts?.armedOnly ? armedPrWatchesBySlugStmt : prWatchesBySlugStmt).all(slug),
19510
19603
  getPrWatch: (id) => prWatchByIdStmt.get(id),
19511
19604
  armedPrWatches: () => armedPrWatchesStmt.all(),
@@ -19782,6 +19875,7 @@ var init_storage = __esm({
19782
19875
  snoozed_until TEXT,
19783
19876
  snooze_prompt TEXT,
19784
19877
  bg_snooze_rested_at TEXT,
19878
+ pinned_at TEXT,
19785
19879
  meta TEXT,
19786
19880
  seen_at TEXT,
19787
19881
  transcript_id TEXT,
@@ -20196,6 +20290,9 @@ var init_settings = __esm({
20196
20290
  // 500K tokens: half the 1M window every Claude worker is dispatched with. See the schema for why a
20197
20291
  // worker left to compact at 1M is the biggest single reason a Frizz thread out-spends the TUI.
20198
20292
  autoCompactWindow: 5e5,
20293
+ // "auto" = the CLI's own tier (1h on a subscription). See the schema and claudePromptCacheEnv for
20294
+ // when "5m" is the cheaper choice.
20295
+ promptCacheTtl: "auto",
20199
20296
  // Hidden until asked for — see the schema for why.
20200
20297
  projectRail: false
20201
20298
  });
@@ -20345,13 +20442,13 @@ function discoverTranscriptDir(logDir, sessionId, memo3 = strandedLogDirs) {
20345
20442
  let best;
20346
20443
  for (const dir of memo3) best = better(best, dir);
20347
20444
  if (best) return best.dir;
20348
- let entries;
20445
+ let entries2;
20349
20446
  try {
20350
- entries = readdirSync3(dirname7(logDir), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
20447
+ entries2 = readdirSync3(dirname7(logDir), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
20351
20448
  } catch {
20352
20449
  return void 0;
20353
20450
  }
20354
- for (const entry of entries) best = better(best, join11(dirname7(logDir), entry));
20451
+ for (const entry of entries2) best = better(best, join11(dirname7(logDir), entry));
20355
20452
  if (!best) return void 0;
20356
20453
  memo3.add(best.dir);
20357
20454
  return best.dir;
@@ -21272,10 +21369,10 @@ function boundedJson(value, label, state, depth) {
21272
21369
  return value.map((entry, index) => boundedJson(entry, `${label}[${index}]`, state, depth + 1));
21273
21370
  }
21274
21371
  if (!value || typeof value !== "object") throw new ClaudeAgentSdkProtocolError(`${label} contains a non-JSON value`);
21275
- const entries = Object.entries(value);
21276
- if (entries.length > 256) throw new ClaudeAgentSdkProtocolError(`${label} contains too many object fields`);
21372
+ const entries2 = Object.entries(value);
21373
+ if (entries2.length > 256) throw new ClaudeAgentSdkProtocolError(`${label} contains too many object fields`);
21277
21374
  const output = {};
21278
- for (const [key, entry] of entries) {
21375
+ for (const [key, entry] of entries2) {
21279
21376
  if (RESERVED_KEYS.has(key) || utf8Bytes(key) > 256 || UNSAFE_TEXT2.test(key)) {
21280
21377
  UNSAFE_TEXT2.lastIndex = 0;
21281
21378
  throw new ClaudeAgentSdkProtocolError(`${label} contains an invalid object key`);
@@ -21414,10 +21511,10 @@ var init_claude_broker_diagnostics = __esm({
21414
21511
  // packages/server/src/detached-daemons.ts
21415
21512
  import { existsSync as existsSync4 } from "node:fs";
21416
21513
  import { fileURLToPath } from "node:url";
21417
- function resolveDetachedDaemonEntry(moduleUrl, basename13) {
21418
- const candidates = [`./${basename13}.js`, `./${basename13}.ts`].map((rel) => fileURLToPath(new URL(rel, moduleUrl)));
21514
+ function resolveDetachedDaemonEntry(moduleUrl, basename14) {
21515
+ const candidates = [`./${basename14}.js`, `./${basename14}.ts`].map((rel) => fileURLToPath(new URL(rel, moduleUrl)));
21419
21516
  const found = candidates.find((path) => existsSync4(path));
21420
- if (!found) throw new Error(`frizz is missing the ${basename13} entry \u2014 looked for ${candidates.join(" and ")}`);
21517
+ if (!found) throw new Error(`frizz is missing the ${basename14} entry \u2014 looked for ${candidates.join(" and ")}`);
21421
21518
  return found;
21422
21519
  }
21423
21520
  var init_detached_daemons = __esm({
@@ -22537,8 +22634,8 @@ function createTailStateCache(scope, schema = foldSchemaDigest()) {
22537
22634
  }
22538
22635
  return map;
22539
22636
  },
22540
- put(entries) {
22541
- if (!usable || !upsert || entries.length === 0) return;
22637
+ put(entries2) {
22638
+ if (!usable || !upsert || entries2.length === 0) return;
22542
22639
  try {
22543
22640
  const write = db.transaction((batch) => {
22544
22641
  for (const entry of batch) {
@@ -22559,7 +22656,7 @@ function createTailStateCache(scope, schema = foldSchemaDigest()) {
22559
22656
  });
22560
22657
  }
22561
22658
  });
22562
- write(entries);
22659
+ write(entries2);
22563
22660
  } catch {
22564
22661
  }
22565
22662
  },
@@ -24132,7 +24229,7 @@ ${ask}`;
24132
24229
  }
24133
24230
  }
24134
24231
  if (cacheDirty.size === 0) return;
24135
- const entries = [];
24232
+ const entries2 = [];
24136
24233
  try {
24137
24234
  for (const key of cacheDirty) {
24138
24235
  const foreign = key.startsWith("foreign:");
@@ -24141,12 +24238,12 @@ ${ask}`;
24141
24238
  const row = foreign ? null : deps.storage.getSession(key) ?? null;
24142
24239
  if (!foreign && !row) continue;
24143
24240
  const entry = cacheSnapshot(state, row);
24144
- if (entry) entries.push(entry);
24241
+ if (entry) entries2.push(entry);
24145
24242
  }
24146
24243
  } catch {
24147
24244
  }
24148
24245
  cacheDirty.clear();
24149
- tailCache.put(entries);
24246
+ tailCache.put(entries2);
24150
24247
  }
24151
24248
  function consume(state, backend, onLine) {
24152
24249
  let size;
@@ -25140,9 +25237,9 @@ function stringifyOutput(output) {
25140
25237
  }
25141
25238
  function collectRollouts(dir, out, budget) {
25142
25239
  if (budget.n <= 0) return;
25143
- const entries = safeReaddir(dir);
25144
- const dirs = entries.filter((e) => e.isDirectory()).sort(descByName);
25145
- const files = entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).sort(descByName);
25240
+ const entries2 = safeReaddir(dir);
25241
+ const dirs = entries2.filter((e) => e.isDirectory()).sort(descByName);
25242
+ const files = entries2.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).sort(descByName);
25146
25243
  for (const d of dirs) {
25147
25244
  if (budget.n <= 0) return;
25148
25245
  collectRollouts(join19(dir, d.name), out, budget);
@@ -28715,7 +28812,7 @@ var require_stream = __commonJS({
28715
28812
  terminateOnDestroy = false;
28716
28813
  duplex.destroy(err);
28717
28814
  });
28718
- ws.once("close", function close() {
28815
+ ws.once("close", function close2() {
28719
28816
  if (duplex.destroyed) return;
28720
28817
  duplex.push(null);
28721
28818
  });
@@ -28730,7 +28827,7 @@ var require_stream = __commonJS({
28730
28827
  called = true;
28731
28828
  callback(err2);
28732
28829
  });
28733
- ws.once("close", function close() {
28830
+ ws.once("close", function close2() {
28734
28831
  if (!called) callback(err);
28735
28832
  process.nextTick(emitClose, duplex);
28736
28833
  });
@@ -29262,13 +29359,18 @@ function claudeCompactionEnv(settings) {
29262
29359
  if (window === void 0 || !Number.isInteger(window) || window <= 0) return {};
29263
29360
  return { [CLAUDE_AUTO_COMPACT_WINDOW_ENV]: String(window) };
29264
29361
  }
29362
+ function claudePromptCacheEnv(settings) {
29363
+ const ttl = settings?.promptCacheTtl;
29364
+ if (ttl !== "5m" && ttl !== "1h") return {};
29365
+ return { [CLAUDE_PROMPT_CACHE_TTL_ENV]: ttl, [CLAUDE_SUBAGENT_PROMPT_CACHE_TTL_ENV]: ttl };
29366
+ }
29265
29367
  function claudeCompactionWindowOf(env) {
29266
29368
  const raw2 = env?.[CLAUDE_AUTO_COMPACT_WINDOW_ENV];
29267
29369
  if (raw2 === void 0) return void 0;
29268
29370
  const window = Number(raw2);
29269
29371
  return Number.isInteger(window) && window > 0 ? window : void 0;
29270
29372
  }
29271
- var FRIZZ_MCP, WORKER_MAX_WEB_SEARCHES, WORKER_MAX_SUBAGENTS, WORKER_MAX_CONCURRENT_SUBAGENTS, CLAUDE_WORKER_ENV, CLAUDE_AUTO_COMPACT_WINDOW_ENV, WORKER_DISALLOWED_TOOLS;
29373
+ 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;
29272
29374
  var init_types2 = __esm({
29273
29375
  "packages/server/src/backend/types.ts"() {
29274
29376
  "use strict";
@@ -29286,6 +29388,8 @@ var init_types2 = __esm({
29286
29388
  BASH_MAX_TIMEOUT_MS: String(24 * 60 * 60 * 1e3)
29287
29389
  };
29288
29390
  CLAUDE_AUTO_COMPACT_WINDOW_ENV = "CLAUDE_CODE_AUTO_COMPACT_WINDOW";
29391
+ CLAUDE_PROMPT_CACHE_TTL_ENV = "CLAUDE_CODE_PROMPT_CACHE_TTL";
29392
+ CLAUDE_SUBAGENT_PROMPT_CACHE_TTL_ENV = "CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL";
29289
29393
  WORKER_DISALLOWED_TOOLS = ["AskUserQuestion"];
29290
29394
  }
29291
29395
  });
@@ -29294,18 +29398,18 @@ var init_types2 = __esm({
29294
29398
  function tomlString(value) {
29295
29399
  return JSON.stringify(value);
29296
29400
  }
29297
- function inlineTable(entries) {
29298
- return `{${entries.map(([k, v]) => `${k}=${v}`).join(",")}}`;
29401
+ function inlineTable(entries2) {
29402
+ return `{${entries2.map(([k, v]) => `${k}=${v}`).join(",")}}`;
29299
29403
  }
29300
29404
  function serverTable(command, args, env) {
29301
- const entries = [
29405
+ const entries2 = [
29302
29406
  ["command", tomlString(command)],
29303
29407
  ["args", `[${args.map(tomlString).join(",")}]`]
29304
29408
  ];
29305
29409
  if (env && Object.keys(env).length > 0) {
29306
- entries.push(["env", inlineTable(Object.entries(env).map(([k, v]) => [k, tomlString(v)]))]);
29410
+ entries2.push(["env", inlineTable(Object.entries(env).map(([k, v]) => [k, tomlString(v)]))]);
29307
29411
  }
29308
- return inlineTable(entries);
29412
+ return inlineTable(entries2);
29309
29413
  }
29310
29414
  function codexMcpConfigArgs(frizzMcp, nodeBin = process.execPath) {
29311
29415
  const args = [];
@@ -30413,10 +30517,10 @@ function mcpFields(raw2) {
30413
30517
  if (schema.type !== "object" || schema.properties === null || typeof schema.properties !== "object" || Array.isArray(schema.properties)) {
30414
30518
  throw new RpcProtocolError(-32602, "MCP elicitation schema must describe object properties");
30415
30519
  }
30416
- const entries = Object.entries(schema.properties).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
30417
- if (entries.length > 32) throw new RpcProtocolError(-32602, "MCP elicitation has too many fields");
30520
+ const entries2 = Object.entries(schema.properties).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
30521
+ if (entries2.length > 32) throw new RpcProtocolError(-32602, "MCP elicitation has too many fields");
30418
30522
  const required = new Set(Array.isArray(schema.required) ? schema.required.filter((value) => typeof value === "string") : []);
30419
- return entries.map(([id, field]) => mcpField(id, field, required.has(id)));
30523
+ return entries2.map(([id, field]) => mcpField(id, field, required.has(id)));
30420
30524
  }
30421
30525
  function userInputFields(questions) {
30422
30526
  return questions.map((question) => {
@@ -32841,14 +32945,14 @@ function sessionsDir2(codexHome) {
32841
32945
  }
32842
32946
  function newestRollouts(dir, out, budget) {
32843
32947
  if (budget.n <= 0) return;
32844
- let entries;
32948
+ let entries2;
32845
32949
  try {
32846
- entries = readdirSync7(dir, { withFileTypes: true });
32950
+ entries2 = readdirSync7(dir, { withFileTypes: true });
32847
32951
  } catch {
32848
32952
  return;
32849
32953
  }
32850
- const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort(descByName2);
32851
- const files = entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).map((e) => e.name).sort(descByName2);
32954
+ const dirs = entries2.filter((e) => e.isDirectory()).map((e) => e.name).sort(descByName2);
32955
+ const files = entries2.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).map((e) => e.name).sort(descByName2);
32852
32956
  for (const d of dirs) {
32853
32957
  if (budget.n <= 0) return;
32854
32958
  newestRollouts(join22(dir, d), out, budget);
@@ -33607,7 +33711,7 @@ function readAwaitingPark(hints) {
33607
33711
  if (isAwaitingItemKind(h.kind)) {
33608
33712
  if (value) items.push({ kind: h.kind, value });
33609
33713
  } else if (h.kind === "for") {
33610
- forMs = parseAwaitingDuration(value);
33714
+ forMs = parseAwaitingDurationRaw(value);
33611
33715
  }
33612
33716
  }
33613
33717
  return { items, forMs };
@@ -33625,9 +33729,13 @@ function parkIsHonoured(park, live) {
33625
33729
  if (park.forMs === null) return false;
33626
33730
  return unaccountedItems(park.items, live).length === 0;
33627
33731
  }
33732
+ function parkForMaxMs(park) {
33733
+ if (park.items.length === 0) return AWAITING_FOR_MAX_MS;
33734
+ return park.items.every((i) => i.kind === "pr") ? PR_WATCH_FOR_MAX_MS : AWAITING_FOR_MAX_MS;
33735
+ }
33628
33736
  function parkExpiresAt(park, fenceAtMs) {
33629
33737
  if (park.forMs === null || !Number.isFinite(fenceAtMs)) return null;
33630
- return fenceAtMs + Math.min(park.forMs, AWAITING_FOR_MAX_MS);
33738
+ return fenceAtMs + Math.min(park.forMs, parkForMaxMs(park));
33631
33739
  }
33632
33740
  function readGithubStatusBook(raw2) {
33633
33741
  if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) return {};
@@ -33657,18 +33765,148 @@ var init_awaiting = __esm({
33657
33765
  }
33658
33766
  });
33659
33767
 
33768
+ // packages/server/src/backend/project-mcp-servers.ts
33769
+ import { readFileSync as readFileSync16 } from "node:fs";
33770
+ import { homedir as homedir10 } from "node:os";
33771
+ import { join as join25 } from "node:path";
33772
+ function readJsonObject(path) {
33773
+ try {
33774
+ const parsed = JSON.parse(readFileSync16(path, "utf8"));
33775
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
33776
+ } catch {
33777
+ return void 0;
33778
+ }
33779
+ }
33780
+ function objectField(source, key) {
33781
+ const value = source?.[key];
33782
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
33783
+ }
33784
+ function stringList(source, key) {
33785
+ const value = source?.[key];
33786
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
33787
+ }
33788
+ function stringMap(value) {
33789
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
33790
+ const out = {};
33791
+ for (const [key, entry] of Object.entries(value)) {
33792
+ if (typeof entry === "string") out[key] = entry;
33793
+ }
33794
+ return out;
33795
+ }
33796
+ function expandMcpEnvRefs(value, env) {
33797
+ let unresolved = false;
33798
+ const out = value.replace(ENV_REF, (_match, name, fallback) => {
33799
+ const resolved = env[name];
33800
+ if (resolved !== void 0) return resolved;
33801
+ if (fallback !== void 0) return fallback;
33802
+ unresolved = true;
33803
+ return "";
33804
+ });
33805
+ return unresolved ? void 0 : out;
33806
+ }
33807
+ function expandMap(map, env) {
33808
+ if (!map) return void 0;
33809
+ const out = {};
33810
+ for (const [key, value] of Object.entries(map)) {
33811
+ const expanded = expandMcpEnvRefs(value, env);
33812
+ if (expanded === void 0) return null;
33813
+ out[key] = expanded;
33814
+ }
33815
+ return out;
33816
+ }
33817
+ function normalizeMcpServer(raw2, env) {
33818
+ if (raw2 === null || typeof raw2 !== "object" || Array.isArray(raw2)) return void 0;
33819
+ const record = raw2;
33820
+ const type = typeof record.type === "string" ? record.type : void 0;
33821
+ if (type === "http" || type === "sse") {
33822
+ if (typeof record.url !== "string") return void 0;
33823
+ const url = expandMcpEnvRefs(record.url, env);
33824
+ const headers = expandMap(stringMap(record.headers), env);
33825
+ if (url === void 0 || headers === null) return void 0;
33826
+ return { type, url, ...headers ? { headers } : {} };
33827
+ }
33828
+ if (type !== void 0 && type !== "stdio") return void 0;
33829
+ if (typeof record.command !== "string") return void 0;
33830
+ const command = expandMcpEnvRefs(record.command, env);
33831
+ if (command === void 0) return void 0;
33832
+ const args = [];
33833
+ for (const entry of Array.isArray(record.args) ? record.args : []) {
33834
+ if (typeof entry !== "string") return void 0;
33835
+ const expanded = expandMcpEnvRefs(entry, env);
33836
+ if (expanded === void 0) return void 0;
33837
+ args.push(expanded);
33838
+ }
33839
+ const serverEnv = expandMap(stringMap(record.env), env);
33840
+ if (serverEnv === null) return void 0;
33841
+ return { command, ...args.length ? { args } : {}, ...serverEnv ? { env: serverEnv } : {} };
33842
+ }
33843
+ function collectApproval(layers) {
33844
+ const approval = { all: false, enabled: /* @__PURE__ */ new Set(), disabled: /* @__PURE__ */ new Set() };
33845
+ for (const layer of layers) {
33846
+ if (layer?.enableAllProjectMcpServers === true) approval.all = true;
33847
+ for (const name of stringList(layer, "enabledMcpjsonServers")) approval.enabled.add(name);
33848
+ for (const name of stringList(layer, "disabledMcpjsonServers")) approval.disabled.add(name);
33849
+ }
33850
+ return approval;
33851
+ }
33852
+ function userScopeRemoteServers(account, cwd, env) {
33853
+ const out = {};
33854
+ const maps = [objectField(account, "mcpServers"), objectField(objectField(objectField(account, "projects"), cwd), "mcpServers")];
33855
+ for (const map of maps) {
33856
+ for (const [name, raw2] of Object.entries(map ?? {})) {
33857
+ const server = normalizeMcpServer(raw2, env);
33858
+ if (server && server.type !== void 0 && server.type !== "stdio") out[name] = server;
33859
+ }
33860
+ }
33861
+ return out;
33862
+ }
33863
+ function projectMcpServers(cwd, options = {}) {
33864
+ const env = options.env ?? process.env;
33865
+ const home = options.home ?? homedir10();
33866
+ const configOverride = options.claudeConfigDir ?? (env.CLAUDE_CONFIG_DIR?.trim() ? env.CLAUDE_CONFIG_DIR.trim() : void 0);
33867
+ const accountFile = configOverride ? join25(configOverride, ".claude.json") : join25(home, ".claude.json");
33868
+ const userSettingsFile = join25(configOverride ?? join25(home, ".claude"), "settings.json");
33869
+ const account = readJsonObject(accountFile);
33870
+ const out = userScopeRemoteServers(account, cwd, env);
33871
+ const declared = objectField(readJsonObject(join25(cwd, ".mcp.json")), "mcpServers");
33872
+ if (!declared) return out;
33873
+ const approval = collectApproval([
33874
+ readJsonObject(join25(cwd, ".claude", "settings.json")),
33875
+ readJsonObject(join25(cwd, ".claude", "settings.local.json")),
33876
+ readJsonObject(userSettingsFile),
33877
+ objectField(objectField(account, "projects"), cwd)
33878
+ ]);
33879
+ for (const [name, raw2] of Object.entries(declared)) {
33880
+ if (approval.disabled.has(name)) continue;
33881
+ if (!approval.all && !approval.enabled.has(name)) continue;
33882
+ const server = normalizeMcpServer(raw2, env);
33883
+ if (server) out[name] = server;
33884
+ }
33885
+ return out;
33886
+ }
33887
+ function workerMcpServers(project, frizz) {
33888
+ return { ...project ?? {}, ...frizz ?? {} };
33889
+ }
33890
+ var ENV_REF;
33891
+ var init_project_mcp_servers = __esm({
33892
+ "packages/server/src/backend/project-mcp-servers.ts"() {
33893
+ "use strict";
33894
+ ENV_REF = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
33895
+ }
33896
+ });
33897
+
33660
33898
  // packages/server/src/worker-plugin-dir.ts
33661
33899
  import { existsSync as existsSync8 } from "node:fs";
33662
- import { dirname as dirname10, join as join25 } from "node:path";
33900
+ import { dirname as dirname10, join as join26 } from "node:path";
33663
33901
  import { fileURLToPath as fileURLToPath2 } from "node:url";
33664
33902
  function resolveWorkerPluginDir(moduleUrl = import.meta.url, env = process.env) {
33665
33903
  const override = env.FRIZZ_WORKER_PLUGIN_DIR;
33666
- if (override && existsSync8(join25(override, ".claude-plugin", "plugin.json")))
33904
+ if (override && existsSync8(join26(override, ".claude-plugin", "plugin.json")))
33667
33905
  return override;
33668
33906
  let current = dirname10(fileURLToPath2(moduleUrl));
33669
33907
  for (; ; ) {
33670
- const candidate = join25(current, "cc-worker");
33671
- if (existsSync8(join25(candidate, ".claude-plugin", "plugin.json"))) return candidate;
33908
+ const candidate = join26(current, "cc-worker");
33909
+ if (existsSync8(join26(candidate, ".claude-plugin", "plugin.json"))) return candidate;
33672
33910
  const parent = dirname10(current);
33673
33911
  if (parent === current) return void 0;
33674
33912
  current = parent;
@@ -33697,6 +33935,7 @@ function buildWorkerPrompt(kind = "claude", opts = {}) {
33697
33935
  AGENT_COMPLETION,
33698
33936
  VISUAL_EVIDENCE,
33699
33937
  QUALITY_BAR,
33938
+ TOKEN_ECONOMY,
33700
33939
  QUESTIONS,
33701
33940
  STOP_CRITERION,
33702
33941
  lean ? null : TRIVIAL_PROMPTS
@@ -33706,7 +33945,7 @@ function buildWorkerPrompt(kind = "claude", opts = {}) {
33706
33945
  out = out.replaceAll("{{FRIZZ_MONITORS_DIR}}", opts.monitorsDir?.trim() || MONITORS_DIR_FALLBACK);
33707
33946
  return out;
33708
33947
  }
33709
- var INLINE, INTRO, DEFER, OPENING, ACTIVITY_CAPTIONS, SIGNALS, AGENT_COMPLETION, VISUAL_EVIDENCE, QUALITY_BAR, QUESTIONS, STOP_CRITERION, TRIVIAL_PROMPTS, THREAD_NAME, SCRATCHPAD, BACKEND, SPAWN_THREAD, THREAD_EXECUTION, MONITORS_DIR_FALLBACK;
33948
+ 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;
33710
33949
  var init_workerPrompt = __esm({
33711
33950
  "packages/server/src/workerPrompt.ts"() {
33712
33951
  "use strict";
@@ -33914,7 +34153,14 @@ exactly ONE of them.
33914
34153
  Register FIRST: this line states the wait, it does not create one. That tool wakes you on CI
33915
34154
  turning green or red, and on every later review, approval or comment, for as long as the PR lives.
33916
34155
  - \`for:\` \u2014 **REQUIRED**, and a DURATION: \`30s\`, \`15m\`, \`2h\`, \`3d\`. Never an instant. When it runs
33917
- out frizz brings you back to re-check everything; re-parking is fine and uncapped.
34156
+ out frizz brings you back to re-check everything; re-parking is fine and uncapped. Capped at a day,
34157
+ except on a park naming ONLY \`prs:\`, where it runs to a year.
34158
+ - **A PULL REQUEST IN SOMEONE ELSE'S REPO TAKES MONTHS, SO ASK FOR MONTHS** \u2014 \`for: 180d\`, and give
34159
+ \`mcp__frizz__watch_pr\` the same. It moves on its maintainers' clock, not yours, so a short \`for:\`
34160
+ expires against a PR nothing has touched: a wake carrying no news, and a re-arm, once per expiry
34161
+ until somebody notices. Long is free \u2014 real activity wakes you the moment it lands either way, and
34162
+ the human snoozes or archives the thread when they want it gone. Hours are for a PR you control and
34163
+ a check you expect to settle today.
33918
34164
  - \`title:\` \u2014 OPTIONAL, the card's heading: **40 characters or fewer**, sentence case, naming THIS
33919
34165
  wait ("Fork-CI approval", "Nightly bench, arm 3 of 3"); longer is trimmed on a word boundary.
33920
34166
  Without it the card is headed "Awaiting", which is true of every park and specific to none.
@@ -34001,6 +34247,19 @@ reads when images are unavailable.`;
34001
34247
  formatter, a rename sweep) \u2014 run it, then name the files it touched in your handoff.
34002
34248
  - Ground every load-bearing claim in code, a command, or a doc you actually read \u2014 never memory. State
34003
34249
  plainly what failed, what you skipped, and what you could not verify.`;
34250
+ TOKEN_ECONOMY = `## Every message you send re-reads your whole context
34251
+
34252
+ The API call is the unit of cost, not the tokens you type: each message re-reads your entire context
34253
+ (150k\u2013450k tokens for most of an effort) whether it carries one tool call or ten.
34254
+
34255
+ - **Batch independent tool calls into ONE message.** List privately what you need next; then request
34256
+ every item that does not depend on another's result in the same response.
34257
+ - **One Bash call, several commands.** Chain the steps of one job with \`&&\` / \`;\` and print
34258
+ labelled sections. Use absolute paths; never spend a call on \`cd\` alone.
34259
+ - **Never poll from the turn.** A \`sleep\`-and-check loop is a full re-read every iteration. A
34260
+ background shell, a sub-agent or a registered watch wakes you when the thing is done.
34261
+ - **Finish the turn's whole job before you rest.** Every wake is a new turn on the full context, so
34262
+ do not rest on a step you could take now.`;
34004
34263
  QUESTIONS = `## Questions for the human
34005
34264
 
34006
34265
  You run under a dashboard, not a live chat, so your FINAL MESSAGE is the whole interface. Open with 2-4
@@ -34055,10 +34314,11 @@ fence is answerable only while its message is on screen, so a question written i
34055
34314
  the transcript scrolls or your context is compacted, while a registration is still there tomorrow. It
34056
34315
  takes \`questions\` \u2014 several at once, each with \`question\`, \`kind\` (\`question\` = pick one,
34057
34316
  \`multi\` = pick several, no options at all = free text), an optional \`danger\`, and \`options\` each
34058
- carrying a \`label\`, a one-line \`description\`, an optional \`recommended\`, an optional \`preview\`
34059
- (markdown the human sees when they pick it \u2014 the diff, the message that would be posted), and optional
34060
- \`followUps\`: questions that become live ONLY if that option is taken, so you can ask "and if so,
34061
- which?" without asking it of somebody who said no. **REGISTERING A QUESTION DOES NOT END YOUR TURN** \u2014
34317
+ carrying a \`label\`, a \`description\` (one line for a simple fork, or FULL MARKDOWN over several
34318
+ lines \u2014 a list of consequences, a code block, the diff an option would produce, the message that would
34319
+ be posted; it renders inside the option, so the human sees it BEFORE choosing), an optional
34320
+ \`recommended\`, and optional \`followUps\`: questions that become live ONLY if that option is taken,
34321
+ 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
34062
34322
  it carries no timeout and waits on a person, so keep doing everything that does not depend on the
34063
34323
  answer. The answer comes back as its own wake, restating what was asked. Withdraw one you no longer
34064
34324
  need with \`mcp__frizz__unask\`, above all when you work the answer out yourself. **ON A THREAD
@@ -34793,12 +35053,12 @@ var init_claude_broker_client = __esm({
34793
35053
  });
34794
35054
 
34795
35055
  // packages/server/src/backend/claude-permission-interactions.ts
34796
- import { homedir as homedir10 } from "node:os";
35056
+ import { homedir as homedir11 } from "node:os";
34797
35057
  function clip(text2, max) {
34798
35058
  return text2.length > max ? `${text2.slice(0, max - 1)}\u2026` : text2;
34799
35059
  }
34800
35060
  function tildePath(path) {
34801
- const home = homedir10();
35061
+ const home = homedir11();
34802
35062
  if (!path.startsWith(home)) return path;
34803
35063
  const rest = path.slice(home.length);
34804
35064
  return rest === "" ? "~" : rest.startsWith("/") ? `~${rest}` : path;
@@ -35044,7 +35304,7 @@ var init_claude_permission_interactions = __esm({
35044
35304
  import { randomUUID as randomUUID12 } from "node:crypto";
35045
35305
  function withFrizzThreadSlug(servers, slug) {
35046
35306
  const frizz = servers?.[FRIZZ_MCP.name];
35047
- if (!frizz) return servers;
35307
+ if (!frizz || !("command" in frizz)) return servers;
35048
35308
  return { ...servers, [FRIZZ_MCP.name]: { ...frizz, env: { ...frizz.env, FRIZZ_THREAD_SLUG: slug } } };
35049
35309
  }
35050
35310
  function claudeBrokerBridgeEnabled(env = process.env) {
@@ -35172,6 +35432,7 @@ function createClaudeAgentBrokerBridge(deps) {
35172
35432
  // The auto-compact ceiling, from Settings (500K by default). Without it a `[1m]` worker compacts
35173
35433
  // only near 1M, re-sending up to 5x a TUI session's conversation on every turn past 200K.
35174
35434
  ...claudeCompactionEnv(deps.getSettings?.()),
35435
+ ...claudePromptCacheEnv(deps.getSettings?.()),
35175
35436
  ...we?.permDir ? { FRIZZ_PERM_DIR: we.permDir } : {},
35176
35437
  // The cc-worker plugin's PreToolUse hook DENIES AskUserQuestion, because without frizz in the
35177
35438
  // loop a blocking question freezes a headless worker where nobody can answer it. On the broker path
@@ -35447,19 +35708,19 @@ var init_claude_agent_broker_bridge = __esm({
35447
35708
  });
35448
35709
 
35449
35710
  // packages/server/src/backend/auth-status.ts
35450
- import { join as join26 } from "node:path";
35451
- import { homedir as homedir11, platform as platform2 } from "node:os";
35452
- import { readFileSync as readFileSync16, statSync as statSync11 } from "node:fs";
35711
+ import { join as join27 } from "node:path";
35712
+ import { homedir as homedir12, platform as platform2 } from "node:os";
35713
+ import { readFileSync as readFileSync17, statSync as statSync11 } from "node:fs";
35453
35714
  import { execFile as execFile4 } from "node:child_process";
35454
35715
  import { promisify as promisify4 } from "node:util";
35455
35716
  function claudeConfigDir2() {
35456
35717
  const override = process.env.CLAUDE_CONFIG_DIR;
35457
- return override && override.trim() ? override : join26(homedir11(), ".claude");
35718
+ return override && override.trim() ? override : join27(homedir12(), ".claude");
35458
35719
  }
35459
35720
  function claudeFileState(configDir) {
35460
35721
  let raw2;
35461
35722
  try {
35462
- raw2 = readFileSync16(join26(configDir, ".credentials.json"), "utf8");
35723
+ raw2 = readFileSync17(join27(configDir, ".credentials.json"), "utf8");
35463
35724
  } catch (err) {
35464
35725
  return err.code === "ENOENT" ? "absent" : "error";
35465
35726
  }
@@ -35492,7 +35753,7 @@ function readCodexAuthState(codexHome = defaultCodexHome()) {
35492
35753
  if (process.env.OPENAI_API_KEY || process.env.CODEX_API_KEY || process.env.CODEX_ACCESS_TOKEN) return "authed";
35493
35754
  let raw2;
35494
35755
  try {
35495
- raw2 = readFileSync16(join26(codexHome, "auth.json"), "utf8");
35756
+ raw2 = readFileSync17(join27(codexHome, "auth.json"), "utf8");
35496
35757
  } catch (err) {
35497
35758
  return err.code === "ENOENT" ? "signed-out" : "unknown";
35498
35759
  }
@@ -35557,7 +35818,7 @@ function asEmail(value) {
35557
35818
  }
35558
35819
  function claudeAccountFile() {
35559
35820
  const override = process.env.CLAUDE_CONFIG_DIR;
35560
- return override && override.trim() ? join26(override, ".claude.json") : join26(homedir11(), ".claude.json");
35821
+ return override && override.trim() ? join27(override, ".claude.json") : join27(homedir12(), ".claude.json");
35561
35822
  }
35562
35823
  function readClaudeAccountEmail(path = claudeAccountFile()) {
35563
35824
  let stat2;
@@ -35570,7 +35831,7 @@ function readClaudeAccountEmail(path = claudeAccountFile()) {
35570
35831
  if (memo3 && memo3.path === path && memo3.mtimeMs === stat2.mtimeMs && memo3.size === stat2.size) return memo3.email;
35571
35832
  let email;
35572
35833
  try {
35573
- const doc = JSON.parse(readFileSync16(path, "utf8"));
35834
+ const doc = JSON.parse(readFileSync17(path, "utf8"));
35574
35835
  email = asEmail(doc?.oauthAccount?.emailAddress);
35575
35836
  } catch {
35576
35837
  email = void 0;
@@ -35581,7 +35842,7 @@ function readClaudeAccountEmail(path = claudeAccountFile()) {
35581
35842
  function readCodexAccountEmail(codexHome = defaultCodexHome()) {
35582
35843
  let doc;
35583
35844
  try {
35584
- doc = JSON.parse(readFileSync16(join26(codexHome, "auth.json"), "utf8"));
35845
+ doc = JSON.parse(readFileSync17(join27(codexHome, "auth.json"), "utf8"));
35585
35846
  } catch {
35586
35847
  return void 0;
35587
35848
  }
@@ -35649,8 +35910,8 @@ var init_auth_status = __esm({
35649
35910
  });
35650
35911
 
35651
35912
  // packages/server/src/dispatch.ts
35652
- import { closeSync as closeSync12, constants, existsSync as existsSync9, fstatSync as fstatSync2, lstatSync as lstatSync3, openSync as openSync13, readFileSync as readFileSync17, realpathSync as realpathSync7, statSync as statSync12, writeFileSync as writeFileSync10, mkdirSync as mkdirSync13, rmSync as rmSync9 } from "node:fs";
35653
- import { basename as basename6, join as join27, dirname as dirname11 } from "node:path";
35913
+ 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";
35914
+ import { basename as basename6, join as join28, dirname as dirname11 } from "node:path";
35654
35915
  import { createHash as createHash11, randomUUID as randomUUID13 } from "node:crypto";
35655
35916
  function fallbackTitle(prompt) {
35656
35917
  const firstLine2 = prompt.trim().split("\n", 1)[0].trim();
@@ -35669,7 +35930,7 @@ function fallbackTitle(prompt) {
35669
35930
  }
35670
35931
  function resolveSlug(frizzDir, base, taken) {
35671
35932
  base = ThreadSlug.parse(base);
35672
- const isTaken = (slug) => existsSync9(join27(frizzDir, `${slug}.md`)) || (taken?.(slug) ?? false);
35933
+ const isTaken = (slug) => existsSync9(join28(frizzDir, `${slug}.md`)) || (taken?.(slug) ?? false);
35673
35934
  if (!isTaken(base)) return base;
35674
35935
  for (let n = 2; ; n++) {
35675
35936
  const suffix = `-${n}`;
@@ -35686,12 +35947,12 @@ function resolveLegacyThreadFile(projectDir, value) {
35686
35947
  if (!parsed.success) return null;
35687
35948
  try {
35688
35949
  const projectRoot = realpathSync7(projectDir);
35689
- const frizzPath = join27(projectRoot, ".frizz");
35950
+ const frizzPath = join28(projectRoot, ".frizz");
35690
35951
  const frizzStat = lstatSync3(frizzPath);
35691
35952
  if (!frizzStat.isDirectory() || frizzStat.isSymbolicLink()) return null;
35692
35953
  const realFrizz = realpathSync7(frizzPath);
35693
35954
  if (dirname11(realFrizz) !== projectRoot || basename6(realFrizz) !== ".frizz") return null;
35694
- const path = join27(realFrizz, `${parsed.data}.md`);
35955
+ const path = join28(realFrizz, `${parsed.data}.md`);
35695
35956
  const before = lstatSync3(path);
35696
35957
  if (!before.isFile() || before.isSymbolicLink()) return null;
35697
35958
  const realPath = realpathSync7(path);
@@ -35702,7 +35963,7 @@ function resolveLegacyThreadFile(projectDir, value) {
35702
35963
  const fd = openSync13(path, constants.O_RDONLY | constants.O_NOFOLLOW);
35703
35964
  try {
35704
35965
  openedBefore = fstatSync2(fd);
35705
- contents = readFileSync17(fd);
35966
+ contents = readFileSync18(fd);
35706
35967
  openedAfter = fstatSync2(fd);
35707
35968
  } finally {
35708
35969
  closeSync12(fd);
@@ -35735,7 +35996,7 @@ function boardAuthorizesAdoption(board, slug) {
35735
35996
  return !board.errorItems.some((item) => item.file === `${slug}.md`);
35736
35997
  }
35737
35998
  function ensureSafeDirectDirectory(parent, name) {
35738
- const path = join27(parent, name);
35999
+ const path = join28(parent, name);
35739
36000
  try {
35740
36001
  mkdirSync13(path);
35741
36002
  } catch (error) {
@@ -35765,8 +36026,8 @@ function loadWorkerPrompt(kind = "claude") {
35765
36026
  function monitorScriptsDir() {
35766
36027
  const plugin = workerPluginDir();
35767
36028
  if (!plugin) return void 0;
35768
- const dir = join27(plugin, "skills", "gh", "scripts");
35769
- return existsSync9(join27(dir, "ci-watch.mjs")) ? dir : void 0;
36029
+ const dir = join28(plugin, "skills", "gh", "scripts");
36030
+ return existsSync9(join28(dir, "ci-watch.mjs")) ? dir : void 0;
35770
36031
  }
35771
36032
  function codexScratchpadHookConfig(hookScript, sessionId) {
35772
36033
  if (!hookScript || !sessionId) return {};
@@ -35778,7 +36039,7 @@ function codexScratchpadHookConfig(hookScript, sessionId) {
35778
36039
  }
35779
36040
  ]
35780
36041
  });
35781
- const bashBackgroundHook = join27(dirname11(hookScript), "bash-background.mjs");
36042
+ const bashBackgroundHook = join28(dirname11(hookScript), "bash-background.mjs");
35782
36043
  return {
35783
36044
  bypass_hook_trust: true,
35784
36045
  hooks: {
@@ -35804,7 +36065,7 @@ function codexScratchpadHookConfig(hookScript, sessionId) {
35804
36065
  }
35805
36066
  function scratchpadHookScript() {
35806
36067
  const plugin = workerPluginDir();
35807
- return plugin ? join27(plugin, "hooks", "scratchpad.mjs") : void 0;
36068
+ return plugin ? join28(plugin, "hooks", "scratchpad.mjs") : void 0;
35808
36069
  }
35809
36070
  function composePrompt(sessionId, prompt, kind = "claude") {
35810
36071
  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.";
@@ -35820,12 +36081,12 @@ function scratchpadOrientation(sessionId, kind = "claude") {
35820
36081
  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.`;
35821
36082
  }
35822
36083
  function frizzConfigBlock(projectDir) {
35823
- const path = join27(projectDir, "FRIZZ.md");
36084
+ const path = join28(projectDir, "FRIZZ.md");
35824
36085
  let body;
35825
36086
  try {
35826
36087
  const st = statSync12(path);
35827
36088
  if (!st.isFile() || st.size > FRIZZ_MD_MAX_BYTES) return "";
35828
- body = readFileSync17(path, "utf8").trim();
36089
+ body = readFileSync18(path, "utf8").trim();
35829
36090
  } catch {
35830
36091
  return "";
35831
36092
  }
@@ -35860,7 +36121,7 @@ function resolveFrizzMcp(target, moduleUrl = import.meta.url, env = process.env,
35860
36121
  const { stateDir, serverLock, projectId } = typeof target === "string" ? { stateDir: target } : target;
35861
36122
  const pluginDir = resolveWorkerPluginDir(moduleUrl, env);
35862
36123
  if (!pluginDir) return void 0;
35863
- const scriptPath = join27(pluginDir, "bin", FRIZZ_MCP.script);
36124
+ const scriptPath = join28(pluginDir, "bin", FRIZZ_MCP.script);
35864
36125
  if (!existsSync9(scriptPath)) return void 0;
35865
36126
  return {
35866
36127
  scriptPath,
@@ -35870,7 +36131,7 @@ function resolveFrizzMcp(target, moduleUrl = import.meta.url, env = process.env,
35870
36131
  ...slug ? { slug } : {}
35871
36132
  };
35872
36133
  }
35873
- function claudeMcpConfig(mcp) {
36134
+ function claudeMcpConfig(mcp, project) {
35874
36135
  const mcpServers = {};
35875
36136
  const allowedTools = [];
35876
36137
  if (mcp) {
@@ -35878,13 +36139,16 @@ function claudeMcpConfig(mcp) {
35878
36139
  mcpServers[FRIZZ_MCP.name] = { command: process.execPath, args: [mcp.scriptPath], env };
35879
36140
  allowedTools.push(`mcp__${FRIZZ_MCP.name}`);
35880
36141
  }
35881
- return { mcpServers, allowedTools };
36142
+ return { mcpServers: workerMcpServers(project, mcpServers), allowedTools };
35882
36143
  }
35883
- function claudeMcpFlags(mcp) {
35884
- const { mcpServers, allowedTools } = claudeMcpConfig(mcp);
35885
- if (Object.keys(mcpServers).length === 0) return [];
36144
+ function claudeMcpFlags(mcp, project) {
36145
+ const { mcpServers, allowedTools } = claudeMcpConfig(mcp, project);
36146
+ const argv = ["--strict-mcp-config"];
36147
+ if (Object.keys(mcpServers).length === 0) return argv;
35886
36148
  const config = JSON.stringify({ mcpServers });
35887
- return ["--mcp-config", config, `--allowedTools=${allowedTools.join(",")}`];
36149
+ argv.push("--mcp-config", config);
36150
+ if (allowedTools.length > 0) argv.push(`--allowedTools=${allowedTools.join(",")}`);
36151
+ return argv;
35888
36152
  }
35889
36153
  function workerDisallowedToolFlags() {
35890
36154
  return [`--disallowedTools=${WORKER_DISALLOWED_TOOLS.join(",")}`];
@@ -35896,7 +36160,7 @@ function buildClaudeCommand(opts) {
35896
36160
  if (effort.effort) argv.push("--effort", effort.effort);
35897
36161
  argv.push(...claudeUltracodeFlags(effort));
35898
36162
  if (opts.pluginDir) argv.push("--plugin-dir", opts.pluginDir);
35899
- argv.push(...claudeMcpFlags(opts.frizzMcp));
36163
+ argv.push(...claudeMcpFlags(opts.frizzMcp, opts.projectMcpServers));
35900
36164
  argv.push(...workerDisallowedToolFlags());
35901
36165
  const worker = opts.workerPrompt ?? loadWorkerPrompt();
35902
36166
  const system = [worker, opts.extraSystemPrompt?.trim()].filter(Boolean).join("\n\n");
@@ -35929,7 +36193,7 @@ function buildClaudeResumeCommand(opts) {
35929
36193
  if (effort.effort) argv.push("--effort", effort.effort);
35930
36194
  argv.push(...claudeUltracodeFlags(effort));
35931
36195
  if (opts.pluginDir) argv.push("--plugin-dir", opts.pluginDir);
35932
- argv.push(...claudeMcpFlags(opts.frizzMcp));
36196
+ argv.push(...claudeMcpFlags(opts.frizzMcp, opts.projectMcpServers));
35933
36197
  argv.push(...workerDisallowedToolFlags());
35934
36198
  const worker = opts.workerPrompt ?? loadWorkerPrompt();
35935
36199
  const system = [worker, opts.extraSystemPrompt?.trim()].filter(Boolean).join("\n\n");
@@ -35940,7 +36204,7 @@ function buildClaudeResumeCommand(opts) {
35940
36204
  }
35941
36205
  function createDispatcher(deps) {
35942
36206
  const readBoardSource = deps.readBoard ?? readBoard;
35943
- const frizzDir = join27(deps.project.dir, ".frizz");
36207
+ const frizzDir = join28(deps.project.dir, ".frizz");
35944
36208
  const adoptionRuntime = deps.adoptionRuntime ?? productionRuntime;
35945
36209
  function cleanupPrewrites(built) {
35946
36210
  for (const path of new Set(built.prewrite.map((file) => file.path))) {
@@ -35953,7 +36217,7 @@ function createDispatcher(deps) {
35953
36217
  function cleanupDispatchFiles(scratchRel, built, sessionId) {
35954
36218
  cleanupPrewrites(built);
35955
36219
  try {
35956
- rmSync9(join27(deps.project.dir, scratchRel), { force: true, recursive: true });
36220
+ rmSync9(join28(deps.project.dir, scratchRel), { force: true, recursive: true });
35957
36221
  } catch {
35958
36222
  }
35959
36223
  cleanupAdoptionSessionFiles(deps.project.dir, sessionId);
@@ -36328,6 +36592,7 @@ var init_dispatch = __esm({
36328
36592
  init_src();
36329
36593
  init_logging();
36330
36594
  init_project();
36595
+ init_project_mcp_servers();
36331
36596
  init_types2();
36332
36597
  init_types2();
36333
36598
  init_worker_plugin_dir();
@@ -36385,8 +36650,8 @@ var init_dispatch = __esm({
36385
36650
  import {
36386
36651
  watch as fsWatch
36387
36652
  } from "node:fs";
36388
- import { homedir as homedir12 } from "node:os";
36389
- import { join as join28 } from "node:path";
36653
+ import { homedir as homedir13 } from "node:os";
36654
+ import { join as join29 } from "node:path";
36390
36655
  import watcher from "@parcel/watcher";
36391
36656
  function appServerTurnStalled(liveness, lastActivityAt, nowMs) {
36392
36657
  if (!liveness) return false;
@@ -36515,7 +36780,7 @@ function safeQuestionAnswer(raw2) {
36515
36780
  return void 0;
36516
36781
  }
36517
36782
  }
36518
- function answersInFlight(rows, lastUserAt) {
36783
+ function answersInFlight(rows, lastUserAt, wakeOnDismissals = false) {
36519
36784
  const userAt = lastUserAt ? Date.parse(lastUserAt) : Number.NaN;
36520
36785
  const answers = [];
36521
36786
  const dismissed = [];
@@ -36531,7 +36796,8 @@ function answersInFlight(rows, lastUserAt) {
36531
36796
  const parsed = safeQuestionAnswer(q.answer);
36532
36797
  if (parsed) answers.push(parsed);
36533
36798
  }
36534
- return answers.length > 0 ? questionAnswerMessage(answers, dismissed) : void 0;
36799
+ if (answers.length > 0) return questionAnswerMessage(answers, dismissed);
36800
+ return wakeOnDismissals && dismissed.length > 0 ? questionsCancelledWakeMessage(dismissed.length) : void 0;
36535
36801
  }
36536
36802
  function hasRegisteredBackgroundPark(tele, armedWatches, nowMs) {
36537
36803
  return armedWatches.some((w) => {
@@ -36681,7 +36947,8 @@ function resolveSessionProfile(row, tele) {
36681
36947
  const observedAt = tele?.profileAt ? Date.parse(tele.profileAt) : NaN;
36682
36948
  const spawnedAt = Date.parse(row.spawned_at);
36683
36949
  const setAt = row.profile_set_at ? Date.parse(row.profile_set_at) : NaN;
36684
- const supersededByOperatorSet = Number.isFinite(setAt) && setAt > observedAt;
36950
+ const operatorSetSticks = (row.backend ?? "claude") !== "codex";
36951
+ const supersededByOperatorSet = Number.isFinite(setAt) && (operatorSetSticks || setAt > observedAt);
36685
36952
  const observedIsCurrent = Number.isFinite(observedAt) && Number.isFinite(spawnedAt) && observedAt >= spawnedAt && !supersededByOperatorSet;
36686
36953
  const observedModel = tele?.model ? normalizeObservedThreadModel(row.backend ?? "claude", tele.model) ?? tele.model.trim() : void 0;
36687
36954
  const model = (!persistedModel || observedIsCurrent ? observedModel : void 0) || persistedModel;
@@ -36781,7 +37048,7 @@ function sessionThreadView(projectDir, storage, row, rawTele, registeredLegacyTe
36781
37048
  const spec = safeQuestionSpec(q.spec);
36782
37049
  if (spec) questions.push({ id: q.id, spec, askedAt: new Date(q.asked_at).toISOString() });
36783
37050
  }
36784
- const inFlightAnswers = answersInFlight(questionRows, rawTele?.lastUserAt);
37051
+ const inFlightAnswers = answersInFlight(questionRows, rawTele?.lastUserAt, row.recurring_on_rest === 1 && Boolean(row.recurring_prompt?.trim()));
36785
37052
  const armedWatches = storage.listThreadWatches(row.slug, { armedOnly: true }).map((w) => ({
36786
37053
  id: w.id,
36787
37054
  kind: w.kind,
@@ -36866,6 +37133,7 @@ function sessionThreadView(projectDir, storage, row, rawTele, registeredLegacyTe
36866
37133
  // Only meaningful while the snooze is still pending — a prompt without a live deadline is an
36867
37134
  // already-delivered (or superseded) bump the row has not been swept clean of yet.
36868
37135
  snoozePrompt: snoozedUntil ? row.snooze_prompt ?? void 0 : void 0,
37136
+ pinnedAt: row.pinned_at ?? void 0,
36869
37137
  // The event-snooze, as a fact the CHAT can read. `awaitingBackground` still ignores it — that flag
36870
37138
  // states whether the thread is waiting, which the snooze does not change — so the suppression is a
36871
37139
  // presentation rule the client applies, not a second opinion about the thread's state.
@@ -37083,7 +37351,7 @@ function createBoard(project, storage, bus, tailer, bootId, deps = {}) {
37083
37351
  ...project.githubRepo ? { githubRepo: project.githubRepo } : {},
37084
37352
  projectSlug: findByPath(project.dir)?.slug,
37085
37353
  // So the client can expand a `~` a worker wrote in prose (see BoardSnapshot.homeDir).
37086
- homeDir: homedir12()
37354
+ homeDir: homedir13()
37087
37355
  };
37088
37356
  const sessionThreads = buildSessionThreads(assembledAtMs);
37089
37357
  armSnoozeWake(sessionThreads, assembledAtMs);
@@ -37184,7 +37452,7 @@ function createBoard(project, storage, bus, tailer, bootId, deps = {}) {
37184
37452
  if (parcelSub || stopped) return Promise.resolve();
37185
37453
  if (watchSetup) return watchSetup;
37186
37454
  const setup = (async () => {
37187
- const next = await subscribe(join28(project.dir, ".frizz"), () => scheduleRebuild());
37455
+ const next = await subscribe(join29(project.dir, ".frizz"), () => scheduleRebuild());
37188
37456
  if (stopped) {
37189
37457
  await next.unsubscribe();
37190
37458
  return;
@@ -37284,6 +37552,9 @@ var init_board = __esm({
37284
37552
  });
37285
37553
 
37286
37554
  // packages/server/src/wake-store.ts
37555
+ function isQuietWindowExempt(hintKey) {
37556
+ return WAKE_QUIET_EXEMPT_HINT_PREFIXES.some((prefix) => hintKey.startsWith(prefix));
37557
+ }
37287
37558
  function delivery(row) {
37288
37559
  return {
37289
37560
  id: row.id,
@@ -37334,14 +37605,22 @@ function ensureWakeDeliverySchema(db) {
37334
37605
  ON wake_delivery(project_id, state, next_attempt_at, created_at);
37335
37606
  `);
37336
37607
  }
37337
- function createWakeDeliveryStore(scope) {
37608
+ function createWakeDeliveryStore(scope, options = {}) {
37338
37609
  const db = scope.db;
37339
37610
  ensureWakeDeliverySchema(db);
37611
+ const quietWindowMs = Math.max(0, options.quietWindowMs ?? WAKE_QUIET_WINDOW_MS);
37340
37612
  const byId = scope.prepare("SELECT * FROM wake_delivery WHERE project_id = @project_id AND id = ?");
37341
37613
  const all = scope.prepare("SELECT * FROM wake_delivery WHERE project_id = @project_id ORDER BY created_at, id");
37342
37614
  const open = scope.prepare(
37343
37615
  "SELECT * FROM wake_delivery WHERE project_id = @project_id AND state IN ('pending', 'leased') ORDER BY created_at, id"
37344
37616
  );
37617
+ const pendingForStmt = scope.prepare(
37618
+ "SELECT * FROM wake_delivery WHERE project_id = @project_id AND thread_slug = ? AND session_id = ? AND state = 'pending' ORDER BY created_at, id"
37619
+ );
37620
+ const lastHandoffStmt = scope.prepare(`
37621
+ SELECT MAX(MAX(COALESCE(delivered_at, 0), COALESCE(sent_at, 0))) AS at
37622
+ FROM wake_delivery WHERE project_id = @project_id AND thread_slug = ?
37623
+ `);
37345
37624
  const insert = scope.prepare(`
37346
37625
  INSERT INTO wake_delivery (
37347
37626
  project_id, id, thread_slug, session_id, fence_id, hint_key, message, reason, state, attempts,
@@ -37349,7 +37628,7 @@ function createWakeDeliveryStore(scope) {
37349
37628
  delivered_at, terminal_at
37350
37629
  ) VALUES (
37351
37630
  @project_id, @id, @slug, @sessionId, @fenceId, @hintKey, @message, @reason, 'pending', 0,
37352
- @now, NULL, NULL, NULL, @now, @now, NULL, NULL
37631
+ @notBefore, NULL, NULL, NULL, @now, @now, NULL, NULL
37353
37632
  )
37354
37633
  ON CONFLICT DO NOTHING
37355
37634
  `);
@@ -37376,6 +37655,12 @@ function createWakeDeliveryStore(scope) {
37376
37655
  last_error = NULL, sent_at = NULL, updated_at = @now
37377
37656
  WHERE project_id = @project_id AND id = @id AND state = 'pending' AND next_attempt_at <= @now AND attempts < @maxAttempts
37378
37657
  `);
37658
+ const adoptStmt = scope.prepare(`
37659
+ UPDATE wake_delivery SET
37660
+ state = 'leased', attempts = attempts + 1, lease_owner = @owner, lease_until = @leaseUntil,
37661
+ last_error = NULL, sent_at = NULL, updated_at = @now
37662
+ WHERE project_id = @project_id AND id = @id AND state = 'pending' AND attempts < @maxAttempts
37663
+ `);
37379
37664
  const deferFailureStmt = scope.prepare(`
37380
37665
  UPDATE wake_delivery SET
37381
37666
  lease_until = @retryAt, last_error = @error, updated_at = @now
@@ -37416,7 +37701,12 @@ function createWakeDeliveryStore(scope) {
37416
37701
  WHERE project_id = @project_id AND id = @id AND state IN ('pending', 'leased')
37417
37702
  `);
37418
37703
  const enqueueTxn = db.transaction((input, now) => {
37419
- const created = insert.run({ ...input, now }).changes === 1;
37704
+ let notBefore = now;
37705
+ if (quietWindowMs > 0 && !isQuietWindowExempt(input.hintKey)) {
37706
+ const lastHandoff = lastHandoffStmt.get(input.slug)?.at ?? 0;
37707
+ if (lastHandoff > 0) notBefore = Math.max(now, lastHandoff + quietWindowMs);
37708
+ }
37709
+ const created = insert.run({ ...input, now, notBefore }).changes === 1;
37420
37710
  const row = byId.get(input.id);
37421
37711
  if (!row) throw new Error("wake delivery disappeared while it was being enqueued");
37422
37712
  if (row.thread_slug !== input.slug || row.session_id !== input.sessionId || row.fence_id !== input.fenceId || row.hint_key !== input.hintKey || row.message !== input.message || row.reason !== input.reason) {
@@ -37432,6 +37722,10 @@ function createWakeDeliveryStore(scope) {
37432
37722
  if (claimStmt.run({ id: candidate.id, owner, now, leaseUntil, maxAttempts }).changes !== 1) return void 0;
37433
37723
  return delivery(byId.get(candidate.id));
37434
37724
  });
37725
+ const adoptTxn = db.transaction((id, owner, now, leaseUntil, maxAttempts) => {
37726
+ if (adoptStmt.run({ id, owner, now, leaseUntil, maxAttempts }).changes !== 1) return void 0;
37727
+ return delivery(byId.get(id));
37728
+ });
37435
37729
  return {
37436
37730
  enqueue: (input, now) => enqueueTxn.immediate(input, now),
37437
37731
  get: (id) => {
@@ -37440,7 +37734,9 @@ function createWakeDeliveryStore(scope) {
37440
37734
  },
37441
37735
  list: () => all.all().map(delivery),
37442
37736
  listOpen: () => open.all().map(delivery),
37737
+ pendingFor: (slug, sessionId) => pendingForStmt.all(slug, sessionId).map(delivery),
37443
37738
  claim: (owner, now, leaseUntil, maxAttempts) => claimTxn.immediate(owner, now, leaseUntil, maxAttempts),
37739
+ adopt: (id, owner, now, leaseUntil, maxAttempts) => adoptTxn.immediate(id, owner, now, leaseUntil, maxAttempts),
37444
37740
  deferFailure: (id, owner, now, retryAt, error) => deferFailureStmt.run({
37445
37741
  id,
37446
37742
  owner,
@@ -37459,10 +37755,12 @@ function createWakeDeliveryStore(scope) {
37459
37755
  supersede: (id, now, reason) => supersedeStmt.run({ id, now, reason: reason.slice(0, 500) }).changes === 1
37460
37756
  };
37461
37757
  }
37462
- var OUTBOX_CAP, WAKE_DELIVERY_TABLES;
37758
+ var WAKE_QUIET_WINDOW_MS, WAKE_QUIET_EXEMPT_HINT_PREFIXES, OUTBOX_CAP, WAKE_DELIVERY_TABLES;
37463
37759
  var init_wake_store = __esm({
37464
37760
  "packages/server/src/wake-store.ts"() {
37465
37761
  "use strict";
37762
+ WAKE_QUIET_WINDOW_MS = 5 * 6e4;
37763
+ WAKE_QUIET_EXEMPT_HINT_PREFIXES = ["answers:", "limit:"];
37466
37764
  OUTBOX_CAP = 2e3;
37467
37765
  WAKE_DELIVERY_TABLES = ["wake_delivery"];
37468
37766
  }
@@ -37745,9 +38043,9 @@ function createGithubReviewFetcher(deps = {}) {
37745
38043
  for (const entry of batch.values()) for (const resolve12 of entry.resolve) resolve12({ status: "deferred" });
37746
38044
  return;
37747
38045
  }
37748
- const entries = [...batch.values()];
37749
- for (let offset = 0; offset < entries.length; offset += MAX_REFS_PER_REQUEST) {
37750
- const chunk = entries.slice(offset, offset + MAX_REFS_PER_REQUEST);
38046
+ const entries2 = [...batch.values()];
38047
+ for (let offset = 0; offset < entries2.length; offset += MAX_REFS_PER_REQUEST) {
38048
+ const chunk = entries2.slice(offset, offset + MAX_REFS_PER_REQUEST);
37751
38049
  if (offset > 0 && now() < rateLimitBlockedUntilMs) {
37752
38050
  for (const entry of chunk) for (const resolve12 of entry.resolve) resolve12({ status: "deferred" });
37753
38051
  continue;
@@ -37960,11 +38258,11 @@ function failedCheckNames(rollup, runs = [], cap = 8) {
37960
38258
  return { names: names.slice(0, cap), omitted: Math.max(0, names.length - cap) };
37961
38259
  }
37962
38260
  function githubWatchStatus(pr, polledAt) {
37963
- const entries = Array.isArray(pr.rollup) ? pr.rollup.filter((c) => c && typeof c === "object") : [];
38261
+ const entries2 = Array.isArray(pr.rollup) ? pr.rollup.filter((c) => c && typeof c === "object") : [];
37964
38262
  let running = 0;
37965
38263
  let passed = 0;
37966
38264
  let failed = 0;
37967
- for (const c of entries) {
38265
+ for (const c of entries2) {
37968
38266
  const status = typeof c.status === "string" ? c.status : void 0;
37969
38267
  const state2 = typeof c.state === "string" ? c.state : void 0;
37970
38268
  const conclusion = typeof c.conclusion === "string" ? c.conclusion : void 0;
@@ -37974,19 +38272,19 @@ function githubWatchStatus(pr, polledAt) {
37974
38272
  else passed++;
37975
38273
  }
37976
38274
  const state = pr.state === "MERGED" ? "merged" : pr.state === "CLOSED" ? "closed" : "open";
37977
- const checks = entries.length === 0 ? "none" : failed > 0 ? "failing" : running > 0 ? "running" : "passing";
38275
+ const checks = entries2.length === 0 ? "none" : failed > 0 ? "failing" : running > 0 ? "running" : "passing";
37978
38276
  const merge = pr.mergeable === "CONFLICTING" ? "conflicting" : pr.mergeable !== "MERGEABLE" ? "unknown" : checks === "failing" || pr.reviewDecision === "CHANGES_REQUESTED" || pr.reviewDecision === "REVIEW_REQUIRED" ? "blocked" : "mergeable";
37979
38277
  return {
37980
38278
  checks,
37981
38279
  running,
37982
38280
  passed,
37983
38281
  failed,
37984
- failing: failedCheckNames(entries, pr.workflowRuns ?? []).names,
38282
+ failing: failedCheckNames(entries2, pr.workflowRuns ?? []).names,
37985
38283
  merge,
37986
38284
  state,
37987
38285
  polledAt,
37988
38286
  head: pr.head,
37989
- failureSig: failureSignature(entries)
38287
+ failureSig: failureSignature(entries2)
37990
38288
  };
37991
38289
  }
37992
38290
  function wakeDeliveryId(slug, sessionId, fenceId) {
@@ -38215,7 +38513,7 @@ function createScheduler(deps) {
38215
38513
  const confirmGraceMs = Math.max(1, deps.confirmGraceMs ?? 6e4);
38216
38514
  const deliveryBatchSize = Math.max(0, deps.deliveryBatchSize ?? 50);
38217
38515
  const deliveryOwner = randomUUID14();
38218
- const outbox = createWakeDeliveryStore(deps.storage.scope);
38516
+ const outbox = createWakeDeliveryStore(deps.storage.scope, { quietWindowMs: deps.wakeQuietWindowMs ?? WAKE_QUIET_WINDOW_MS });
38219
38517
  const reviewFailures = /* @__PURE__ */ new Map();
38220
38518
  let timer = null;
38221
38519
  let activeTick = null;
@@ -38311,9 +38609,14 @@ function createScheduler(deps) {
38311
38609
  }
38312
38610
  return "superseded";
38313
38611
  }
38314
- function isDeliverableNow(item, context) {
38612
+ function isDeliverableNow(item, context, nowMs) {
38315
38613
  if (context === "current-idle") return true;
38316
- return context === "current-busy" && (isHeartbeatFenceId(item.fenceId) || isTimerFenceId(item.fenceId) || isCompactFenceId(item.fenceId));
38614
+ if (context !== "current-busy") return false;
38615
+ if (isHeartbeatFenceId(item.fenceId) || isTimerFenceId(item.fenceId) || isCompactFenceId(item.fenceId)) return true;
38616
+ return nowMs - item.createdAt >= MID_TURN_HOLD_MAX_MS;
38617
+ }
38618
+ function heldPastBound(item, context, nowMs) {
38619
+ return context === "current-busy" && item.sentAt === null && item.lastError === WAKE_HOLD_DEFERRAL && nowMs - item.createdAt >= MID_TURN_HOLD_MAX_MS;
38317
38620
  }
38318
38621
  function activityLabel(a) {
38319
38622
  if (a.kind === "comment") return "comment";
@@ -38517,7 +38820,7 @@ function createScheduler(deps) {
38517
38820
  if (tele.authFault) continue;
38518
38821
  if (tele.apiFault) continue;
38519
38822
  const questionRows = deps.storage.listThreadQuestions(row.slug);
38520
- if (tele.lastFence || tele.pendingQuestion || registeredDoneFence(deps.storage.getThreadDone(row.slug), tele.lastUserAt) !== void 0 || questionRows.some((q) => q.state === "open") || answersInFlight(questionRows, tele.lastUserAt) !== void 0 || deps.storage.listThreadWatches(row.slug, { armedOnly: true }).length > 0) {
38823
+ if (tele.lastFence || tele.pendingQuestion || registeredDoneFence(deps.storage.getThreadDone(row.slug), tele.lastUserAt) !== void 0 || questionRows.some((q) => q.state === "open") || answersInFlight(questionRows, tele.lastUserAt, row.recurring_on_rest === 1 && Boolean(row.recurring_prompt?.trim())) !== void 0 || deps.storage.listThreadWatches(row.slug, { armedOnly: true }).length > 0) {
38521
38824
  if ((row.signoff_nudges ?? 0) > 0) deps.storage.resetSignoffNudges(row.slug);
38522
38825
  continue;
38523
38826
  }
@@ -38724,12 +39027,27 @@ ${clock}`;
38724
39027
  }
38725
39028
  }
38726
39029
  const prWatchPolledAt = /* @__PURE__ */ new Map();
39030
+ function readPrWatchHeld(raw2) {
39031
+ if (!raw2 || typeof raw2 !== "object") return void 0;
39032
+ const h = raw2;
39033
+ if (!Array.isArray(h.items)) return void 0;
39034
+ const items = h.items.flatMap((i) => GithubWakeItem.safeParse(i).success ? [i] : []);
39035
+ const c = h.checks && typeof h.checks === "object" ? h.checks : void 0;
39036
+ const checks = c && (c.verdict === "passing" || c.verdict === "failing") ? {
39037
+ verdict: c.verdict,
39038
+ passed: Number(c.passed) || 0,
39039
+ failed: Number(c.failed) || 0,
39040
+ failing: Array.isArray(c.failing) ? c.failing.filter((x) => typeof x === "string") : []
39041
+ } : void 0;
39042
+ return { items, omitted: Number(h.omitted) || 0, ...checks ? { checks } : {} };
39043
+ }
38727
39044
  function readPrWatchCursor(raw2) {
38728
39045
  try {
38729
39046
  const parsed = raw2 ? JSON.parse(raw2) : null;
38730
39047
  if (!parsed || typeof parsed !== "object") return { seen: [] };
38731
39048
  const seen = Array.isArray(parsed.seen) ? parsed.seen.filter((x) => typeof x === "string") : [];
38732
- return { seen, checks: typeof parsed.checks === "string" ? parsed.checks : void 0, report: Number(parsed.report) || 0 };
39049
+ const held = readPrWatchHeld(parsed.held);
39050
+ return { seen, checks: typeof parsed.checks === "string" ? parsed.checks : void 0, report: Number(parsed.report) || 0, ...held ? { held } : {} };
38733
39051
  } catch {
38734
39052
  return { seen: [] };
38735
39053
  }
@@ -38812,7 +39130,7 @@ ${clock}`;
38812
39130
  }), `pr-watch ${key} ${st.state}`, nowMs);
38813
39131
  continue;
38814
39132
  }
38815
- if (waiting) continue;
39133
+ if (waiting && !cursor.held) continue;
38816
39134
  const terminal = st && (st.checks === "passing" || st.checks === "failing") ? st.checks : void 0;
38817
39135
  const stamp = terminal !== void 0 ? `${st?.head ?? "?"}:${terminal}:${st?.failureSig ?? ""}` : void 0;
38818
39136
  const checksChanged = stamp !== void 0 && cursor.checks !== stamp;
@@ -38829,37 +39147,42 @@ ${clock}`;
38829
39147
  return Number.isFinite(landed) && landed > w.created_at;
38830
39148
  });
38831
39149
  const named = fresh.slice(0, REVIEW_STEER_CAP);
38832
- const reviewSteer = fresh.length > 0 ? formatGithubWakeSteer({
38833
- ref: key,
38834
- omitted: fresh.length - named.length,
38835
- items: [...named].reverse().map((a) => ({
38836
- label: activityLabel(a),
38837
- actor: a.actor,
38838
- bot: isBotGithubActor(a),
38839
- ...a.at ? { at: a.at } : {},
38840
- ...a.url ? { url: a.url } : {}
38841
- }))
38842
- }) : void 0;
39150
+ const freshItems = [...named].reverse().map((a) => ({
39151
+ label: activityLabel(a),
39152
+ actor: a.actor,
39153
+ bot: isBotGithubActor(a),
39154
+ ...a.at ? { at: a.at } : {},
39155
+ ...a.url ? { url: a.url } : {}
39156
+ }));
38843
39157
  const nextCursor = {
38844
39158
  seen: acts ? [.../* @__PURE__ */ new Set([...cursor.seen, ...acts.map((a) => a.id)])].slice(-REVIEW_SEEN_CAP) : cursor.seen,
38845
39159
  // The STAMP, so the next poll compares against the commit as well as the colour.
38846
39160
  checks: stamp ?? cursor.checks,
38847
39161
  report: cursor.report ?? 0
38848
39162
  };
38849
- if (!checksChanged && !reviewSteer) {
39163
+ if (!checksChanged && fresh.length === 0) {
38850
39164
  if (JSON.stringify(nextCursor) !== JSON.stringify(cursor)) {
38851
39165
  deps.storage.setPrWatchCursor(w.id, JSON.stringify(nextCursor));
38852
39166
  }
38853
39167
  continue;
38854
39168
  }
38855
39169
  deps.storage.setSnoozedUntil(row.slug, null);
39170
+ const held = waiting ? cursor.held : void 0;
39171
+ const items = [...held?.items ?? [], ...freshItems];
39172
+ const carried = {
39173
+ items: items.slice(-REVIEW_STEER_CAP),
39174
+ omitted: (held?.omitted ?? 0) + (fresh.length - named.length) + Math.max(0, items.length - REVIEW_STEER_CAP),
39175
+ ...checksChanged && terminal ? { checks: { verdict: terminal, passed: st.passed, failed: st.failed, failing: st.failing } } : held?.checks ? { checks: held.checks } : {}
39176
+ };
39177
+ const review = carried.items.length > 0 ? formatGithubWakeSteer({ ref: key, items: carried.items, omitted: carried.omitted }) : void 0;
38856
39178
  const report = nextReport(cursor);
39179
+ if (waiting) outbox.supersede(waiting.id, nowMs, `folded into report ${report}, which says this and everything since`);
38857
39180
  enqueuePrWatchWake(row, w.id, report, prWatchWakeMessage({
38858
39181
  target: key,
38859
- ...checksChanged && terminal ? { checks: { verdict: terminal, passed: st.passed, failed: st.failed, failing: st.failing } } : {},
38860
- ...reviewSteer ? { review: reviewSteer } : {}
38861
- }), `pr-watch ${key}${checksChanged ? ` CI ${terminal}` : ""}${reviewSteer ? " review" : ""}`, nowMs);
38862
- deps.storage.setPrWatchCursor(w.id, JSON.stringify({ ...nextCursor, report }));
39182
+ ...carried.checks ? { checks: carried.checks } : {},
39183
+ ...review ? { review } : {}
39184
+ }), `pr-watch ${key}${carried.checks ? ` CI ${carried.checks.verdict}` : ""}${review ? " review" : ""}`, nowMs);
39185
+ deps.storage.setPrWatchCursor(w.id, JSON.stringify({ ...nextCursor, report, held: carried }));
38863
39186
  }
38864
39187
  }
38865
39188
  function nextReport(cursor) {
@@ -39085,6 +39408,7 @@ ${clock}`;
39085
39408
  if (tele.authFault) continue;
39086
39409
  if (tele.apiFault) continue;
39087
39410
  if (restMessageIsSignedOff(deps.storage, row.slug, tele, registeredPrWatchesOf(deps.storage, row.slug), armedTimerIdsOf(deps.storage, row.slug))) continue;
39411
+ if (answersInFlight(deps.storage.listThreadQuestions(row.slug), tele.lastUserAt, true) !== void 0) continue;
39088
39412
  const reminder = outbox.get(wakeDeliveryId(row.slug, row.session_id, signoffFenceId(restedAt)));
39089
39413
  if (reminder && reminder.state !== "superseded" && reminder.state !== "exhausted") continue;
39090
39414
  const fenceId = stopHookFenceId(armed.armedAt, restedAt);
@@ -39155,11 +39479,8 @@ ${clock}`;
39155
39479
  const context = deliveryContext(item);
39156
39480
  if (context === "confirmed") {
39157
39481
  outbox.confirm(item.id, nowMs);
39158
- settleSnooze(item);
39159
- settleSchedulePrompt(item);
39160
- settleRestPrompt(item);
39161
- settleCompactPrompt(item);
39162
- settleTimer(item);
39482
+ settleDelivered(item);
39483
+ confirmFrame(item, nowMs);
39163
39484
  continue;
39164
39485
  }
39165
39486
  if (context === "superseded") {
@@ -39200,7 +39521,7 @@ ${clock}`;
39200
39521
  }
39201
39522
  continue;
39202
39523
  }
39203
- if (context !== "current-idle") continue;
39524
+ if (context !== "current-idle" && !heldPastBound(item, context, nowMs)) continue;
39204
39525
  const recovered = outbox.recoverExpired(
39205
39526
  item.id,
39206
39527
  nowMs,
@@ -39215,6 +39536,63 @@ ${clock}`;
39215
39536
  }
39216
39537
  }
39217
39538
  }
39539
+ function settleDelivered(item) {
39540
+ settleSnooze(item);
39541
+ settleSchedulePrompt(item);
39542
+ settleRestPrompt(item);
39543
+ settleCompactPrompt(item);
39544
+ settleTimer(item);
39545
+ }
39546
+ function adoptCompanions(carrier, claimedAt) {
39547
+ if (isQuestionAnswerFenceId(carrier.fenceId)) return [];
39548
+ const adopted = [];
39549
+ for (const sibling of outbox.pendingFor(carrier.slug, carrier.sessionId)) {
39550
+ if (sibling.id === carrier.id || isQuestionAnswerFenceId(sibling.fenceId)) continue;
39551
+ const context = deliveryContext(sibling);
39552
+ if (context === "confirmed") {
39553
+ outbox.confirm(sibling.id, now());
39554
+ settleDelivered(sibling);
39555
+ continue;
39556
+ }
39557
+ if (context === "superseded") {
39558
+ outbox.supersede(sibling.id, now(), "the exact awaiting fence or session was superseded before delivery");
39559
+ settleSnooze(sibling);
39560
+ continue;
39561
+ }
39562
+ if (!isDeliverableNow(sibling, context, claimedAt)) continue;
39563
+ const leased = outbox.adopt(sibling.id, deliveryOwner, claimedAt, claimedAt + deliveryLeaseMs, maxDeliveryAttempts);
39564
+ if (leased) adopted.push(leased);
39565
+ }
39566
+ return adopted;
39567
+ }
39568
+ function mergedWakeMessage(frame, spokeAt) {
39569
+ const sections = [];
39570
+ for (const part of frame) {
39571
+ const body2 = stripWakeTimeHeader(part.message).trim();
39572
+ const same = sections.find((s) => s.reason === part.reason && s.body === body2);
39573
+ if (same) same.count++;
39574
+ else sections.push({ reason: part.reason, body: body2, count: 1 });
39575
+ }
39576
+ const lead = `Frizz held ${frame.length} wakes for this thread and is delivering them together, oldest first. Each is under its own heading; read all of them before acting on any.`;
39577
+ const body = sections.map((s, i) => `### ${i + 1}. ${s.reason}${s.count > 1 ? ` \u2014 ${s.count} identical events` : ""}
39578
+
39579
+ ${s.body}`);
39580
+ return `${lead}
39581
+
39582
+ ${body.join("\n\n")}
39583
+
39584
+ ${wakeTimeHeader(now(), spokeAt)}`;
39585
+ }
39586
+ function confirmFrame(carrier, nowMs) {
39587
+ if (carrier.sentAt === null) return;
39588
+ for (const d of outbox.listOpen()) {
39589
+ if (d.id === carrier.id || d.slug !== carrier.slug || d.sessionId !== carrier.sessionId) continue;
39590
+ if (d.state !== "leased" || d.sentAt !== carrier.sentAt) continue;
39591
+ if (!outbox.confirm(d.id, nowMs)) continue;
39592
+ settleDelivered(d);
39593
+ log3(`waker: delivered ${d.slug} \u2014 ${d.reason} (in the same frame as: ${carrier.reason})`);
39594
+ }
39595
+ }
39218
39596
  async function deliverDue() {
39219
39597
  for (let delivered = 0; delivered < deliveryBatchSize; delivered++) {
39220
39598
  const claimedAt = now();
@@ -39224,11 +39602,7 @@ ${clock}`;
39224
39602
  const context = deliveryContext(item);
39225
39603
  if (context === "confirmed") {
39226
39604
  outbox.confirm(item.id, now());
39227
- settleSnooze(item);
39228
- settleSchedulePrompt(item);
39229
- settleRestPrompt(item);
39230
- settleCompactPrompt(item);
39231
- settleTimer(item);
39605
+ settleDelivered(item);
39232
39606
  continue;
39233
39607
  }
39234
39608
  if (context === "superseded") {
@@ -39236,44 +39610,55 @@ ${clock}`;
39236
39610
  settleSnooze(item);
39237
39611
  continue;
39238
39612
  }
39239
- if (!isDeliverableNow(item, context)) {
39613
+ if (!isDeliverableNow(item, context, claimedAt)) {
39240
39614
  const deferredAt = now();
39241
39615
  outbox.deferFailure(
39242
39616
  item.id,
39243
39617
  deliveryOwner,
39244
39618
  deferredAt,
39245
39619
  deferredAt + Math.max(deliveryLeaseMs, retryDelay(item.attempts)),
39246
- "delivery deferred until exact awaiting telemetry is idle and available"
39620
+ WAKE_HOLD_DEFERRAL
39247
39621
  );
39248
39622
  continue;
39249
39623
  }
39624
+ const companions = adoptCompanions(item, claimedAt);
39625
+ const frame = [...companions, item].sort((a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id));
39626
+ const message = companions.length === 0 ? item.message : mergedWakeMessage(frame, deps.tailer.get(item.slug)?.lastAssistantAt);
39627
+ if (companions.length > 0) {
39628
+ log3(`waker: merged ${frame.length} wakes for ${item.slug} into one delivery \u2014 ${frame.map((d) => d.reason).join("; ")}`);
39629
+ }
39250
39630
  try {
39251
- await deps.resume(item.slug, item.message, item.id);
39631
+ await deps.resume(item.slug, message, item.id);
39252
39632
  } catch (error) {
39253
- const message = error instanceof Error ? error.message : String(error);
39633
+ const message2 = error instanceof Error ? error.message : String(error);
39254
39634
  const failedAt = now();
39255
39635
  if (error?.terminalDelivery === true) {
39256
- outbox.supersede(item.id, failedAt, message);
39257
- settleSnooze(item);
39258
- settleTimer(item);
39259
- log3(`waker: delivery ABANDONED for ${item.slug} (terminal, no retry): ${message}`);
39636
+ for (const d of frame) {
39637
+ outbox.supersede(d.id, failedAt, message2);
39638
+ settleSnooze(d);
39639
+ settleTimer(d);
39640
+ }
39641
+ log3(`waker: delivery ABANDONED for ${item.slug} (terminal, no retry): ${message2}`);
39260
39642
  continue;
39261
39643
  }
39262
- outbox.deferFailure(
39263
- item.id,
39264
- deliveryOwner,
39265
- failedAt,
39266
- failedAt + Math.max(deliveryLeaseMs, retryDelay(item.attempts)),
39267
- message
39268
- );
39269
- log3(`waker: delivery FAILED for ${item.slug} (attempt ${item.attempts} of ${maxDeliveryAttempts}): ${message}`);
39644
+ for (const d of frame) {
39645
+ outbox.deferFailure(
39646
+ d.id,
39647
+ deliveryOwner,
39648
+ failedAt,
39649
+ failedAt + Math.max(deliveryLeaseMs, retryDelay(d.attempts)),
39650
+ message2
39651
+ );
39652
+ }
39653
+ log3(`waker: delivery FAILED for ${item.slug} (attempt ${item.attempts} of ${maxDeliveryAttempts}): ${message2}`);
39270
39654
  continue;
39271
39655
  }
39272
39656
  const runtime = deps.wakeRuntimeState?.(item.slug, item.sessionId) ?? "unknown";
39273
39657
  if (runtime !== "unknown") {
39274
39658
  const sentAt = now();
39275
39659
  if (outbox.markSent(item.id, deliveryOwner, sentAt, sentAt + confirmGraceMs)) {
39276
- settleSignoffNudge(item);
39660
+ for (const d of companions) outbox.markSent(d.id, deliveryOwner, sentAt, sentAt + confirmGraceMs);
39661
+ for (const d of frame) settleSignoffNudge(d);
39277
39662
  log3(`waker: sent ${item.slug} \u2014 ${item.reason}; confirming within ${Math.round(confirmGraceMs / 1e3)}s`);
39278
39663
  checkpoint("after-delivery", item);
39279
39664
  continue;
@@ -39282,16 +39667,15 @@ ${clock}`;
39282
39667
  const retried = item.attempts > 1 ? ` (on attempt ${item.attempts})` : "";
39283
39668
  log3(`waker: delivered ${item.slug} \u2014 ${item.reason}${retried}`);
39284
39669
  checkpoint("after-delivery", item);
39285
- if (!outbox.acknowledge(item.id, deliveryOwner, now())) {
39670
+ const acked = frame.filter((d) => outbox.acknowledge(d.id, deliveryOwner, now()));
39671
+ if (!acked.includes(item)) {
39286
39672
  log3(`waker: delivery acknowledgement lost ownership for ${item.slug}; preserving the authoritative terminal state`);
39287
- continue;
39288
39673
  }
39289
- settleSnooze(item);
39290
- settleSchedulePrompt(item);
39291
- settleRestPrompt(item);
39292
- settleCompactPrompt(item);
39293
- settleTimer(item);
39294
- settleSignoffNudge(item);
39674
+ for (const d of acked) {
39675
+ settleDelivered(d);
39676
+ settleSignoffNudge(d);
39677
+ }
39678
+ if (!acked.includes(item)) continue;
39295
39679
  const acknowledged = outbox.get(item.id);
39296
39680
  if (acknowledged) checkpoint("after-ack", acknowledged);
39297
39681
  }
@@ -39412,7 +39796,7 @@ ${clock}`;
39412
39796
  }
39413
39797
  };
39414
39798
  }
39415
- var execFileAsync4, LIMIT_FENCE_PREFIX, LIMIT_HINT_PREFIX, LIMIT_RESUME_GRACE_MS, LIMIT_RESUME_HEADROOM_PERCENT, LIMIT_HEADROOM_MIN_FAULT_AGE_MS, REPORT_FENCE_PREFIX, REPORT_HINT_PREFIX, SNOOZE_FENCE_PREFIX, SNOOZE_HINT_PREFIX, HEARTBEAT_FENCE_PREFIX, HEARTBEAT_HINT_PREFIX, STOP_HOOK_FENCE_PREFIX, STOP_HOOK_HINT_KEY, COMPACT_FENCE_PREFIX, COMPACT_HINT_KEY, TIMER_FENCE_PREFIX, TIMER_HINT_PREFIX, SHELL_FENCE_PREFIX, PR_WATCH_FENCE_PREFIX, PR_WATCH_EXPIRED_FENCE_PREFIX, OWN_WATCH_EXPIRED_FENCE_PREFIX, QUESTION_ANSWER_FENCE_PREFIX, PR_WATCH_POLL_MS, SIGNOFF_FENCE_PREFIX, SIGNOFF_HINT_KEY, SIGNOFF_NUDGE_MAX, PARK_BUMP_MAX, SIGNOFF_NUDGE_SETTING, PARK_FENCE_PREFIX, GITHUB_STATUS_CAP, REVIEW_SEEN_CAP, REVIEW_STEER_CAP, AWAITING_KEY_OF;
39799
+ var execFileAsync4, LIMIT_FENCE_PREFIX, LIMIT_HINT_PREFIX, LIMIT_RESUME_GRACE_MS, LIMIT_RESUME_HEADROOM_PERCENT, LIMIT_HEADROOM_MIN_FAULT_AGE_MS, REPORT_FENCE_PREFIX, REPORT_HINT_PREFIX, SNOOZE_FENCE_PREFIX, SNOOZE_HINT_PREFIX, HEARTBEAT_FENCE_PREFIX, HEARTBEAT_HINT_PREFIX, STOP_HOOK_FENCE_PREFIX, STOP_HOOK_HINT_KEY, COMPACT_FENCE_PREFIX, COMPACT_HINT_KEY, TIMER_FENCE_PREFIX, TIMER_HINT_PREFIX, SHELL_FENCE_PREFIX, PR_WATCH_FENCE_PREFIX, PR_WATCH_EXPIRED_FENCE_PREFIX, OWN_WATCH_EXPIRED_FENCE_PREFIX, QUESTION_ANSWER_FENCE_PREFIX, PR_WATCH_POLL_MS, SIGNOFF_FENCE_PREFIX, SIGNOFF_HINT_KEY, SIGNOFF_NUDGE_MAX, PARK_BUMP_MAX, SIGNOFF_NUDGE_SETTING, PARK_FENCE_PREFIX, GITHUB_STATUS_CAP, REVIEW_SEEN_CAP, REVIEW_STEER_CAP, AWAITING_KEY_OF, MID_TURN_HOLD_MAX_MS, WAKE_HOLD_DEFERRAL;
39416
39800
  var init_scheduler = __esm({
39417
39801
  "packages/server/src/scheduler.ts"() {
39418
39802
  "use strict";
@@ -39467,6 +39851,8 @@ var init_scheduler = __esm({
39467
39851
  timer: "timers",
39468
39852
  pr: "prs"
39469
39853
  };
39854
+ MID_TURN_HOLD_MAX_MS = 10 * 6e4;
39855
+ WAKE_HOLD_DEFERRAL = "delivery deferred until exact awaiting telemetry is idle and available";
39470
39856
  }
39471
39857
  });
39472
39858
 
@@ -39546,7 +39932,7 @@ var init_resume = __esm({
39546
39932
  });
39547
39933
 
39548
39934
  // packages/server/src/backend/claude.ts
39549
- import { join as join29 } from "node:path";
39935
+ import { join as join30 } from "node:path";
39550
39936
  function toolResultText2(content) {
39551
39937
  if (typeof content === "string") return content;
39552
39938
  if (!Array.isArray(content)) return "";
@@ -39615,7 +40001,8 @@ function createClaudeBackend(opts) {
39615
40001
  pluginDir: workerPluginDir(),
39616
40002
  workerPrompt: o.workerContract,
39617
40003
  extraSystemPrompt: o.extraSystemPrompt,
39618
- frizzMcp: o.frizzMcp
40004
+ frizzMcp: o.frizzMcp,
40005
+ projectMcpServers: (opts.resolveProjectMcpServers ?? projectMcpServers)(o.cwd)
39619
40006
  });
39620
40007
  return { argv, env: claudeWorkerEnvironment(), prewrite: [] };
39621
40008
  },
@@ -39630,12 +40017,13 @@ function createClaudeBackend(opts) {
39630
40017
  pluginDir: workerPluginDir(),
39631
40018
  workerPrompt: o.workerContract,
39632
40019
  extraSystemPrompt: o.extraSystemPrompt,
39633
- frizzMcp: o.frizzMcp
40020
+ frizzMcp: o.frizzMcp,
40021
+ projectMcpServers: (opts.resolveProjectMcpServers ?? projectMcpServers)(o.cwd)
39634
40022
  });
39635
40023
  return { argv, env: claudeWorkerEnvironment(), prewrite: [] };
39636
40024
  },
39637
40025
  transcriptPath(sessionId) {
39638
- return join29(opts.logDir, `${sessionId}.jsonl`);
40026
+ return join30(opts.logDir, `${sessionId}.jsonl`);
39639
40027
  },
39640
40028
  parseLine(line) {
39641
40029
  return parseClaudeLine(line);
@@ -39653,6 +40041,7 @@ function createClaudeBackend(opts) {
39653
40041
  var init_claude = __esm({
39654
40042
  "packages/server/src/backend/claude.ts"() {
39655
40043
  "use strict";
40044
+ init_project_mcp_servers();
39656
40045
  init_dispatch();
39657
40046
  init_tailer();
39658
40047
  }
@@ -39805,12 +40194,12 @@ var init_login_utility = __esm({
39805
40194
 
39806
40195
  // packages/server/src/backend/codex-app-server-diagnostics.ts
39807
40196
  import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync14, renameSync as renameSync8, statSync as statSync13 } from "node:fs";
39808
- import { join as join30 } from "node:path";
40197
+ import { join as join31 } from "node:path";
39809
40198
  function codexDiagnosticLogPath(stateDir, projectId) {
39810
- return join30(stateDir, "codex-app-server", `${projectId}.diagnostics.log`);
40199
+ return join31(stateDir, "codex-app-server", `${projectId}.diagnostics.log`);
39811
40200
  }
39812
40201
  function createCodexDiagnosticSink(stateDir, projectId, now = () => /* @__PURE__ */ new Date()) {
39813
- const dir = join30(stateDir, "codex-app-server");
40202
+ const dir = join31(stateDir, "codex-app-server");
39814
40203
  const path = codexDiagnosticLogPath(stateDir, projectId);
39815
40204
  let ensured = false;
39816
40205
  return (event) => {
@@ -39842,7 +40231,7 @@ var init_codex_app_server_diagnostics = __esm({
39842
40231
  import { execFile as execFile7 } from "node:child_process";
39843
40232
  import { readdirSync as readdirSync8, unlinkSync as unlinkSync4 } from "node:fs";
39844
40233
  import { connect as connectSocket } from "node:net";
39845
- import { join as join31 } from "node:path";
40234
+ import { join as join32 } from "node:path";
39846
40235
  function parseLsofSocketNames(stdout) {
39847
40236
  const referenced = /* @__PURE__ */ new Set();
39848
40237
  for (const line of stdout.split("\n")) {
@@ -39859,7 +40248,7 @@ function sweepStaleSockets(options, deps = {}) {
39859
40248
  const keep = new Set(options.keep ?? []);
39860
40249
  let candidates;
39861
40250
  try {
39862
- candidates = (deps.readdir ?? readdirSync8)(options.dir).filter((name) => name.startsWith(options.prefix) && name.endsWith(".sock")).map((name) => join31(options.dir, name)).filter((path) => !keep.has(path));
40251
+ candidates = (deps.readdir ?? readdirSync8)(options.dir).filter((name) => name.startsWith(options.prefix) && name.endsWith(".sock")).map((name) => join32(options.dir, name)).filter((path) => !keep.has(path));
39863
40252
  } catch {
39864
40253
  return;
39865
40254
  }
@@ -39909,7 +40298,7 @@ var init_stale_socket_sweep = __esm({
39909
40298
 
39910
40299
  // packages/server/src/orphan-reaper.ts
39911
40300
  import { execFileSync as execFileSync5 } from "node:child_process";
39912
- import { readFileSync as readFileSync18 } from "node:fs";
40301
+ import { readFileSync as readFileSync19 } from "node:fs";
39913
40302
  import { dirname as dirname12 } from "node:path";
39914
40303
  function firstTokenBasename(command) {
39915
40304
  const first = command.trimStart().split(/\s+/, 1)[0] ?? "";
@@ -40176,7 +40565,7 @@ var init_orphan_reaper = __esm({
40176
40565
  defaultExec = (file, args) => execFileSync5(file, args, { encoding: "utf8", maxBuffer: 16 * 1024 * 1024, timeout: 1e4 });
40177
40566
  defaultReadEnv = (pid) => {
40178
40567
  try {
40179
- return readFileSync18(`/proc/${pid}/environ`, "utf8");
40568
+ return readFileSync19(`/proc/${pid}/environ`, "utf8");
40180
40569
  } catch {
40181
40570
  return null;
40182
40571
  }
@@ -40323,9 +40712,9 @@ var init_thread_hibernation = __esm({
40323
40712
  });
40324
40713
 
40325
40714
  // packages/server/src/context.ts
40326
- import { join as join32 } from "node:path";
40715
+ import { join as join33 } from "node:path";
40327
40716
  import { randomUUID as randomUUID15 } from "node:crypto";
40328
- import { homedir as homedir13 } from "node:os";
40717
+ import { homedir as homedir14 } from "node:os";
40329
40718
  function reconcileSessions(storage) {
40330
40719
  for (const row of storage.allSessions()) {
40331
40720
  if (isHeadlessRow(row)) continue;
@@ -40471,9 +40860,9 @@ async function createContext(opts = {}) {
40471
40860
  }
40472
40861
  }
40473
40862
  function createContextUnchecked(opts, resources) {
40474
- const home = opts.home ?? homedir13();
40863
+ const home = opts.home ?? homedir14();
40475
40864
  const project = opts.project ?? resolveProject();
40476
- const storage = createStorage(opts.database ?? join32(project.stateDir, "ui.db"), project.id);
40865
+ const storage = createStorage(opts.database ?? join33(project.stateDir, "ui.db"), project.id);
40477
40866
  resources.storage = storage;
40478
40867
  const bus = new Bus();
40479
40868
  const transcriptChange = new Emitter();
@@ -40622,7 +41011,8 @@ function createContextUnchecked(opts, resources) {
40622
41011
  // The frizz worker environment — the SDK equivalent of the CLI argv's --plugin-dir / --mcp-config.
40623
41012
  // Computed ONCE here (constant per project) and applied on every broker fork so a broker worker
40624
41013
  // gets the frizz sub-agent profiles, the frizz MCP server (the only one frizz mounts), and the
40625
- // cc-worker hooks. A project's own `.mcp.json` servers load on top of these, not through them.
41014
+ // cc-worker hooks. The project's own `.mcp.json` servers are merged in by the broker daemon at each
41015
+ // fork (project-mcp-servers.ts): under `--strict-mcp-config` nothing mounts that frizz did not hand over.
40626
41016
  workerEnv: {
40627
41017
  pluginDir: workerPluginDir(),
40628
41018
  ...claudeMcpConfig(resolveFrizzMcp(frizzMcpTarget)),
@@ -40765,6 +41155,8 @@ ${wakeDeliveryToken(deliveryId)}`;
40765
41155
  Date.now(),
40766
41156
  (tailer.get(slug)?.subAgents ?? []).some((agent) => agent.state === "running")
40767
41157
  )
41158
+ }).then(() => {
41159
+ if (row.exited === 1) storage.setExitedIfCurrent(slug, row.session_id, row.runtime_generation ?? 0, false);
40768
41160
  });
40769
41161
  }
40770
41162
  resumeThread(
@@ -43523,7 +43915,7 @@ var init_server = __esm({
43523
43915
  });
43524
43916
 
43525
43917
  // packages/server/src/repair.ts
43526
- import { existsSync as existsSync11, readFileSync as readFileSync19, writeFileSync as writeFileSync11 } from "node:fs";
43918
+ import { existsSync as existsSync11, readFileSync as readFileSync20, writeFileSync as writeFileSync11 } from "node:fs";
43527
43919
  import { basename as basename7, dirname as dirname13, resolve as resolve7 } from "node:path";
43528
43920
  function quoteValue(v) {
43529
43921
  if (/^[\w./#:+-]+$/.test(v)) return v;
@@ -43539,7 +43931,7 @@ function repairThreadFile(frizzDir, file) {
43539
43931
  if (dirname13(abs) !== root) throw new RepairError(`refusing to repair "${file}": not directly under .frizz/`);
43540
43932
  if (!abs.endsWith(".md")) throw new RepairError(`refusing to repair "${file}": not a .md thread file`);
43541
43933
  if (!existsSync11(abs)) throw new RepairError(`no thread file to repair: ${basename7(abs)}`);
43542
- const content = readFileSync19(abs, "utf8");
43934
+ const content = readFileSync20(abs, "utf8");
43543
43935
  const firstLine2 = content.replace(/^/, "").split(/\r?\n/).find((l) => l.trim() !== "");
43544
43936
  if (firstLine2?.trim() === "---") {
43545
43937
  throw new RepairError(`${basename7(abs)} already opens with a "---" block \u2014 repair only heals a MISSING frontmatter block`);
@@ -43617,7 +44009,7 @@ var init_edited_files = __esm({
43617
44009
  });
43618
44010
 
43619
44011
  // cc-worker/hooks/bash-background.mjs
43620
- import { readFileSync as readFileSync20 } from "node:fs";
44012
+ import { readFileSync as readFileSync21 } from "node:fs";
43621
44013
  import { basename as basename8 } from "node:path";
43622
44014
  import { pathToFileURL } from "node:url";
43623
44015
  function emit(obj) {
@@ -43766,7 +44158,7 @@ var init_bash_background = __esm({
43766
44158
  if (isDirectHookExecution(process.argv[1], import.meta.url)) {
43767
44159
  try {
43768
44160
  const env = process.argv.includes("--frizz-thread") ? { ...process.env, FRIZZ_THREAD: process.env.FRIZZ_THREAD || "codex-worker" } : process.env;
43769
- emit(evaluateBashBackgroundHook(JSON.parse(readFileSync20(0, "utf8")), env));
44161
+ emit(evaluateBashBackgroundHook(JSON.parse(readFileSync21(0, "utf8")), env));
43770
44162
  } catch {
43771
44163
  emit({});
43772
44164
  }
@@ -43775,11 +44167,11 @@ var init_bash_background = __esm({
43775
44167
  });
43776
44168
 
43777
44169
  // packages/server/src/transcript.ts
43778
- import { closeSync as closeSync13, existsSync as existsSync12, fstatSync as fstatSync3, mkdirSync as mkdirSync15, openSync as openSync14, readdirSync as readdirSync9, readFileSync as readFileSync21, readSync as readSync7, renameSync as renameSync9, statSync as statSync14, unlinkSync as unlinkSync5, writeFileSync as writeFileSync12 } from "node:fs";
44170
+ 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";
43779
44171
  import { createHash as createHash13 } from "node:crypto";
43780
44172
  import { StringDecoder as StringDecoder4 } from "node:string_decoder";
43781
- import { join as join33 } from "node:path";
43782
- import { homedir as homedir14 } from "node:os";
44173
+ import { join as join34 } from "node:path";
44174
+ import { homedir as homedir15 } from "node:os";
43783
44175
  function formatTokens(n) {
43784
44176
  if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
43785
44177
  if (n >= 1e3) return `${Math.round(n / 1e3)}k`;
@@ -44329,18 +44721,18 @@ function looksLikeImage(buf, ext) {
44329
44721
  }
44330
44722
  function pruneScreenshotCache() {
44331
44723
  try {
44332
- const entries = readdirSync9(SCREENSHOT_CACHE_DIR).filter((n) => !n.startsWith("."));
44333
- if (entries.length <= SCREENSHOT_CACHE_MAX) return;
44334
- const byMtime = entries.map((n) => {
44724
+ const entries2 = readdirSync9(SCREENSHOT_CACHE_DIR).filter((n) => !n.startsWith("."));
44725
+ if (entries2.length <= SCREENSHOT_CACHE_MAX) return;
44726
+ const byMtime = entries2.map((n) => {
44335
44727
  try {
44336
- return { n, m: statSync14(join33(SCREENSHOT_CACHE_DIR, n)).mtimeMs };
44728
+ return { n, m: statSync14(join34(SCREENSHOT_CACHE_DIR, n)).mtimeMs };
44337
44729
  } catch {
44338
44730
  return { n, m: 0 };
44339
44731
  }
44340
44732
  }).sort((a, b) => b.m - a.m);
44341
44733
  for (const { n } of byMtime.slice(SCREENSHOT_CACHE_MAX)) {
44342
44734
  try {
44343
- unlinkSync5(join33(SCREENSHOT_CACHE_DIR, n));
44735
+ unlinkSync5(join34(SCREENSHOT_CACHE_DIR, n));
44344
44736
  } catch {
44345
44737
  }
44346
44738
  }
@@ -44370,14 +44762,14 @@ function persistBase64Image(mediaType, data, idKey) {
44370
44762
  const ext = IMAGE_MEDIA_EXT[typeof mediaType === "string" ? mediaType.toLowerCase() : ""];
44371
44763
  if (!ext || !data) return void 0;
44372
44764
  const name = createHash13("sha256").update(idKey).digest("hex").slice(0, 32);
44373
- const path = join33(SCREENSHOT_CACHE_DIR, `${name}.${ext}`);
44765
+ const path = join34(SCREENSHOT_CACHE_DIR, `${name}.${ext}`);
44374
44766
  try {
44375
44767
  if (existsSync12(path)) return path;
44376
44768
  if (data.length > SCREENSHOT_MAX_BASE64) return void 0;
44377
44769
  const buf = Buffer.from(data, "base64");
44378
44770
  if (buf.length === 0 || !looksLikeImage(buf, ext)) return void 0;
44379
44771
  mkdirSync15(SCREENSHOT_CACHE_DIR, { recursive: true });
44380
- const tmp = join33(SCREENSHOT_CACHE_DIR, `.${name}.${process.pid}.${screenshotTmpSeq++}.tmp`);
44772
+ const tmp = join34(SCREENSHOT_CACHE_DIR, `.${name}.${process.pid}.${screenshotTmpSeq++}.tmp`);
44381
44773
  writeFileSync12(tmp, buf);
44382
44774
  renameSync9(tmp, path);
44383
44775
  pruneScreenshotCache();
@@ -44392,14 +44784,14 @@ function persistSentFile(srcPath, idKey) {
44392
44784
  const outExt = ext === "jpeg" ? "jpg" : ext;
44393
44785
  try {
44394
44786
  const name = createHash13("sha256").update(idKey).digest("hex").slice(0, 32);
44395
- const dest = join33(SCREENSHOT_CACHE_DIR, `${name}.${outExt}`);
44787
+ const dest = join34(SCREENSHOT_CACHE_DIR, `${name}.${outExt}`);
44396
44788
  if (existsSync12(dest)) return dest;
44397
44789
  const size = statSync14(srcPath).size;
44398
44790
  if (size === 0 || size > SENT_IMAGE_MAX_BYTES) return void 0;
44399
- const buf = readFileSync21(srcPath);
44791
+ const buf = readFileSync22(srcPath);
44400
44792
  if (!looksLikeImage(buf, outExt)) return void 0;
44401
44793
  mkdirSync15(SCREENSHOT_CACHE_DIR, { recursive: true });
44402
- const tmp = join33(SCREENSHOT_CACHE_DIR, `.${name}.${process.pid}.${screenshotTmpSeq++}.tmp`);
44794
+ const tmp = join34(SCREENSHOT_CACHE_DIR, `.${name}.${process.pid}.${screenshotTmpSeq++}.tmp`);
44403
44795
  writeFileSync12(tmp, buf);
44404
44796
  renameSync9(tmp, dest);
44405
44797
  pruneScreenshotCache();
@@ -44837,7 +45229,7 @@ function readTranscript(project, sessionId) {
44837
45229
  }
44838
45230
  function verifyIncrementalParse(path, identityPrefix, incremental) {
44839
45231
  try {
44840
- const fresh = parseTranscript(readFileSync21(path, "utf8"), identityPrefix);
45232
+ const fresh = parseTranscript(readFileSync22(path, "utf8"), identityPrefix);
44841
45233
  const a = JSON.stringify(incremental);
44842
45234
  const b = JSON.stringify(fresh);
44843
45235
  if (a !== b) {
@@ -44857,16 +45249,16 @@ function verifyIncrementalParse(path, identityPrefix, incremental) {
44857
45249
  }
44858
45250
  }
44859
45251
  function logDirOf(project) {
44860
- return join33(homedir14(), ".claude", "projects", project.cwdSlug);
45252
+ return join34(homedir15(), ".claude", "projects", project.cwdSlug);
44861
45253
  }
44862
45254
  function resolveTranscriptPath(project, sessionId) {
44863
- const path = join33(logDirOf(project), `${sessionId}.jsonl`);
45255
+ const path = join34(logDirOf(project), `${sessionId}.jsonl`);
44864
45256
  try {
44865
45257
  if (statSync14(path).size > 0) return path;
44866
45258
  } catch {
44867
45259
  }
44868
45260
  const stranded = discoverTranscriptDir(logDirOf(project), sessionId);
44869
- return stranded ? join33(stranded, `${sessionId}.jsonl`) : path;
45261
+ return stranded ? join34(stranded, `${sessionId}.jsonl`) : path;
44870
45262
  }
44871
45263
  function projectCodexTranscript(raw2, identityPrefix = "codex") {
44872
45264
  const out = [];
@@ -45395,10 +45787,10 @@ function wrappedInvocations(source) {
45395
45787
  let open = nameStart + name.length;
45396
45788
  while (/\s/.test(source[open] ?? "")) open++;
45397
45789
  if (source[open] !== "(") continue;
45398
- const close = matchingParen(source, open);
45399
- if (close === -1) break;
45400
- out.push({ name, args: source.slice(open + 1, close).trim() });
45401
- i = close;
45790
+ const close2 = matchingParen(source, open);
45791
+ if (close2 === -1) break;
45792
+ out.push({ name, args: source.slice(open + 1, close2).trim() });
45793
+ i = close2;
45402
45794
  }
45403
45795
  return out;
45404
45796
  }
@@ -46074,7 +46466,7 @@ function readEarlierThreadTranscriptPage(project, storage, slug, cursor, backend
46074
46466
  }
46075
46467
  function readCodexTranscriptFile(absPath, nativeId = absPath) {
46076
46468
  try {
46077
- return parseCodexTranscript(readFileSync21(absPath, "utf8"), `codex:${nativeId}`);
46469
+ return parseCodexTranscript(readFileSync22(absPath, "utf8"), `codex:${nativeId}`);
46078
46470
  } catch {
46079
46471
  return [];
46080
46472
  }
@@ -46172,7 +46564,7 @@ function projectTranscriptPageAgentLifecycles(page, lookup, peerLookup) {
46172
46564
  function readTranscriptFile(absPath) {
46173
46565
  try {
46174
46566
  const pathKey = createHash13("sha256").update(absPath).digest("base64url").slice(0, 16);
46175
- return parseTranscript(readFileSync21(absPath, "utf8"), `claude-file:${pathKey}`);
46567
+ return parseTranscript(readFileSync22(absPath, "utf8"), `claude-file:${pathKey}`);
46176
46568
  } catch {
46177
46569
  return [];
46178
46570
  }
@@ -46271,9 +46663,9 @@ var init_open_external = __esm({
46271
46663
 
46272
46664
  // packages/server/src/local-file.ts
46273
46665
  import { spawn as spawn6 } from "node:child_process";
46274
- import { readFileSync as readFileSync22, realpathSync as realpathSync8, statSync as statSync15 } from "node:fs";
46275
- import { homedir as homedir15 } from "node:os";
46276
- import { isAbsolute as isAbsolute4, join as join34, resolve as resolve8, sep as sep2 } from "node:path";
46666
+ import { readFileSync as readFileSync23, realpathSync as realpathSync8, statSync as statSync15 } from "node:fs";
46667
+ import { homedir as homedir16 } from "node:os";
46668
+ import { isAbsolute as isAbsolute4, join as join35, resolve as resolve8, sep as sep2 } from "node:path";
46277
46669
  function isUnder(real, root) {
46278
46670
  let rootReal;
46279
46671
  try {
@@ -46300,10 +46692,10 @@ function resolveLocalFile(rawPath, roots) {
46300
46692
  }
46301
46693
  return real;
46302
46694
  }
46303
- function resolveOpenableFile(raw2, projectDir, roots, home = homedir15()) {
46695
+ function resolveOpenableFile(raw2, projectDir, roots, home = homedir16()) {
46304
46696
  const trimmed = raw2.trim().replace(/:\d+(?::\d+)?$/, "");
46305
46697
  if (!trimmed) return null;
46306
- const abs = trimmed === "~" ? home : trimmed.startsWith("~/") ? join34(home, trimmed.slice(2)) : isAbsolute4(trimmed) ? trimmed : resolve8(projectDir, trimmed);
46698
+ const abs = trimmed === "~" ? home : trimmed.startsWith("~/") ? join35(home, trimmed.slice(2)) : isAbsolute4(trimmed) ? trimmed : resolve8(projectDir, trimmed);
46307
46699
  try {
46308
46700
  return resolveLocalFile(abs, roots);
46309
46701
  } catch {
@@ -46312,7 +46704,7 @@ function resolveOpenableFile(raw2, projectDir, roots, home = homedir15()) {
46312
46704
  }
46313
46705
  function readLocalTextFile(rawPath, roots) {
46314
46706
  const path = resolveLocalFile(rawPath, roots);
46315
- const bytes = readFileSync22(path);
46707
+ const bytes = readFileSync23(path);
46316
46708
  if (bytes.subarray(0, 8192).includes(0)) throw new Error("Local file is not a text file");
46317
46709
  if (bytes.length <= MARKDOWN_READ_LIMIT) return { path, text: bytes.toString("utf8"), truncated: false };
46318
46710
  const head = bytes.subarray(0, MARKDOWN_READ_LIMIT);
@@ -46323,12 +46715,19 @@ function readLocalMarkdown(rawPath, roots) {
46323
46715
  if (!MARKDOWN_FILE_EXT.test(rawPath.trim())) throw new Error("Local file is not a Markdown file");
46324
46716
  const path = resolveLocalFile(rawPath, roots);
46325
46717
  if (!MARKDOWN_FILE_EXT.test(path)) throw new Error("Local file is not a Markdown file");
46326
- const bytes = readFileSync22(path);
46718
+ const bytes = readFileSync23(path);
46327
46719
  if (bytes.length <= MARKDOWN_READ_LIMIT) return { path, markdown: bytes.toString("utf8"), truncated: false };
46328
46720
  const head = bytes.subarray(0, MARKDOWN_READ_LIMIT);
46329
46721
  const lastBreak = head.lastIndexOf(10);
46330
46722
  return { path, markdown: head.subarray(0, lastBreak > 0 ? lastBreak : head.length).toString("utf8"), truncated: true };
46331
46723
  }
46724
+ function resolveWatchableLocalFile(rawPath, roots) {
46725
+ const path = resolveLocalFile(rawPath, roots);
46726
+ if (MARKDOWN_FILE_EXT.test(rawPath.trim()) && !MARKDOWN_FILE_EXT.test(path)) {
46727
+ throw new Error("Local file is not a Markdown file");
46728
+ }
46729
+ return path;
46730
+ }
46332
46731
  function defaultSpawn(command, args, options) {
46333
46732
  return spawn6(command, [...args], options);
46334
46733
  }
@@ -47022,12 +47421,12 @@ var init_directory_picker = __esm({
47022
47421
  });
47023
47422
 
47024
47423
  // packages/server/src/router.ts
47025
- import { readFileSync as readFileSync23, statSync as statSync17 } from "node:fs";
47026
- import { join as join35, resolve as resolve9 } from "node:path";
47424
+ import { readFileSync as readFileSync24, statSync as statSync17 } from "node:fs";
47425
+ import { join as join36, resolve as resolve9 } from "node:path";
47027
47426
  import { randomUUID as randomUUID16 } from "node:crypto";
47028
47427
  import { basename as basename9, dirname as dirname14 } from "node:path";
47029
47428
  import { existsSync as existsSync13, mkdirSync as mkdirSync16, rmSync as rmSync10, writeFileSync as writeFileSync13 } from "node:fs";
47030
- import { homedir as homedir16 } from "node:os";
47429
+ import { homedir as homedir17 } from "node:os";
47031
47430
  function validateGithubDispatchProfile(input) {
47032
47431
  validateThreadProfile(input.backend, input.model, input.effort);
47033
47432
  }
@@ -47122,6 +47521,10 @@ function holdOps(ops) {
47122
47521
  state: op.state
47123
47522
  }));
47124
47523
  }
47524
+ function cutOffHold(telemetry) {
47525
+ if (telemetry?.turn !== "in-flight") return void 0;
47526
+ return { turnInFlight: true, cutOff: true, unobservable: false, subAgents: [], subAgentCount: 0, bgShells: [], bgShellCount: 0 };
47527
+ }
47125
47528
  async function completeRegisteredThread(storage, row, terminateLive, runtime = cachedLivenessTerminator, telemetry, codex, claudeBroker) {
47126
47529
  const binding = adoptionRuntimeBinding(storage, row);
47127
47530
  if (binding.kind === "conflict") {
@@ -47130,7 +47533,7 @@ async function completeRegisteredThread(storage, row, terminateLive, runtime = c
47130
47533
  const appServerCodex = isAppServerCodexRow(row);
47131
47534
  const brokerClaude = isBrokerClaudeRow(row);
47132
47535
  const live = brokerClaude ? claudeBroker?.isDaemonAlive(row.session_id) ?? false : appServerCodex ? appServerCodexTurnLive(codex, row) : false;
47133
- const hold = live && !terminateLive ? completionConfirmationHold(telemetry) : void 0;
47536
+ const hold = terminateLive ? void 0 : live ? completionConfirmationHold(telemetry) : cutOffHold(telemetry);
47134
47537
  if (hold) return { needsConfirmation: true, hold };
47135
47538
  if (live) {
47136
47539
  await stopThreadRuntime(storage, row, runtime, codex, claudeBroker);
@@ -47183,10 +47586,10 @@ function projectCard(entry, stale) {
47183
47586
  iconIsCustom: entry.iconSource === "custom" ? true : void 0
47184
47587
  };
47185
47588
  }
47186
- function addProjectAtPath(input) {
47589
+ function addProjectAtPath(input, home = homedir17()) {
47187
47590
  const typed = input.trim();
47188
47591
  if (!typed) throw new Error("Enter a folder path.");
47189
- const expanded = typed === "~" || typed.startsWith("~/") ? join35(homedir16(), typed.slice(1)) : typed;
47592
+ const expanded = typed === "~" || typed.startsWith("~/") ? join36(home, typed.slice(1)) : typed;
47190
47593
  const absolute = resolve9(expanded);
47191
47594
  let stats;
47192
47595
  try {
@@ -47195,12 +47598,13 @@ function addProjectAtPath(input) {
47195
47598
  throw new Error(`No folder at ${absolute}`);
47196
47599
  }
47197
47600
  if (!stats.isDirectory()) throw new Error(`That is a file, not a folder: ${absolute}`);
47198
- const root = discoverProjectRoot(absolute);
47199
- const id = ensureProjectIdFile(root, homedir16(), existingProjectId(root));
47601
+ const root = chosenProjectRoot(absolute, home);
47602
+ if (isHomeDirectory(root, home)) throw new Error("The home folder cannot be a project \u2014 choose a folder inside it.");
47603
+ const id = ensureProjectIdFile(root, home, existingProjectId(root));
47200
47604
  const remoteOwner = resolveProjectLabel(root)?.split("/")[0];
47201
- let registered = registerProject({ dir: root, id, remoteOwner });
47605
+ let registered = registerProject({ dir: root, id, remoteOwner }, home);
47202
47606
  if (registered.action === "duplicate") {
47203
- registered = registerProject({ dir: root, id: writeProjectIdFile(root, randomUUID16()), remoteOwner });
47607
+ registered = registerProject({ dir: root, id: writeProjectIdFile(root, randomUUID16()), remoteOwner }, home);
47204
47608
  }
47205
47609
  if (!registered.entry) throw new Error("Could not register that folder.");
47206
47610
  return projectCard(registered.entry, false);
@@ -47231,7 +47635,7 @@ function storeProjectIcon(id, name, bytes) {
47231
47635
  function setProjectIconFromFile(id, file) {
47232
47636
  let bytes;
47233
47637
  try {
47234
- bytes = readFileSync23(file);
47638
+ bytes = readFileSync24(file);
47235
47639
  } catch {
47236
47640
  throw new Error("That file could not be read.");
47237
47641
  }
@@ -47239,7 +47643,7 @@ function setProjectIconFromFile(id, file) {
47239
47643
  return storeProjectIcon(id, file, bytes);
47240
47644
  }
47241
47645
  function createRouter(ctx) {
47242
- const frizzDir = join35(ctx.project.dir, ".frizz");
47646
+ const frizzDir = join36(ctx.project.dir, ".frizz");
47243
47647
  const openRoots = openableFileRoots(ctx.project);
47244
47648
  function isAutoTitledSession(slug) {
47245
47649
  return ctx.storage.getSession(slug)?.title_auto === 1;
@@ -47390,6 +47794,9 @@ function createRouter(ctx) {
47390
47794
  if (row.claude_runtime !== "broker" || !ctx.claudeBroker) {
47391
47795
  return blocked("Steering a sub-agent needs the Claude session broker; this thread predates it.");
47392
47796
  }
47797
+ if (ctx.tailer.get(slug)?.turn === "in-flight") {
47798
+ return blocked("The thread is working on its own turn right now, and a steer sent mid-turn is delivered to the thread instead of this sub-agent. The box comes back when the thread rests.");
47799
+ }
47393
47800
  return { sessionId: row.session_id };
47394
47801
  }
47395
47802
  function codexShellTarget(slug, id) {
@@ -47644,9 +48051,10 @@ function createRouter(ctx) {
47644
48051
  //
47645
48052
  // WHY THE GATE IS STRICT. Measured live: addressing a child that has ALREADY SETTLED does not
47646
48053
  // error and does not vanish — the CLI falls the message back onto the MAIN thread, where the
47647
- // parent obeys it as if the operator had typed it into the thread composer. So an ungated steer
47648
- // is not a no-op, it is a misdelivery. `subAgentSteerable` is the single predicate that decides,
47649
- // and the drawer's prompt box is rendered off the same answer.
48054
+ // parent obeys it as if the operator had typed it into the thread composer. A steer sent while
48055
+ // the parent's own turn is IN FLIGHT misdelivers the same way (absorbed into that turn — see the
48056
+ // predicate). So an ungated steer is not a no-op, it is a misdelivery. `subAgentSteerable` is the
48057
+ // single predicate that decides, and the drawer's prompt box is rendered off the same answer.
47650
48058
  subAgentSteer: mutation({
47651
48059
  input: external_exports.object({ slug: ThreadSlug, id: external_exports.string(), message: external_exports.string().min(1), deliveryId: external_exports.string().optional() }).strict(),
47652
48060
  output: external_exports.object({ delivered: external_exports.boolean() }),
@@ -47915,6 +48323,7 @@ ${gapNote}` : input.message;
47915
48323
  mayHaveLiveBackgroundWork(ctx.tailer.get(input.slug))
47916
48324
  )
47917
48325
  });
48326
+ if (row.exited === 1) ctx.storage.setExitedIfCurrent(input.slug, row.session_id, row.runtime_generation ?? 0, false);
47918
48327
  if (input.freshProcess) retireOutstandingDeliveries(ctx.storage, input.slug);
47919
48328
  const preempted = input.interrupt === true && bridge.interruptTurn({ threadSlug: input.slug, sessionId: row.session_id });
47920
48329
  if (input.deliveryId) {
@@ -48191,6 +48600,19 @@ ${gapNote}` : input.message;
48191
48600
  ctx.board.refresh();
48192
48601
  }
48193
48602
  }),
48603
+ // Pin/unpin: the human lifts a thread out of the rail's band system into the pinned band at the
48604
+ // very top (or drops it back in). Same editability gate as the snooze, but NO archived refusal —
48605
+ // the pin deliberately outranks Done, so a pinned thread that finishes stays pinned until unpinned.
48606
+ setThreadPinned: mutation({
48607
+ input: SetThreadPinnedInput,
48608
+ handler: async ({ input }) => {
48609
+ currentOwnedSession(input.slug, input.sessionId);
48610
+ const thread = (await ctx.board.snapshot()).threads.find((candidate) => candidate.id === input.slug);
48611
+ if (!thread || thread.kind !== "session" || thread.foreign) throw new Error(`thread ${input.slug} is not editable`);
48612
+ ctx.storage.setPinnedAt(input.slug, input.pinned ? (/* @__PURE__ */ new Date()).toISOString() : null);
48613
+ ctx.board.refresh();
48614
+ }
48615
+ }),
48194
48616
  // Re-read the worker plugin closure INTO the live session: hooks, skills, agent profiles and MCP
48195
48617
  // servers, without restarting the process. This is `/reload-plugins` driven from the board.
48196
48618
  //
@@ -48239,7 +48661,7 @@ ${gapNote}` : input.message;
48239
48661
  throw new Error("This thread moved on; reopen it and try again");
48240
48662
  }
48241
48663
  if (input.stopHook && input.prompt?.trim() && autonomousGoal(row) === void 0) {
48242
- cancelQuestionsForAutonomy(input.slug);
48664
+ if (cancelQuestionsForAutonomy(input.slug) > 0) ctx.scheduler.kick();
48243
48665
  }
48244
48666
  ctx.board.refresh();
48245
48667
  }
@@ -48271,7 +48693,7 @@ ${gapNote}` : input.message;
48271
48693
  throw new Error(`thread ${input.slug} could not be updated`);
48272
48694
  }
48273
48695
  if (input.stopHook && input.prompt?.trim() && autonomousGoal(row) === void 0) {
48274
- cancelQuestionsForAutonomy(input.slug);
48696
+ if (cancelQuestionsForAutonomy(input.slug) > 0) ctx.scheduler.kick();
48275
48697
  }
48276
48698
  ctx.board.refresh();
48277
48699
  return { replaced };
@@ -48431,14 +48853,19 @@ ${gapNote}` : input.message;
48431
48853
  const armed = ctx.storage.listPrWatches(input.slug, { armedOnly: true });
48432
48854
  const existing = armed.find((w) => w.owner === ref.owner && w.repo === ref.repo && w.number === ref.number);
48433
48855
  const target = `${ref.owner}/${ref.repo}#${ref.number}`;
48434
- if (existing) return { id: existing.id, target, alreadyArmed: true, watches: armedPrWatchViews(input.slug) };
48856
+ if (existing) {
48857
+ const expiresAt = new Date(existing.expires_at ?? Date.now()).toISOString();
48858
+ return { id: existing.id, target, alreadyArmed: true, expiresAt, watches: armedPrWatchViews(input.slug) };
48859
+ }
48435
48860
  if (armed.length >= PR_WATCH_MAX_ARMED) {
48436
48861
  throw new Error(`this thread already watches ${armed.length} pull requests (the limit is ${PR_WATCH_MAX_ARMED}) \u2014 drop one first`);
48437
48862
  }
48438
- const forMs = input.for === void 0 ? PR_WATCH_DEFAULT_FOR_MS : parseAwaitingDuration(input.for);
48439
- if (forMs === null) {
48440
- throw new Error(`\`for: ${input.for}\` is not a duration \u2014 give one like \`30m\`, \`2h\` or \`3d\` (max 24h)`);
48863
+ const asked = input.for === void 0 ? PR_WATCH_DEFAULT_FOR_MS : parseAwaitingDurationRaw(input.for);
48864
+ if (asked === null) {
48865
+ throw new Error(`\`for: ${input.for}\` is not a duration \u2014 give one like \`2h\`, \`3d\` or \`180d\` (max 365d)`);
48441
48866
  }
48867
+ const forMs = Math.min(asked, PR_WATCH_FOR_MAX_MS);
48868
+ const clampedFrom = input.for !== void 0 && asked > PR_WATCH_FOR_MAX_MS ? { clampedFrom: input.for } : {};
48442
48869
  const probe = await ctx.probePr(ref);
48443
48870
  if (!probe.ok) {
48444
48871
  throw new Error(
@@ -48450,7 +48877,7 @@ ${gapNote}` : input.message;
48450
48877
  ctx.storage.clearThreadDone(input.slug);
48451
48878
  ctx.storage.armPrWatch({ id, slug: input.slug, owner: ref.owner, repo: ref.repo, number: ref.number, createdAtMs: now, expiresAtMs: now + forMs });
48452
48879
  ctx.board.refresh();
48453
- return { id, target, alreadyArmed: false, watches: armedPrWatchViews(input.slug) };
48880
+ return { id, target, alreadyArmed: false, expiresAt: new Date(now + forMs).toISOString(), ...clampedFrom, watches: armedPrWatchViews(input.slug) };
48454
48881
  }
48455
48882
  }),
48456
48883
  dropOwnPrWatch: mutation({
@@ -48495,16 +48922,18 @@ ${gapNote}` : input.message;
48495
48922
  if (armed.length >= OWN_WATCH_MAX_ARMED) {
48496
48923
  throw new Error(`this thread already holds ${armed.length} watches (the limit is ${OWN_WATCH_MAX_ARMED}) \u2014 drop one first`);
48497
48924
  }
48498
- const forMs = parseAwaitingDuration(input.for);
48499
- if (forMs === null) {
48925
+ const asked = parseAwaitingDurationRaw(input.for);
48926
+ if (asked === null) {
48500
48927
  throw new Error(`\`for: ${input.for}\` is not a duration \u2014 give one like \`30m\`, \`2h\` or \`3d\` (max 24h)`);
48501
48928
  }
48929
+ const forMs = Math.min(asked, AWAITING_FOR_MAX_MS);
48930
+ const clampedFrom = asked > AWAITING_FOR_MAX_MS ? { clampedFrom: input.for } : {};
48502
48931
  const now = Date.now();
48503
48932
  const id = `wch_${randomUUID16().replace(/-/g, "").slice(0, 12)}`;
48504
48933
  ctx.storage.clearThreadDone(input.slug);
48505
48934
  ctx.storage.armThreadWatch({ id, slug: input.slug, kind: input.kind, target, createdAtMs: now, expiresAtMs: now + forMs });
48506
48935
  ctx.board.refresh();
48507
- return { id, kind: input.kind, target, alreadyArmed: false, watches: armedOwnWatchViews(input.slug) };
48936
+ return { id, kind: input.kind, target, alreadyArmed: false, ...clampedFrom, watches: armedOwnWatchViews(input.slug) };
48508
48937
  }
48509
48938
  }),
48510
48939
  dropOwnWatch: mutation({
@@ -48547,12 +48976,6 @@ Say which way you went and why in your write-up, so the human can course-correct
48547
48976
  }
48548
48977
  const faults = input.questions.flatMap((q) => askedQuestionFaults(q));
48549
48978
  if (faults.length > 0) throw new Error(faults.join("\n"));
48550
- const open = ctx.storage.listThreadQuestions(input.slug, { openOnly: true });
48551
- if (open.length + input.questions.length > ASK_MAX_OPEN) {
48552
- throw new Error(
48553
- `this thread already has ${open.length} unanswered question(s); the limit is ${ASK_MAX_OPEN}. Withdraw the ones you no longer need answered with \`unask\`, or decide them yourself.`
48554
- );
48555
- }
48556
48979
  const now = Date.now();
48557
48980
  const registered = input.questions.map((spec) => {
48558
48981
  const id = `qst_${randomUUID16().replace(/-/g, "").slice(0, 12)}`;
@@ -48787,12 +49210,13 @@ Say which way you went and why in your write-up, so the human can course-correct
48787
49210
  output: external_exports.object({ path: external_exports.string(), markdown: external_exports.string(), truncated: external_exports.boolean() }),
48788
49211
  handler: async ({ input }) => readLocalMarkdown(input.path, openRoots)
48789
49212
  }),
48790
- // A project file's SOURCE, for the fullscreen page's file viewer. Rooted at the project directory
48791
- // ONLY — see readLocalTextFile for why this gate is narrower than openRoots.
49213
+ // A file's SOURCE, for the fullscreen page's file viewer. The SAME openable roots as the Markdown
49214
+ // reader — see readLocalTextFile for why the project-directory-only gate this replaced refused 41%
49215
+ // of the rail's own rows (a worker's checkout is very often a worktree outside the project dir).
48792
49216
  localFile: query({
48793
49217
  input: external_exports.object({ path: external_exports.string().max(4096) }).strict(),
48794
49218
  output: external_exports.object({ path: external_exports.string(), text: external_exports.string(), truncated: external_exports.boolean() }),
48795
- handler: async ({ input }) => readLocalTextFile(input.path, [ctx.project.dir])
49219
+ handler: async ({ input }) => readLocalTextFile(input.path, openRoots)
48796
49220
  }),
48797
49221
  // Batch-classify path REFERENCES (as they appear in inline code) → their canonical openable path, or
48798
49222
  // null when a candidate doesn't resolve to a real file under the openable roots. The client renders
@@ -49191,8 +49615,8 @@ Say which way you went and why in your write-up, so the human can course-correct
49191
49615
  * Register a directory as a project, from the grid's phantom card.
49192
49616
  *
49193
49617
  * The same authority as running `frizz` in that directory, and strictly less: this registers and
49194
- * resolves an id, it dispatches nothing. Everything it does — walk up to the project root, mint or
49195
- * adopt `.frizz/.id`, write the index — is what the CLI already does on every launch.
49618
+ * resolves an id, it dispatches nothing. The root comes from chosenProjectRoot — a folder inside
49619
+ * a checkout adds the checkout, but an adopted plain-directory ancestor never captures the pick.
49196
49620
  */
49197
49621
  projectAdd: mutation({
49198
49622
  input: external_exports.object({ path: external_exports.string().min(1) }),
@@ -49573,7 +49997,7 @@ var init_compression = __esm({
49573
49997
  });
49574
49998
 
49575
49999
  // packages/server/src/local-image.ts
49576
- import { readFileSync as readFileSync24, realpathSync as realpathSync9, statSync as statSync18 } from "node:fs";
50000
+ import { readFileSync as readFileSync25, realpathSync as realpathSync9, statSync as statSync18 } from "node:fs";
49577
50001
  import { extname as extname2, isAbsolute as isAbsolute5 } from "node:path";
49578
50002
  function resolveLocalImage(rawPath) {
49579
50003
  if (!rawPath || !isAbsolute5(rawPath)) return { status: 400 };
@@ -49587,7 +50011,7 @@ function resolveLocalImage(rawPath) {
49587
50011
  }
49588
50012
  try {
49589
50013
  if (!statSync18(real).isFile()) return { status: 404 };
49590
- return { status: 200, contentType, body: readFileSync24(real) };
50014
+ return { status: 200, contentType, body: readFileSync25(real) };
49591
50015
  } catch {
49592
50016
  return { status: 404 };
49593
50017
  }
@@ -49607,9 +50031,9 @@ var init_local_image = __esm({
49607
50031
  });
49608
50032
 
49609
50033
  // packages/server/src/project-icon.ts
49610
- import { readFileSync as readFileSync25, readdirSync as readdirSync10, realpathSync as realpathSync10, statSync as statSync19 } from "node:fs";
49611
- import { homedir as homedir17 } from "node:os";
49612
- import { basename as basename10, dirname as dirname15, extname as extname3, join as join36, relative as relative2, sep as sep3 } from "node:path";
50034
+ import { readFileSync as readFileSync26, readdirSync as readdirSync10, realpathSync as realpathSync10, statSync as statSync19 } from "node:fs";
50035
+ import { homedir as homedir18 } from "node:os";
50036
+ import { basename as basename10, dirname as dirname15, extname as extname3, join as join37, relative as relative2, sep as sep3 } from "node:path";
49613
50037
  function normalizeStem(stem) {
49614
50038
  const sized = stem.toLowerCase().replace(/[@-]\d+(\.\d+)?x(\d+)?$/u, "").replace(/[-_]\d{2,4}$/u, "");
49615
50039
  const normalized = sized.replace(VARIANT_SUFFIX, "");
@@ -49627,7 +50051,7 @@ function sizeScore(dimensions) {
49627
50051
  return Math.max(0, Math.min(40, Math.round(Math.log2(edge / 24) * 8)));
49628
50052
  }
49629
50053
  function under(root, relativeDirectory) {
49630
- return relativeDirectory ? join36(root, relativeDirectory) : root;
50054
+ return relativeDirectory ? join37(root, relativeDirectory) : root;
49631
50055
  }
49632
50056
  function listFiles(directory) {
49633
50057
  try {
@@ -49649,19 +50073,19 @@ function iconDirectories(root) {
49649
50073
  for (const asset of ASSET_DIRECTORIES) directories.push(under(root, asset));
49650
50074
  for (const host of HOST_DIRECTORIES) {
49651
50075
  if (!children2.has(host)) continue;
49652
- for (const asset of ASSET_DIRECTORIES) directories.push(under(join36(root, host), asset));
50076
+ for (const asset of ASSET_DIRECTORIES) directories.push(under(join37(root, host), asset));
49653
50077
  }
49654
50078
  for (const extra of EXTRA_DIRECTORIES) directories.push(under(root, extra));
49655
50079
  for (const container of WORKSPACE_CONTAINERS) {
49656
50080
  if (!children2.has(container)) continue;
49657
50081
  let members;
49658
50082
  try {
49659
- members = readdirSync10(join36(root, container), { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name);
50083
+ members = readdirSync10(join37(root, container), { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name);
49660
50084
  } catch {
49661
50085
  continue;
49662
50086
  }
49663
50087
  for (const member of members) {
49664
- for (const asset of ASSET_DIRECTORIES) directories.push(under(join36(root, container, member), asset));
50088
+ for (const asset of ASSET_DIRECTORIES) directories.push(under(join37(root, container, member), asset));
49665
50089
  }
49666
50090
  }
49667
50091
  return [...new Set(directories)];
@@ -49669,13 +50093,13 @@ function iconDirectories(root) {
49669
50093
  function manifestIcons(manifestPath) {
49670
50094
  let parsed;
49671
50095
  try {
49672
- parsed = JSON.parse(readFileSync25(manifestPath, "utf8"));
50096
+ parsed = JSON.parse(readFileSync26(manifestPath, "utf8"));
49673
50097
  } catch {
49674
50098
  return [];
49675
50099
  }
49676
50100
  if (!Array.isArray(parsed?.icons)) return [];
49677
50101
  const directory = dirname15(manifestPath);
49678
- return parsed.icons.map((icon) => typeof icon?.src === "string" ? icon.src : void 0).filter((src) => !!src && !/^(https?:)?\/\//u.test(src) && !src.startsWith("data:")).map((src) => join36(directory, src.replace(/[?#].*$/u, "").replace(/^\//u, "")));
50102
+ return parsed.icons.map((icon) => typeof icon?.src === "string" ? icon.src : void 0).filter((src) => !!src && !/^(https?:)?\/\//u.test(src) && !src.startsWith("data:")).map((src) => join37(directory, src.replace(/[?#].*$/u, "").replace(/^\//u, "")));
49679
50103
  }
49680
50104
  function projectIconCandidates(root) {
49681
50105
  const projectStem = normalizeStem(basename10(root)).normalized;
@@ -49694,7 +50118,7 @@ function projectIconCandidates(root) {
49694
50118
  for (const name of listFiles(directory)) {
49695
50119
  const extension2 = extname3(name).toLowerCase();
49696
50120
  if (MANIFEST_NAMES.includes(name.toLowerCase())) {
49697
- for (const icon of manifestIcons(join36(directory, name))) {
50121
+ for (const icon of manifestIcons(join37(directory, name))) {
49698
50122
  if (MEASURABLE_IMAGE_EXTENSIONS.has(extname3(icon).toLowerCase())) {
49699
50123
  fromManifest.add(icon);
49700
50124
  files.push({ path: icon, directory: dirname15(icon) });
@@ -49705,7 +50129,7 @@ function projectIconCandidates(root) {
49705
50129
  if (!MEASURABLE_IMAGE_EXTENSIONS.has(extension2)) continue;
49706
50130
  const stem = name.slice(0, name.length - extension2.length);
49707
50131
  if (normalizeStem(stem).normalized === "favicon") iconHomes.add(directory);
49708
- files.push({ path: join36(directory, name), directory });
50132
+ files.push({ path: join37(directory, name), directory });
49709
50133
  if (files.length >= MAX_CANDIDATES) break;
49710
50134
  }
49711
50135
  }
@@ -49747,7 +50171,7 @@ function iconMediaType(path) {
49747
50171
  }
49748
50172
  function resolveProjectIconResponse(id, options = {}) {
49749
50173
  if (!id) return { status: 400 };
49750
- const home = options.home ?? homedir17();
50174
+ const home = options.home ?? homedir18();
49751
50175
  const entry = readRegistry(home).projects.find((project) => project.id === id);
49752
50176
  if (!entry) return { status: 404 };
49753
50177
  const path = resolveProjectIcon(id, (root) => detectProjectIcon(root)?.path, { home });
@@ -49770,7 +50194,7 @@ function resolveProjectIconResponse(id, options = {}) {
49770
50194
  });
49771
50195
  if (!roots.some((root) => real === root || real.startsWith(root + sep3))) return { status: 404 };
49772
50196
  try {
49773
- return { status: 200, contentType, body: readFileSync25(real) };
50197
+ return { status: 200, contentType, body: readFileSync26(real) };
49774
50198
  } catch {
49775
50199
  return { status: 404 };
49776
50200
  }
@@ -49882,8 +50306,8 @@ var init_project_icon = __esm({
49882
50306
  });
49883
50307
 
49884
50308
  // packages/server/src/local-visualization.ts
49885
- import { readdirSync as readdirSync11, readFileSync as readFileSync26, realpathSync as realpathSync11, statSync as statSync20 } from "node:fs";
49886
- import { join as join37, sep as sep4 } from "node:path";
50309
+ import { readdirSync as readdirSync11, readFileSync as readFileSync27, realpathSync as realpathSync11, statSync as statSync20 } from "node:fs";
50310
+ import { join as join38, sep as sep4 } from "node:path";
49887
50311
  function children(path, pattern) {
49888
50312
  try {
49889
50313
  return readdirSync11(path, { withFileTypes: true }).filter((entry) => entry.isDirectory() && pattern.test(entry.name)).map((entry) => entry.name).sort().reverse();
@@ -49895,7 +50319,7 @@ function isUnder2(path, root) {
49895
50319
  return path === root || path.startsWith(root.endsWith(sep4) ? root : root + sep4);
49896
50320
  }
49897
50321
  function resolveFragment(projectDir, sessionId, file) {
49898
- const base = join37(projectDir, ".codex", "visualizations");
50322
+ const base = join38(projectDir, ".codex", "visualizations");
49899
50323
  let projectReal;
49900
50324
  let baseReal;
49901
50325
  try {
@@ -49906,12 +50330,12 @@ function resolveFragment(projectDir, sessionId, file) {
49906
50330
  }
49907
50331
  if (!isUnder2(baseReal, projectReal)) return null;
49908
50332
  for (const year of children(base, YEAR_PART)) {
49909
- const yearDir = join37(base, year);
50333
+ const yearDir = join38(base, year);
49910
50334
  for (const month of children(yearDir, DATE_PART)) {
49911
- const monthDir = join37(yearDir, month);
50335
+ const monthDir = join38(yearDir, month);
49912
50336
  for (const day of children(monthDir, DATE_PART)) {
49913
- const sessionRoot = join37(baseReal, year, month, day, sessionId);
49914
- const candidate = join37(monthDir, day, sessionId, file);
50337
+ const sessionRoot = join38(baseReal, year, month, day, sessionId);
50338
+ const candidate = join38(monthDir, day, sessionId, file);
49915
50339
  try {
49916
50340
  const real = realpathSync11(candidate);
49917
50341
  if (isUnder2(real, sessionRoot) && statSync20(real).isFile()) return real;
@@ -49997,7 +50421,7 @@ function resolveLocalVisualization(projectDir, sessionId, file) {
49997
50421
  try {
49998
50422
  const size = statSync20(path).size;
49999
50423
  if (size > MAX_FRAGMENT_BYTES) return { status: 413 };
50000
- fragment = readFileSync26(path, "utf8");
50424
+ fragment = readFileSync27(path, "utf8");
50001
50425
  } catch {
50002
50426
  return { status: 404 };
50003
50427
  }
@@ -50040,7 +50464,7 @@ var init_local_visualization = __esm({
50040
50464
 
50041
50465
  // packages/server/src/app.ts
50042
50466
  import { mkdirSync as mkdirSync17, writeFileSync as writeFileSync14 } from "node:fs";
50043
- import { join as join38 } from "node:path";
50467
+ import { join as join39 } from "node:path";
50044
50468
  import { randomUUID as randomUUID17 } from "node:crypto";
50045
50469
  function createApp(ctx, options = {}) {
50046
50470
  const app = new Hono2();
@@ -50144,10 +50568,10 @@ function createApp(ctx, options = {}) {
50144
50568
  if (typeof body.data !== "string" || body.data.length > ATTACHMENT_MAX_BASE64_CHARS) return c.json({ error: "bad payload" }, 400);
50145
50569
  const ext = `.${attachmentExtension(name)}`;
50146
50570
  const buf = Buffer.from(body.data, "base64");
50147
- const dir = join38(ctx.project.stateDir, "attachments");
50571
+ const dir = join39(ctx.project.stateDir, "attachments");
50148
50572
  mkdirSync17(dir, { recursive: true });
50149
50573
  const base = name.replace(/\.[^.]+$/, "").replace(/[^a-zA-Z0-9_-]+/g, "-").slice(0, 40) || "file";
50150
- const path = join38(dir, `${Date.now()}-${randomUUID17().slice(0, 8)}-${base}${ext}`);
50574
+ const path = join39(dir, `${Date.now()}-${randomUUID17().slice(0, 8)}-${base}${ext}`);
50151
50575
  writeFileSync14(path, buf);
50152
50576
  return c.json({ path });
50153
50577
  });
@@ -50634,6 +51058,7 @@ function createAppSocketServer(deps) {
50634
51058
  const maxOutputBufferBytes = deps.maxOutputBufferBytes ?? APP_SOCKET_MAX_OUTPUT_BUFFER_BYTES;
50635
51059
  const maxConnections = boundedCount(deps.maxConnections, APP_SOCKET_MAX_CONNECTIONS);
50636
51060
  const maxSubscriptions = deps.maxSubscriptionsPerConnection ?? APP_SOCKET_MAX_SUBSCRIPTIONS;
51061
+ const maxFileWatches = boundedCount(deps.maxFileWatchesPerConnection, APP_SOCKET_MAX_FILE_WATCHES);
50637
51062
  const maxMessagesPerWindow = deps.maxMessagesPerWindow ?? APP_SOCKET_MAX_MESSAGES_PER_WINDOW;
50638
51063
  const messageWindowMs = deps.messageWindowMs ?? APP_SOCKET_MESSAGE_WINDOW_MS;
50639
51064
  const maxBufferedKeyframeEvents = deps.maxBufferedKeyframeEvents ?? APP_SOCKET_MAX_BUFFERED_KEYFRAME_EVENTS;
@@ -50673,6 +51098,55 @@ function createAppSocketServer(deps) {
50673
51098
  let pendingUpgrades = 0;
50674
51099
  const socketOrigins = /* @__PURE__ */ new WeakMap();
50675
51100
  const lastSig = /* @__PURE__ */ new Map();
51101
+ const fileWatches = /* @__PURE__ */ new WeakMap();
51102
+ let fileWatchCount = 0;
51103
+ function subscribeFile(ws, path) {
51104
+ let held = fileWatches.get(ws);
51105
+ if (!held) {
51106
+ held = /* @__PURE__ */ new Map();
51107
+ fileWatches.set(ws, held);
51108
+ }
51109
+ if (held.has(path)) return;
51110
+ if (held.size >= maxFileWatches) {
51111
+ closeSocket(ws, 1008, "subscription limit exceeded");
51112
+ return;
51113
+ }
51114
+ const watchFile = deps.watchFile;
51115
+ if (!watchFile) return;
51116
+ let release;
51117
+ try {
51118
+ release = watchFile(path, () => {
51119
+ sendMsg(ws, { t: "file-changed", path });
51120
+ });
51121
+ } catch {
51122
+ return;
51123
+ }
51124
+ held.set(path, release);
51125
+ fileWatchCount++;
51126
+ }
51127
+ function unsubscribeFile(ws, path) {
51128
+ const held = fileWatches.get(ws);
51129
+ const release = held?.get(path);
51130
+ if (!held || !release) return;
51131
+ held.delete(path);
51132
+ fileWatchCount--;
51133
+ try {
51134
+ release();
51135
+ } catch {
51136
+ }
51137
+ }
51138
+ function dropFileWatches(ws) {
51139
+ const held = fileWatches.get(ws);
51140
+ if (!held) return;
51141
+ fileWatches.delete(ws);
51142
+ for (const release of held.values()) {
51143
+ fileWatchCount--;
51144
+ try {
51145
+ release();
51146
+ } catch {
51147
+ }
51148
+ }
51149
+ }
50676
51150
  function closeSocket(ws, code, reason) {
50677
51151
  if (ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) return;
50678
51152
  try {
@@ -51145,6 +51619,11 @@ function createAppSocketServer(deps) {
51145
51619
  return;
51146
51620
  }
51147
51621
  const msg = parsed.data;
51622
+ if (msg.topic === "file") {
51623
+ if (msg.t === "sub") subscribeFile(ws, msg.path);
51624
+ else unsubscribeFile(ws, msg.path);
51625
+ return;
51626
+ }
51148
51627
  if (msg.t === "sub") {
51149
51628
  if (registry.isSubscribed(ws, msg.slug)) return;
51150
51629
  if (registry.slugsFor(ws).length >= maxSubscriptions) {
@@ -51165,6 +51644,7 @@ function createAppSocketServer(deps) {
51165
51644
  }
51166
51645
  clearInterval(heartbeat);
51167
51646
  cancelPendingSubscriptionPush(ws);
51647
+ dropFileWatches(ws);
51168
51648
  const terminationTimer = terminationTimers.get(ws);
51169
51649
  if (terminationTimer) clearTimeout(terminationTimer);
51170
51650
  terminationTimers.delete(ws);
@@ -51288,10 +51768,13 @@ function createAppSocketServer(deps) {
51288
51768
  },
51289
51769
  get pendingTranscriptRefreshes() {
51290
51770
  return pendingTranscriptRefreshes.size;
51771
+ },
51772
+ get fileWatchCount() {
51773
+ return fileWatchCount;
51291
51774
  }
51292
51775
  };
51293
51776
  }
51294
- var WS_PATH, SubscriptionRegistry, HEARTBEAT_MS, APP_SOCKET_MAX_MESSAGE_BYTES, APP_SOCKET_MAX_LOGICAL_FRAME_BYTES, APP_SOCKET_MAX_OUTPUT_BUFFER_BYTES, APP_SOCKET_MAX_CONNECTIONS, APP_SOCKET_MAX_SUBSCRIPTIONS, APP_SOCKET_MAX_MESSAGES_PER_WINDOW, APP_SOCKET_MESSAGE_WINDOW_MS, APP_SOCKET_MAX_BUFFERED_KEYFRAME_EVENTS, APP_SOCKET_MAX_TRANSCRIPT_READS_PER_ORIGIN, APP_SOCKET_MAX_TRANSCRIPT_READS_OVERALL, APP_SOCKET_TRANSCRIPT_READ_WINDOW_MS, APP_SOCKET_MAX_TRANSCRIPT_CACHE_ENTRIES, APP_SOCKET_MAX_TRANSCRIPT_CACHE_BYTES;
51777
+ var WS_PATH, SubscriptionRegistry, HEARTBEAT_MS, APP_SOCKET_MAX_MESSAGE_BYTES, APP_SOCKET_MAX_LOGICAL_FRAME_BYTES, APP_SOCKET_MAX_OUTPUT_BUFFER_BYTES, APP_SOCKET_MAX_CONNECTIONS, APP_SOCKET_MAX_SUBSCRIPTIONS, APP_SOCKET_MAX_FILE_WATCHES, APP_SOCKET_MAX_MESSAGES_PER_WINDOW, APP_SOCKET_MESSAGE_WINDOW_MS, APP_SOCKET_MAX_BUFFERED_KEYFRAME_EVENTS, APP_SOCKET_MAX_TRANSCRIPT_READS_PER_ORIGIN, APP_SOCKET_MAX_TRANSCRIPT_READS_OVERALL, APP_SOCKET_TRANSCRIPT_READ_WINDOW_MS, APP_SOCKET_MAX_TRANSCRIPT_CACHE_ENTRIES, APP_SOCKET_MAX_TRANSCRIPT_CACHE_BYTES;
51295
51778
  var init_app_socket = __esm({
51296
51779
  "packages/server/src/app-socket.ts"() {
51297
51780
  "use strict";
@@ -51374,6 +51857,7 @@ var init_app_socket = __esm({
51374
51857
  APP_SOCKET_MAX_OUTPUT_BUFFER_BYTES = 4 * 1024 * 1024;
51375
51858
  APP_SOCKET_MAX_CONNECTIONS = 64;
51376
51859
  APP_SOCKET_MAX_SUBSCRIPTIONS = 32;
51860
+ APP_SOCKET_MAX_FILE_WATCHES = 16;
51377
51861
  APP_SOCKET_MAX_MESSAGES_PER_WINDOW = 120;
51378
51862
  APP_SOCKET_MESSAGE_WINDOW_MS = 1e3;
51379
51863
  APP_SOCKET_MAX_BUFFERED_KEYFRAME_EVENTS = 256;
@@ -51385,11 +51869,60 @@ var init_app_socket = __esm({
51385
51869
  }
51386
51870
  });
51387
51871
 
51872
+ // packages/server/src/local-file-watch.ts
51873
+ import { watch } from "node:fs";
51874
+ import { basename as basename11, dirname as dirname16 } from "node:path";
51875
+ function close(path) {
51876
+ const entry = entries.get(path);
51877
+ if (!entry) return;
51878
+ entries.delete(path);
51879
+ if (entry.timer) clearTimeout(entry.timer);
51880
+ try {
51881
+ entry.watcher.close();
51882
+ } catch {
51883
+ }
51884
+ }
51885
+ function watchLocalFile(path, onChange) {
51886
+ let entry = entries.get(path);
51887
+ if (!entry) {
51888
+ const name = basename11(path);
51889
+ const watcher2 = watch(dirname16(path), { persistent: false }, (_event, changed) => {
51890
+ if (changed !== null && changed !== void 0 && changed.toString() !== name) return;
51891
+ const current = entries.get(path);
51892
+ if (!current) return;
51893
+ if (current.timer) clearTimeout(current.timer);
51894
+ current.timer = setTimeout(() => {
51895
+ current.timer = null;
51896
+ for (const listener of current.listeners) listener();
51897
+ }, SETTLE_MS);
51898
+ current.timer.unref?.();
51899
+ });
51900
+ entry = { watcher: watcher2, listeners: /* @__PURE__ */ new Set(), timer: null };
51901
+ watcher2.on("error", () => close(path));
51902
+ entries.set(path, entry);
51903
+ }
51904
+ entry.listeners.add(onChange);
51905
+ return () => {
51906
+ const current = entries.get(path);
51907
+ if (!current) return;
51908
+ current.listeners.delete(onChange);
51909
+ if (current.listeners.size === 0) close(path);
51910
+ };
51911
+ }
51912
+ var SETTLE_MS, entries;
51913
+ var init_local_file_watch = __esm({
51914
+ "packages/server/src/local-file-watch.ts"() {
51915
+ "use strict";
51916
+ SETTLE_MS = 50;
51917
+ entries = /* @__PURE__ */ new Map();
51918
+ }
51919
+ });
51920
+
51388
51921
  // packages/server/src/boot-progress.ts
51389
- import { mkdirSync as mkdirSync18, readFileSync as readFileSync27, renameSync as renameSync10, rmSync as rmSync11, writeFileSync as writeFileSync15 } from "node:fs";
51390
- import { basename as basename11, dirname as dirname16, join as join39 } from "node:path";
51922
+ import { mkdirSync as mkdirSync18, readFileSync as readFileSync28, renameSync as renameSync10, rmSync as rmSync11, writeFileSync as writeFileSync15 } from "node:fs";
51923
+ import { basename as basename12, dirname as dirname17, join as join40 } from "node:path";
51391
51924
  function bootProgressPath(stateDir) {
51392
- return join39(stateDir, BOOT_PROGRESS_NAME);
51925
+ return join40(stateDir, BOOT_PROGRESS_NAME);
51393
51926
  }
51394
51927
  function createBootProgressPublisher(stateDir, minIntervalMs = 200) {
51395
51928
  if (!stateDir) {
@@ -51409,8 +51942,8 @@ function createBootProgressPublisher(stateDir, minIntervalMs = 200) {
51409
51942
  lastAt = now;
51410
51943
  step++;
51411
51944
  try {
51412
- mkdirSync18(dirname16(path), { recursive: true, mode: 448 });
51413
- const temp = join39(dirname16(path), `.${basename11(path)}.${process.pid}.tmp`);
51945
+ mkdirSync18(dirname17(path), { recursive: true, mode: 448 });
51946
+ const temp = join40(dirname17(path), `.${basename12(path)}.${process.pid}.tmp`);
51414
51947
  writeFileSync15(temp, `${JSON.stringify({ pid: process.pid, step, phase, at: new Date(now).toISOString() })}
51415
51948
  `, "utf8");
51416
51949
  renameSync10(temp, path);
@@ -51963,17 +52496,17 @@ var init_legacy_project_db = __esm({
51963
52496
 
51964
52497
  // packages/server/src/frizz-db.ts
51965
52498
  import { existsSync as existsSync14, mkdirSync as mkdirSync19, readdirSync as readdirSync12, renameSync as renameSync11, statSync as statSync21 } from "node:fs";
51966
- import { basename as basename12, dirname as dirname17, join as join40 } from "node:path";
52499
+ import { basename as basename13, dirname as dirname18, join as join41 } from "node:path";
51967
52500
  function frizzDatabaseLocation(options) {
51968
52501
  if (options.stateDir !== void 0) {
51969
- const projectsDir = dirname17(options.stateDir);
51970
- if (basename12(projectsDir) === "projects") {
51971
- return { path: options.path ?? join40(dirname17(projectsDir), "ui.db"), projectsDir };
52502
+ const projectsDir = dirname18(options.stateDir);
52503
+ if (basename13(projectsDir) === "projects") {
52504
+ return { path: options.path ?? join41(dirname18(projectsDir), "ui.db"), projectsDir };
51972
52505
  }
51973
- return { path: options.path ?? join40(options.stateDir, "ui.db"), projectsDir: void 0 };
52506
+ return { path: options.path ?? join41(options.stateDir, "ui.db"), projectsDir: void 0 };
51974
52507
  }
51975
52508
  const data = options.home ? frizzPaths({ home: options.home }).data : frizzRoots().data;
51976
- return { path: options.path ?? join40(data, "ui.db"), projectsDir: join40(data, "projects") };
52509
+ return { path: options.path ?? join41(data, "ui.db"), projectsDir: join41(data, "projects") };
51977
52510
  }
51978
52511
  function ensureFrizzSchema(db) {
51979
52512
  db.exec(`
@@ -51992,7 +52525,7 @@ function ensureFrizzSchema(db) {
51992
52525
  }
51993
52526
  function openFrizzDatabase(options = {}) {
51994
52527
  const { path, projectsDir } = frizzDatabaseLocation(options);
51995
- mkdirSync19(dirname17(path), { recursive: true });
52528
+ mkdirSync19(dirname18(path), { recursive: true });
51996
52529
  const db = new sqlite_default(path);
51997
52530
  db.pragma("busy_timeout = 5000");
51998
52531
  db.pragma("journal_mode = WAL");
@@ -52004,10 +52537,10 @@ function openFrizzDatabase(options = {}) {
52004
52537
  );
52005
52538
  for (const projectId of retiredLegacyProjectIds(projectsDir)) {
52006
52539
  if (alreadyImported.has(projectId)) continue;
52007
- const dir = join40(projectsDir, projectId);
52540
+ const dir = join41(projectsDir, projectId);
52008
52541
  try {
52009
- renameSync11(join40(dir, "ui.db.imported"), join40(dir, "ui.db"));
52010
- db.prepare("INSERT INTO imported_project (project_id, source, rows, imported_at) VALUES (?, ?, 0, ?)").run(projectId, join40(dir, "ui.db"), (/* @__PURE__ */ new Date()).toISOString());
52542
+ renameSync11(join41(dir, "ui.db.imported"), join41(dir, "ui.db"));
52543
+ db.prepare("INSERT INTO imported_project (project_id, source, rows, imported_at) VALUES (?, ?, 0, ?)").run(projectId, join41(dir, "ui.db"), (/* @__PURE__ */ new Date()).toISOString());
52011
52544
  alreadyImported.add(projectId);
52012
52545
  log.info("frizz-db", `${projectId}: restored ui.db.imported to ui.db and recorded it as imported`);
52013
52546
  } catch (error) {
@@ -52017,7 +52550,7 @@ function openFrizzDatabase(options = {}) {
52017
52550
  }
52018
52551
  for (const projectId of legacyProjectIds(projectsDir)) {
52019
52552
  if (alreadyImported.has(projectId)) continue;
52020
- const legacyPath = join40(projectsDir, projectId, "ui.db");
52553
+ const legacyPath = join41(projectsDir, projectId, "ui.db");
52021
52554
  try {
52022
52555
  const rows = importLegacyProject(db, projectId, legacyPath);
52023
52556
  imported.push({ projectId, rows });
@@ -52050,7 +52583,7 @@ function projectIdsWithFile(projectsDir, file) {
52050
52583
  if (!existsSync14(projectsDir)) return [];
52051
52584
  return readdirSync12(projectsDir).filter((name) => {
52052
52585
  try {
52053
- return statSync21(join40(projectsDir, name, file)).isFile();
52586
+ return statSync21(join41(projectsDir, name, file)).isFile();
52054
52587
  } catch {
52055
52588
  return false;
52056
52589
  }
@@ -52060,7 +52593,7 @@ function legacyProjectIds(projectsDir) {
52060
52593
  return projectIdsWithFile(projectsDir, "ui.db");
52061
52594
  }
52062
52595
  function retiredLegacyProjectIds(projectsDir) {
52063
- return projectIdsWithFile(projectsDir, "ui.db.imported").filter((id) => !existsSync14(join40(projectsDir, id, "ui.db")));
52596
+ return projectIdsWithFile(projectsDir, "ui.db.imported").filter((id) => !existsSync14(join41(projectsDir, id, "ui.db")));
52064
52597
  }
52065
52598
  function tableColumns(db, schema, table) {
52066
52599
  return db.prepare(`PRAGMA ${schema}.table_info(${table})`).all().map((c) => c.name);
@@ -52213,8 +52746,8 @@ var init_tenant_prime = __esm({
52213
52746
 
52214
52747
  // packages/server/src/project-teardown.ts
52215
52748
  import { rmSync as rmSync12 } from "node:fs";
52216
- import { homedir as homedir18 } from "node:os";
52217
- import { dirname as dirname18, resolve as resolve10 } from "node:path";
52749
+ import { homedir as homedir19 } from "node:os";
52750
+ import { dirname as dirname19, resolve as resolve10 } from "node:path";
52218
52751
  async function stopProjectWorkers(ctx) {
52219
52752
  let stopped = 0;
52220
52753
  for (const row of ctx.storage.allSessions()) {
@@ -52229,12 +52762,12 @@ async function stopProjectWorkers(ctx) {
52229
52762
  }
52230
52763
  return stopped;
52231
52764
  }
52232
- function deleteProjectState(projectId, home = homedir18()) {
52765
+ function deleteProjectState(projectId, home = homedir19()) {
52233
52766
  const target = projectStateDir(projectId, home);
52234
52767
  if (!/^[\w.-]+$/u.test(projectId) || projectId === "." || projectId === "..") {
52235
52768
  throw new Error(`Refusing to delete state for a malformed project id: ${projectId}`);
52236
52769
  }
52237
- if (dirname18(resolve10(target)) !== resolve10(dirname18(projectStateDir("x", home)))) {
52770
+ if (dirname19(resolve10(target)) !== resolve10(dirname19(projectStateDir("x", home)))) {
52238
52771
  throw new Error(`Refusing to delete ${target} \u2014 it is not a project state directory`);
52239
52772
  }
52240
52773
  rmSync12(target, { recursive: true, force: true });
@@ -52262,11 +52795,11 @@ __export(index_exports, {
52262
52795
  unknownProjectPage: () => unknownProjectPage
52263
52796
  });
52264
52797
  import { createServer } from "node:http";
52265
- import { readFileSync as readFileSync28, existsSync as existsSync15 } from "node:fs";
52266
- import { dirname as dirname19, join as join41, resolve as resolve11, extname as extname4, normalize } from "node:path";
52798
+ import { readFileSync as readFileSync29, existsSync as existsSync15 } from "node:fs";
52799
+ import { dirname as dirname20, join as join42, resolve as resolve11, extname as extname4, normalize } from "node:path";
52267
52800
  function readServerAddressHolder(path) {
52268
52801
  try {
52269
- const value = JSON.parse(readFileSync28(path, "utf8"));
52802
+ const value = JSON.parse(readFileSync29(path, "utf8"));
52270
52803
  if (typeof value?.pid !== "number") return void 0;
52271
52804
  return { pid: value.pid, port: typeof value.port === "number" ? value.port : -1 };
52272
52805
  } catch {
@@ -52366,11 +52899,11 @@ function createShutdownSignalHandler(options) {
52366
52899
  }
52367
52900
  function serveStatic(distDir, req, res) {
52368
52901
  const rel = normalize((req.url ?? "/").split("?")[0]).replace(/^(\.\.[/\\])+/, "");
52369
- let file = join41(distDir, rel === "/" ? "index.html" : rel);
52370
- if (!file.startsWith(distDir)) file = join41(distDir, "index.html");
52371
- if (!existsSync15(file)) file = join41(distDir, "index.html");
52902
+ let file = join42(distDir, rel === "/" ? "index.html" : rel);
52903
+ if (!file.startsWith(distDir)) file = join42(distDir, "index.html");
52904
+ if (!existsSync15(file)) file = join42(distDir, "index.html");
52372
52905
  try {
52373
- const body = readFileSync28(file);
52906
+ const body = readFileSync29(file);
52374
52907
  res.writeHead(200, { "content-type": MIME[extname4(file)] ?? "application/octet-stream" });
52375
52908
  res.end(body);
52376
52909
  } catch {
@@ -52472,7 +53005,11 @@ async function startServer(opts = {}) {
52472
53005
  // The /ws producer is the one the live UI actually renders, so a dead owner has to reach it
52473
53006
  // too — projecting only the RPC is the exact half-fix the × already had to correct once.
52474
53007
  (slug) => c.tailer.ownerGone?.(slug) ?? false
52475
- )
53008
+ ),
53009
+ // A reader's live watch on the file it shows. Gated as the read is (one set of openable roots for
53010
+ // both readers, plus the Markdown reader's canonical-extension check) and watched by CANONICAL
53011
+ // path, so a symlinked `.md` and its target are one watch.
53012
+ watchFile: (path, onChange) => watchLocalFile(resolveWatchableLocalFile(path, openableFileRoots(c.project)), onChange)
52476
53013
  });
52477
53014
  let githubInit;
52478
53015
  let terminal;
@@ -52616,7 +53153,7 @@ async function startServer(opts = {}) {
52616
53153
  if (statusPath && statusIdentity) runtime.removeStatus(statusPath, statusIdentity);
52617
53154
  if (statusPath && statusIdentity) {
52618
53155
  try {
52619
- runtime.removeStatus(serverAddressPathForStateDir(dirname19(statusPath)), statusIdentity);
53156
+ runtime.removeStatus(serverAddressPathForStateDir(dirname20(statusPath)), statusIdentity);
52620
53157
  } catch {
52621
53158
  }
52622
53159
  }
@@ -52745,7 +53282,7 @@ async function startServer(opts = {}) {
52745
53282
  }
52746
53283
  statusPath = serverLockPathFor(ctx.project);
52747
53284
  const webRoot = resolve11(import.meta.dirname, "..", "..", "web");
52748
- const distDir = opts.webDistDir ? resolve11(opts.webDistDir) : join41(webRoot, "dist");
53285
+ const distDir = opts.webDistDir ? resolve11(opts.webDistDir) : join42(webRoot, "dist");
52749
53286
  startupPhase = "Vite";
52750
53287
  if (opts.dev) {
52751
53288
  try {
@@ -52821,7 +53358,7 @@ async function startServer(opts = {}) {
52821
53358
  if (vite) {
52822
53359
  vite.middlewares(req, res, () => {
52823
53360
  try {
52824
- const html = readFileSync28(join41(webRoot, "index.html"), "utf8");
53361
+ const html = readFileSync29(join42(webRoot, "index.html"), "utf8");
52825
53362
  void vite.transformIndexHtml(url, html).then((out) => {
52826
53363
  res.writeHead(200, { "content-type": "text/html" });
52827
53364
  res.end(out);
@@ -52975,6 +53512,8 @@ var init_index = __esm({
52975
53512
  init_app_socket();
52976
53513
  init_shutdown();
52977
53514
  init_project();
53515
+ init_local_file();
53516
+ init_local_file_watch();
52978
53517
  init_project_launch();
52979
53518
  init_frizz_paths();
52980
53519
  init_project_identity();
@@ -53026,7 +53565,7 @@ var init_index = __esm({
53026
53565
  }
53027
53566
  };
53028
53567
  isApiUrl = (url) => url === FRIZZ_ROUTE_PREFIX || url.startsWith(`${FRIZZ_ROUTE_PREFIX}/`);
53029
- serverLockPathFor = (project) => join41(project.stateDir, "server.lock");
53568
+ serverLockPathFor = (project) => join42(project.stateDir, "server.lock");
53030
53569
  MIME = {
53031
53570
  ".html": "text/html",
53032
53571
  ".js": "text/javascript",
@@ -53045,7 +53584,7 @@ init_project_launch();
53045
53584
  init_shutdown();
53046
53585
  init_logging();
53047
53586
  import { existsSync as existsSync16 } from "node:fs";
53048
- import { join as join42 } from "node:path";
53587
+ import { join as join43 } from "node:path";
53049
53588
  process.on("uncaughtException", (error) => {
53050
53589
  log.error("dev-child", `uncaught exception: ${error instanceof Error ? error.stack ?? error.message : error}`);
53051
53590
  process.exit(1);
@@ -53085,7 +53624,7 @@ try {
53085
53624
  ];
53086
53625
  if (!existsSync16(stableWebDist)) throw new Error("stable artifact launch is missing its verified web directory");
53087
53626
  for (const [name, directory, requiredFile] of required) {
53088
- if (!directory || !existsSync16(join42(directory, requiredFile)))
53627
+ if (!directory || !existsSync16(join43(directory, requiredFile)))
53089
53628
  throw new Error(`stable artifact launch is missing verified ${name}`);
53090
53629
  }
53091
53630
  }