frizz 0.9.0 → 0.10.0

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 +869 -333
  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-XNji7D_c.js} +1 -1
  8. package/web-dist/assets/{abnfDiagram-VRR7QNED-DNBrcJfV.js → abnfDiagram-VRR7QNED-gPeb-B7g.js} +1 -1
  9. package/web-dist/assets/architecture-TIHT7OUA-BsgSgexh.js +1 -0
  10. package/web-dist/assets/{architectureDiagram-ZJ3FMSHR-SXVydqly.js → architectureDiagram-ZJ3FMSHR-DV69ktbl.js} +1 -1
  11. package/web-dist/assets/{blockDiagram-677ZJIJ3-CgI1GSdx.js → blockDiagram-677ZJIJ3-D8vw5eim.js} +1 -1
  12. package/web-dist/assets/{c4Diagram-LMCZKHZV-KDmJm2tG.js → c4Diagram-LMCZKHZV--9PLGn-L.js} +1 -1
  13. package/web-dist/assets/channel-ClLnncxh.js +1 -0
  14. package/web-dist/assets/{chunk-32BRIVSS-j3eOjq4Z.js → chunk-32BRIVSS-D4BSk0FZ.js} +1 -1
  15. package/web-dist/assets/{chunk-52WLFC77-DZHfa78H.js → chunk-52WLFC77-WNcmTkcR.js} +1 -1
  16. package/web-dist/assets/{chunk-C7G6YPKG-CiB5uo24.js → chunk-C7G6YPKG-BDlcLa5W.js} +1 -1
  17. package/web-dist/assets/{chunk-EX3LRPZG-B35Inyjz.js → chunk-EX3LRPZG-CFnqFt7s.js} +1 -1
  18. package/web-dist/assets/{chunk-FWX5IMBZ-D2sRvM5U.js → chunk-FWX5IMBZ-Ci9LgOOg.js} +2 -2
  19. package/web-dist/assets/{chunk-HOUHSVGY-D247PDv6.js → chunk-HOUHSVGY-BTD_X5OP.js} +1 -1
  20. package/web-dist/assets/{chunk-ICXQ74PX-Bu3Tb6Ec.js → chunk-ICXQ74PX-D2Y3Ys5K.js} +1 -1
  21. package/web-dist/assets/{chunk-MOJQB5TN-C_lQy4on.js → chunk-MOJQB5TN-C-GDt7vC.js} +1 -1
  22. package/web-dist/assets/{chunk-OGEWGWER-DU5ZBedR.js → chunk-OGEWGWER-DvnPoUnz.js} +1 -1
  23. package/web-dist/assets/{chunk-PUDLZKDR-DWRT7ztS.js → chunk-PUDLZKDR-C3AymM-j.js} +1 -1
  24. package/web-dist/assets/{chunk-Q4XR5HBZ-CCX4AK_n.js → chunk-Q4XR5HBZ-DmyBNAl6.js} +1 -1
  25. package/web-dist/assets/{chunk-V7JOEXUC-VbixbkDG.js → chunk-V7JOEXUC-DTuPUeRa.js} +1 -1
  26. package/web-dist/assets/{chunk-VAUOI2AC-PJvChxMA.js → chunk-VAUOI2AC-CcW8PviB.js} +1 -1
  27. package/web-dist/assets/{chunk-VR4S4FIN-BjsMfxj8.js → chunk-VR4S4FIN-0tm6n83G.js} +1 -1
  28. package/web-dist/assets/{chunk-WYO6CB5R-Bcb97r3w.js → chunk-WYO6CB5R-CH3ADqeI.js} +1 -1
  29. package/web-dist/assets/{chunk-ZGVPDNZ5-CT2sZlhT.js → chunk-ZGVPDNZ5-BHFnlpXH.js} +1 -1
  30. package/web-dist/assets/classDiagram-OUVF2IWQ-CwLnPgud.js +1 -0
  31. package/web-dist/assets/classDiagram-v2-EOCWNBFH-CwLnPgud.js +1 -0
  32. package/web-dist/assets/{cynefin-VYW2F7L2-dUpO4u0w.js → cynefin-VYW2F7L2-DtFCLOIN.js} +1 -1
  33. package/web-dist/assets/{cynefinDiagram-TSTJHNR4-BQZ3UIsx.js → cynefinDiagram-TSTJHNR4-llFTHp2q.js} +1 -1
  34. package/web-dist/assets/{dagre-VKFMJZFB-K-oO8w51.js → dagre-VKFMJZFB-CvBRVl_k.js} +1 -1
  35. package/web-dist/assets/{diagram-FQU43EPY-Cr47GJ_S.js → diagram-FQU43EPY-D1QXo6ri.js} +1 -1
  36. package/web-dist/assets/{diagram-G47NLZAW-BbWzSkoZ.js → diagram-G47NLZAW-2pZew-Bk.js} +1 -1
  37. package/web-dist/assets/{diagram-NH7WQ7WH-BGJJBgGM.js → diagram-NH7WQ7WH-Ch9JRwGN.js} +1 -1
  38. package/web-dist/assets/{diagram-OA4YK3LP-2W7aunEN.js → diagram-OA4YK3LP-BIbrLf02.js} +1 -1
  39. package/web-dist/assets/{diagram-WEI45ONY-D-4ITFI6.js → diagram-WEI45ONY-D3KtL2s-.js} +1 -1
  40. package/web-dist/assets/{ebnfDiagram-CCIWWBDH-CxUcx103.js → ebnfDiagram-CCIWWBDH-D829sKG_.js} +1 -1
  41. package/web-dist/assets/{erDiagram-Q63AITRT-Cf_5SWAE.js → erDiagram-Q63AITRT-Cj3Grovg.js} +1 -1
  42. package/web-dist/assets/eventmodeling-45OFAUF4-CzZyv3HA.js +1 -0
  43. package/web-dist/assets/flowDiagram-23GEKE2U-rQ7PRoGL.js +1 -0
  44. package/web-dist/assets/{ganttDiagram-NO4QXBWP-CVZI8Ywa.js → ganttDiagram-NO4QXBWP-CGHaXF-g.js} +1 -1
  45. package/web-dist/assets/{gitGraph-TEB2WS4Q-BsiGpR9l.js → gitGraph-TEB2WS4Q-BbxeaotX.js} +1 -1
  46. package/web-dist/assets/{gitGraphDiagram-IHSO6WYX-DVym_4l1.js → gitGraphDiagram-IHSO6WYX-C4WHE0cI.js} +1 -1
  47. package/web-dist/assets/index-BpCFdr7Z.css +1 -0
  48. package/web-dist/assets/index-CiaKj-0V.js +506 -0
  49. package/web-dist/assets/{info-DKCQHKI2-DYcsKFv-.js → info-DKCQHKI2-3fKpn7Qr.js} +1 -1
  50. package/web-dist/assets/{infoDiagram-FWYZ7A6U-hjVxlrTT.js → infoDiagram-FWYZ7A6U-CDQl4JMv.js} +1 -1
  51. package/web-dist/assets/{ishikawaDiagram-FXEZZL3T-B0DKOPSS.js → ishikawaDiagram-FXEZZL3T-CDKjMW-u.js} +1 -1
  52. package/web-dist/assets/{journeyDiagram-5HDEW3XC-Cohibc5O.js → journeyDiagram-5HDEW3XC-BOsabu1Z.js} +1 -1
  53. package/web-dist/assets/{kanban-definition-HUTT4EX6-_6Zr31vM.js → kanban-definition-HUTT4EX6-KTuh-6eN.js} +1 -1
  54. package/web-dist/assets/{line-B14EZI6M.js → line-KaxbYSN3.js} +1 -1
  55. package/web-dist/assets/{mermaid-parser.core-DHkc6cqw.js → mermaid-parser.core-B53huyyd.js} +3 -3
  56. package/web-dist/assets/{mermaid.core-DuTvdsKf.js → mermaid.core-Bz6B5ouC.js} +3 -3
  57. package/web-dist/assets/{mindmap-definition-LN4V7U3C-DPRF9pW2.js → mindmap-definition-LN4V7U3C-Bb7T7B3a.js} +1 -1
  58. package/web-dist/assets/{packet-7NZHBO7P-Ci8oCcGM.js → packet-7NZHBO7P-w3lynC8r.js} +1 -1
  59. package/web-dist/assets/{pegDiagram-2B236MQR-Cr3ctC6e.js → pegDiagram-2B236MQR-nQ7c2Z03.js} +1 -1
  60. package/web-dist/assets/{pie-RZYD4A2V-EHgA307y.js → pie-RZYD4A2V-B3Yko5Wb.js} +1 -1
  61. package/web-dist/assets/{pieDiagram-ENE6RG2P-UW6Bmu6F.js → pieDiagram-ENE6RG2P-CN4Lp5HG.js} +1 -1
  62. package/web-dist/assets/{quadrantDiagram-ABIIQ3AL-D6tAsRj9.js → quadrantDiagram-ABIIQ3AL-3QTB92dH.js} +1 -1
  63. package/web-dist/assets/{radar-I7S5WNFK-CAQqojcd.js → radar-I7S5WNFK-8ZsXt0FI.js} +1 -1
  64. package/web-dist/assets/{railroad-3IZDKUUU-BZsAOTrm.js → railroad-3IZDKUUU-C4IKxzEZ.js} +1 -1
  65. package/web-dist/assets/railroad-abnf-AHOZXSZD-g2si5iUv.js +1 -0
  66. package/web-dist/assets/railroad-ebnf-EBAXGLYW-BikkLG6y.js +1 -0
  67. package/web-dist/assets/railroad-peg-LSFZ7HO6-Dv4rDehz.js +1 -0
  68. package/web-dist/assets/{railroadDiagram-RFXS5EU6-bSpff55H.js → railroadDiagram-RFXS5EU6-DrGnmjpD.js} +1 -1
  69. package/web-dist/assets/{requirementDiagram-TGXJPOKE-DIAYvXoB.js → requirementDiagram-TGXJPOKE-Bh9XNuxa.js} +1 -1
  70. package/web-dist/assets/{sankeyDiagram-HTMAVEWB-DEnsSz3F.js → sankeyDiagram-HTMAVEWB-CXy3ra5-.js} +1 -1
  71. package/web-dist/assets/{sequenceDiagram-DBY2YBRQ-DsG7eb9R.js → sequenceDiagram-DBY2YBRQ-B7ESCT52.js} +1 -1
  72. package/web-dist/assets/{stateDiagram-2N3HPSRC-DT3XNwjo.js → stateDiagram-2N3HPSRC-DFr9k5Xv.js} +1 -1
  73. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-BQUd0MEa.js +1 -0
  74. package/web-dist/assets/{swimlanes-5IMT3BWC-B5hZwgRo.js → swimlanes-5IMT3BWC-B2RKoz1q.js} +1 -1
  75. package/web-dist/assets/swimlanesDiagram-G3AALYLV-GWn4zXxj.js +8 -0
  76. package/web-dist/assets/{timeline-definition-FHXFAJF6-BVTsjkfu.js → timeline-definition-FHXFAJF6-KpTqaG5g.js} +1 -1
  77. package/web-dist/assets/{treeView-QDETBFTQ-M6HmP4bB.js → treeView-QDETBFTQ-oB1a6ikv.js} +1 -1
  78. package/web-dist/assets/{treemap-6X3UGDF4-C60TppfC.js → treemap-6X3UGDF4-DO8OvvZg.js} +1 -1
  79. package/web-dist/assets/{vennDiagram-L72KCM5P-Dv1y3g4S.js → vennDiagram-L72KCM5P-BgTjK4_q.js} +1 -1
  80. package/web-dist/assets/{wardley-OPB4EBWU-D0lMJMWt.js → wardley-OPB4EBWU-CGyjn939.js} +1 -1
  81. package/web-dist/assets/{wardleyDiagram-EHGQE667-B1E5NtZY.js → wardleyDiagram-EHGQE667-CM_HxruS.js} +1 -1
  82. package/web-dist/assets/{xychartDiagram-FW5EYKEG-oA91YOvh.js → xychartDiagram-FW5EYKEG-vamHu0jN.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"
@@ -19498,6 +19589,7 @@ function createStorage(source, projectId) {
19498
19589
  // never be written without a deadline to fire it.
19499
19590
  setSnoozedUntil: (slug, until, prompt = null) => void snoozedUntilStmt.run(until, until === null ? null : prompt, slug),
19500
19591
  setSnoozedUntilIfCurrent: (slug, sessionId, generation, until) => snoozedUntilIfCurrentStmt.run(until, slug, sessionId, generation).changes === 1,
19592
+ setPinnedAt: (slug, at) => void pinnedAtStmt.run(at, slug),
19501
19593
  setBgSnoozeRestedAtIfCurrent: (slug, sessionId, generation, restedAt) => bgSnoozeRestedAtIfCurrentStmt.run(restedAt, slug, sessionId, generation).changes === 1,
19502
19594
  setRecurringPromptIfCurrent: (slug, sessionId, generation, write) => recurringStmt.run(...recurringArgs(write), slug, sessionId, generation).changes === 1,
19503
19595
  setRecurringPromptBySlug: (slug, write) => recurringBySlugStmt.run(...recurringArgs(write), slug).changes === 1,
@@ -19505,7 +19597,7 @@ function createStorage(source, projectId) {
19505
19597
  resetSignoffNudges: (slug) => void resetNudgesStmt.run(slug),
19506
19598
  countParkBump: (slug, anchor) => void countParkBumpStmt.run(anchor, slug),
19507
19599
  resetParkBumps: (slug) => void resetParkBumpsStmt.run(slug),
19508
- armPrWatch: (watch) => void armPrWatchStmt.run(watch),
19600
+ armPrWatch: (watch2) => void armPrWatchStmt.run(watch2),
19509
19601
  listPrWatches: (slug, opts) => (opts?.armedOnly ? armedPrWatchesBySlugStmt : prWatchesBySlugStmt).all(slug),
19510
19602
  getPrWatch: (id) => prWatchByIdStmt.get(id),
19511
19603
  armedPrWatches: () => armedPrWatchesStmt.all(),
@@ -19782,6 +19874,7 @@ var init_storage = __esm({
19782
19874
  snoozed_until TEXT,
19783
19875
  snooze_prompt TEXT,
19784
19876
  bg_snooze_rested_at TEXT,
19877
+ pinned_at TEXT,
19785
19878
  meta TEXT,
19786
19879
  seen_at TEXT,
19787
19880
  transcript_id TEXT,
@@ -20196,6 +20289,9 @@ var init_settings = __esm({
20196
20289
  // 500K tokens: half the 1M window every Claude worker is dispatched with. See the schema for why a
20197
20290
  // worker left to compact at 1M is the biggest single reason a Frizz thread out-spends the TUI.
20198
20291
  autoCompactWindow: 5e5,
20292
+ // "auto" = the CLI's own tier (1h on a subscription). See the schema and claudePromptCacheEnv for
20293
+ // when "5m" is the cheaper choice.
20294
+ promptCacheTtl: "auto",
20199
20295
  // Hidden until asked for — see the schema for why.
20200
20296
  projectRail: false
20201
20297
  });
@@ -20345,13 +20441,13 @@ function discoverTranscriptDir(logDir, sessionId, memo3 = strandedLogDirs) {
20345
20441
  let best;
20346
20442
  for (const dir of memo3) best = better(best, dir);
20347
20443
  if (best) return best.dir;
20348
- let entries;
20444
+ let entries2;
20349
20445
  try {
20350
- entries = readdirSync3(dirname7(logDir), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
20446
+ entries2 = readdirSync3(dirname7(logDir), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
20351
20447
  } catch {
20352
20448
  return void 0;
20353
20449
  }
20354
- for (const entry of entries) best = better(best, join11(dirname7(logDir), entry));
20450
+ for (const entry of entries2) best = better(best, join11(dirname7(logDir), entry));
20355
20451
  if (!best) return void 0;
20356
20452
  memo3.add(best.dir);
20357
20453
  return best.dir;
@@ -21272,10 +21368,10 @@ function boundedJson(value, label, state, depth) {
21272
21368
  return value.map((entry, index) => boundedJson(entry, `${label}[${index}]`, state, depth + 1));
21273
21369
  }
21274
21370
  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`);
21371
+ const entries2 = Object.entries(value);
21372
+ if (entries2.length > 256) throw new ClaudeAgentSdkProtocolError(`${label} contains too many object fields`);
21277
21373
  const output = {};
21278
- for (const [key, entry] of entries) {
21374
+ for (const [key, entry] of entries2) {
21279
21375
  if (RESERVED_KEYS.has(key) || utf8Bytes(key) > 256 || UNSAFE_TEXT2.test(key)) {
21280
21376
  UNSAFE_TEXT2.lastIndex = 0;
21281
21377
  throw new ClaudeAgentSdkProtocolError(`${label} contains an invalid object key`);
@@ -21414,10 +21510,10 @@ var init_claude_broker_diagnostics = __esm({
21414
21510
  // packages/server/src/detached-daemons.ts
21415
21511
  import { existsSync as existsSync4 } from "node:fs";
21416
21512
  import { fileURLToPath } from "node:url";
21417
- function resolveDetachedDaemonEntry(moduleUrl, basename13) {
21418
- const candidates = [`./${basename13}.js`, `./${basename13}.ts`].map((rel) => fileURLToPath(new URL(rel, moduleUrl)));
21513
+ function resolveDetachedDaemonEntry(moduleUrl, basename14) {
21514
+ const candidates = [`./${basename14}.js`, `./${basename14}.ts`].map((rel) => fileURLToPath(new URL(rel, moduleUrl)));
21419
21515
  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 ")}`);
21516
+ if (!found) throw new Error(`frizz is missing the ${basename14} entry \u2014 looked for ${candidates.join(" and ")}`);
21421
21517
  return found;
21422
21518
  }
21423
21519
  var init_detached_daemons = __esm({
@@ -22537,8 +22633,8 @@ function createTailStateCache(scope, schema = foldSchemaDigest()) {
22537
22633
  }
22538
22634
  return map;
22539
22635
  },
22540
- put(entries) {
22541
- if (!usable || !upsert || entries.length === 0) return;
22636
+ put(entries2) {
22637
+ if (!usable || !upsert || entries2.length === 0) return;
22542
22638
  try {
22543
22639
  const write = db.transaction((batch) => {
22544
22640
  for (const entry of batch) {
@@ -22559,7 +22655,7 @@ function createTailStateCache(scope, schema = foldSchemaDigest()) {
22559
22655
  });
22560
22656
  }
22561
22657
  });
22562
- write(entries);
22658
+ write(entries2);
22563
22659
  } catch {
22564
22660
  }
22565
22661
  },
@@ -24132,7 +24228,7 @@ ${ask}`;
24132
24228
  }
24133
24229
  }
24134
24230
  if (cacheDirty.size === 0) return;
24135
- const entries = [];
24231
+ const entries2 = [];
24136
24232
  try {
24137
24233
  for (const key of cacheDirty) {
24138
24234
  const foreign = key.startsWith("foreign:");
@@ -24141,12 +24237,12 @@ ${ask}`;
24141
24237
  const row = foreign ? null : deps.storage.getSession(key) ?? null;
24142
24238
  if (!foreign && !row) continue;
24143
24239
  const entry = cacheSnapshot(state, row);
24144
- if (entry) entries.push(entry);
24240
+ if (entry) entries2.push(entry);
24145
24241
  }
24146
24242
  } catch {
24147
24243
  }
24148
24244
  cacheDirty.clear();
24149
- tailCache.put(entries);
24245
+ tailCache.put(entries2);
24150
24246
  }
24151
24247
  function consume(state, backend, onLine) {
24152
24248
  let size;
@@ -25140,9 +25236,9 @@ function stringifyOutput(output) {
25140
25236
  }
25141
25237
  function collectRollouts(dir, out, budget) {
25142
25238
  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);
25239
+ const entries2 = safeReaddir(dir);
25240
+ const dirs = entries2.filter((e) => e.isDirectory()).sort(descByName);
25241
+ const files = entries2.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).sort(descByName);
25146
25242
  for (const d of dirs) {
25147
25243
  if (budget.n <= 0) return;
25148
25244
  collectRollouts(join19(dir, d.name), out, budget);
@@ -28715,7 +28811,7 @@ var require_stream = __commonJS({
28715
28811
  terminateOnDestroy = false;
28716
28812
  duplex.destroy(err);
28717
28813
  });
28718
- ws.once("close", function close() {
28814
+ ws.once("close", function close2() {
28719
28815
  if (duplex.destroyed) return;
28720
28816
  duplex.push(null);
28721
28817
  });
@@ -28730,7 +28826,7 @@ var require_stream = __commonJS({
28730
28826
  called = true;
28731
28827
  callback(err2);
28732
28828
  });
28733
- ws.once("close", function close() {
28829
+ ws.once("close", function close2() {
28734
28830
  if (!called) callback(err);
28735
28831
  process.nextTick(emitClose, duplex);
28736
28832
  });
@@ -29262,13 +29358,18 @@ function claudeCompactionEnv(settings) {
29262
29358
  if (window === void 0 || !Number.isInteger(window) || window <= 0) return {};
29263
29359
  return { [CLAUDE_AUTO_COMPACT_WINDOW_ENV]: String(window) };
29264
29360
  }
29361
+ function claudePromptCacheEnv(settings) {
29362
+ const ttl = settings?.promptCacheTtl;
29363
+ if (ttl !== "5m" && ttl !== "1h") return {};
29364
+ return { [CLAUDE_PROMPT_CACHE_TTL_ENV]: ttl, [CLAUDE_SUBAGENT_PROMPT_CACHE_TTL_ENV]: ttl };
29365
+ }
29265
29366
  function claudeCompactionWindowOf(env) {
29266
29367
  const raw2 = env?.[CLAUDE_AUTO_COMPACT_WINDOW_ENV];
29267
29368
  if (raw2 === void 0) return void 0;
29268
29369
  const window = Number(raw2);
29269
29370
  return Number.isInteger(window) && window > 0 ? window : void 0;
29270
29371
  }
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;
29372
+ 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
29373
  var init_types2 = __esm({
29273
29374
  "packages/server/src/backend/types.ts"() {
29274
29375
  "use strict";
@@ -29286,6 +29387,8 @@ var init_types2 = __esm({
29286
29387
  BASH_MAX_TIMEOUT_MS: String(24 * 60 * 60 * 1e3)
29287
29388
  };
29288
29389
  CLAUDE_AUTO_COMPACT_WINDOW_ENV = "CLAUDE_CODE_AUTO_COMPACT_WINDOW";
29390
+ CLAUDE_PROMPT_CACHE_TTL_ENV = "CLAUDE_CODE_PROMPT_CACHE_TTL";
29391
+ CLAUDE_SUBAGENT_PROMPT_CACHE_TTL_ENV = "CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL";
29289
29392
  WORKER_DISALLOWED_TOOLS = ["AskUserQuestion"];
29290
29393
  }
29291
29394
  });
@@ -29294,18 +29397,18 @@ var init_types2 = __esm({
29294
29397
  function tomlString(value) {
29295
29398
  return JSON.stringify(value);
29296
29399
  }
29297
- function inlineTable(entries) {
29298
- return `{${entries.map(([k, v]) => `${k}=${v}`).join(",")}}`;
29400
+ function inlineTable(entries2) {
29401
+ return `{${entries2.map(([k, v]) => `${k}=${v}`).join(",")}}`;
29299
29402
  }
29300
29403
  function serverTable(command, args, env) {
29301
- const entries = [
29404
+ const entries2 = [
29302
29405
  ["command", tomlString(command)],
29303
29406
  ["args", `[${args.map(tomlString).join(",")}]`]
29304
29407
  ];
29305
29408
  if (env && Object.keys(env).length > 0) {
29306
- entries.push(["env", inlineTable(Object.entries(env).map(([k, v]) => [k, tomlString(v)]))]);
29409
+ entries2.push(["env", inlineTable(Object.entries(env).map(([k, v]) => [k, tomlString(v)]))]);
29307
29410
  }
29308
- return inlineTable(entries);
29411
+ return inlineTable(entries2);
29309
29412
  }
29310
29413
  function codexMcpConfigArgs(frizzMcp, nodeBin = process.execPath) {
29311
29414
  const args = [];
@@ -30413,10 +30516,10 @@ function mcpFields(raw2) {
30413
30516
  if (schema.type !== "object" || schema.properties === null || typeof schema.properties !== "object" || Array.isArray(schema.properties)) {
30414
30517
  throw new RpcProtocolError(-32602, "MCP elicitation schema must describe object properties");
30415
30518
  }
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");
30519
+ const entries2 = Object.entries(schema.properties).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
30520
+ if (entries2.length > 32) throw new RpcProtocolError(-32602, "MCP elicitation has too many fields");
30418
30521
  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)));
30522
+ return entries2.map(([id, field]) => mcpField(id, field, required.has(id)));
30420
30523
  }
30421
30524
  function userInputFields(questions) {
30422
30525
  return questions.map((question) => {
@@ -32841,14 +32944,14 @@ function sessionsDir2(codexHome) {
32841
32944
  }
32842
32945
  function newestRollouts(dir, out, budget) {
32843
32946
  if (budget.n <= 0) return;
32844
- let entries;
32947
+ let entries2;
32845
32948
  try {
32846
- entries = readdirSync7(dir, { withFileTypes: true });
32949
+ entries2 = readdirSync7(dir, { withFileTypes: true });
32847
32950
  } catch {
32848
32951
  return;
32849
32952
  }
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);
32953
+ const dirs = entries2.filter((e) => e.isDirectory()).map((e) => e.name).sort(descByName2);
32954
+ const files = entries2.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).map((e) => e.name).sort(descByName2);
32852
32955
  for (const d of dirs) {
32853
32956
  if (budget.n <= 0) return;
32854
32957
  newestRollouts(join22(dir, d), out, budget);
@@ -33607,7 +33710,7 @@ function readAwaitingPark(hints) {
33607
33710
  if (isAwaitingItemKind(h.kind)) {
33608
33711
  if (value) items.push({ kind: h.kind, value });
33609
33712
  } else if (h.kind === "for") {
33610
- forMs = parseAwaitingDuration(value);
33713
+ forMs = parseAwaitingDurationRaw(value);
33611
33714
  }
33612
33715
  }
33613
33716
  return { items, forMs };
@@ -33625,9 +33728,13 @@ function parkIsHonoured(park, live) {
33625
33728
  if (park.forMs === null) return false;
33626
33729
  return unaccountedItems(park.items, live).length === 0;
33627
33730
  }
33731
+ function parkForMaxMs(park) {
33732
+ if (park.items.length === 0) return AWAITING_FOR_MAX_MS;
33733
+ return park.items.every((i) => i.kind === "pr") ? PR_WATCH_FOR_MAX_MS : AWAITING_FOR_MAX_MS;
33734
+ }
33628
33735
  function parkExpiresAt(park, fenceAtMs) {
33629
33736
  if (park.forMs === null || !Number.isFinite(fenceAtMs)) return null;
33630
- return fenceAtMs + Math.min(park.forMs, AWAITING_FOR_MAX_MS);
33737
+ return fenceAtMs + Math.min(park.forMs, parkForMaxMs(park));
33631
33738
  }
33632
33739
  function readGithubStatusBook(raw2) {
33633
33740
  if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) return {};
@@ -33657,18 +33764,148 @@ var init_awaiting = __esm({
33657
33764
  }
33658
33765
  });
33659
33766
 
33767
+ // packages/server/src/backend/project-mcp-servers.ts
33768
+ import { readFileSync as readFileSync16 } from "node:fs";
33769
+ import { homedir as homedir10 } from "node:os";
33770
+ import { join as join25 } from "node:path";
33771
+ function readJsonObject(path) {
33772
+ try {
33773
+ const parsed = JSON.parse(readFileSync16(path, "utf8"));
33774
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
33775
+ } catch {
33776
+ return void 0;
33777
+ }
33778
+ }
33779
+ function objectField(source, key) {
33780
+ const value = source?.[key];
33781
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
33782
+ }
33783
+ function stringList(source, key) {
33784
+ const value = source?.[key];
33785
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
33786
+ }
33787
+ function stringMap(value) {
33788
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
33789
+ const out = {};
33790
+ for (const [key, entry] of Object.entries(value)) {
33791
+ if (typeof entry === "string") out[key] = entry;
33792
+ }
33793
+ return out;
33794
+ }
33795
+ function expandMcpEnvRefs(value, env) {
33796
+ let unresolved = false;
33797
+ const out = value.replace(ENV_REF, (_match, name, fallback) => {
33798
+ const resolved = env[name];
33799
+ if (resolved !== void 0) return resolved;
33800
+ if (fallback !== void 0) return fallback;
33801
+ unresolved = true;
33802
+ return "";
33803
+ });
33804
+ return unresolved ? void 0 : out;
33805
+ }
33806
+ function expandMap(map, env) {
33807
+ if (!map) return void 0;
33808
+ const out = {};
33809
+ for (const [key, value] of Object.entries(map)) {
33810
+ const expanded = expandMcpEnvRefs(value, env);
33811
+ if (expanded === void 0) return null;
33812
+ out[key] = expanded;
33813
+ }
33814
+ return out;
33815
+ }
33816
+ function normalizeMcpServer(raw2, env) {
33817
+ if (raw2 === null || typeof raw2 !== "object" || Array.isArray(raw2)) return void 0;
33818
+ const record = raw2;
33819
+ const type = typeof record.type === "string" ? record.type : void 0;
33820
+ if (type === "http" || type === "sse") {
33821
+ if (typeof record.url !== "string") return void 0;
33822
+ const url = expandMcpEnvRefs(record.url, env);
33823
+ const headers = expandMap(stringMap(record.headers), env);
33824
+ if (url === void 0 || headers === null) return void 0;
33825
+ return { type, url, ...headers ? { headers } : {} };
33826
+ }
33827
+ if (type !== void 0 && type !== "stdio") return void 0;
33828
+ if (typeof record.command !== "string") return void 0;
33829
+ const command = expandMcpEnvRefs(record.command, env);
33830
+ if (command === void 0) return void 0;
33831
+ const args = [];
33832
+ for (const entry of Array.isArray(record.args) ? record.args : []) {
33833
+ if (typeof entry !== "string") return void 0;
33834
+ const expanded = expandMcpEnvRefs(entry, env);
33835
+ if (expanded === void 0) return void 0;
33836
+ args.push(expanded);
33837
+ }
33838
+ const serverEnv = expandMap(stringMap(record.env), env);
33839
+ if (serverEnv === null) return void 0;
33840
+ return { command, ...args.length ? { args } : {}, ...serverEnv ? { env: serverEnv } : {} };
33841
+ }
33842
+ function collectApproval(layers) {
33843
+ const approval = { all: false, enabled: /* @__PURE__ */ new Set(), disabled: /* @__PURE__ */ new Set() };
33844
+ for (const layer of layers) {
33845
+ if (layer?.enableAllProjectMcpServers === true) approval.all = true;
33846
+ for (const name of stringList(layer, "enabledMcpjsonServers")) approval.enabled.add(name);
33847
+ for (const name of stringList(layer, "disabledMcpjsonServers")) approval.disabled.add(name);
33848
+ }
33849
+ return approval;
33850
+ }
33851
+ function userScopeRemoteServers(account, cwd, env) {
33852
+ const out = {};
33853
+ const maps = [objectField(account, "mcpServers"), objectField(objectField(objectField(account, "projects"), cwd), "mcpServers")];
33854
+ for (const map of maps) {
33855
+ for (const [name, raw2] of Object.entries(map ?? {})) {
33856
+ const server = normalizeMcpServer(raw2, env);
33857
+ if (server && server.type !== void 0 && server.type !== "stdio") out[name] = server;
33858
+ }
33859
+ }
33860
+ return out;
33861
+ }
33862
+ function projectMcpServers(cwd, options = {}) {
33863
+ const env = options.env ?? process.env;
33864
+ const home = options.home ?? homedir10();
33865
+ const configOverride = options.claudeConfigDir ?? (env.CLAUDE_CONFIG_DIR?.trim() ? env.CLAUDE_CONFIG_DIR.trim() : void 0);
33866
+ const accountFile = configOverride ? join25(configOverride, ".claude.json") : join25(home, ".claude.json");
33867
+ const userSettingsFile = join25(configOverride ?? join25(home, ".claude"), "settings.json");
33868
+ const account = readJsonObject(accountFile);
33869
+ const out = userScopeRemoteServers(account, cwd, env);
33870
+ const declared = objectField(readJsonObject(join25(cwd, ".mcp.json")), "mcpServers");
33871
+ if (!declared) return out;
33872
+ const approval = collectApproval([
33873
+ readJsonObject(join25(cwd, ".claude", "settings.json")),
33874
+ readJsonObject(join25(cwd, ".claude", "settings.local.json")),
33875
+ readJsonObject(userSettingsFile),
33876
+ objectField(objectField(account, "projects"), cwd)
33877
+ ]);
33878
+ for (const [name, raw2] of Object.entries(declared)) {
33879
+ if (approval.disabled.has(name)) continue;
33880
+ if (!approval.all && !approval.enabled.has(name)) continue;
33881
+ const server = normalizeMcpServer(raw2, env);
33882
+ if (server) out[name] = server;
33883
+ }
33884
+ return out;
33885
+ }
33886
+ function workerMcpServers(project, frizz) {
33887
+ return { ...project ?? {}, ...frizz ?? {} };
33888
+ }
33889
+ var ENV_REF;
33890
+ var init_project_mcp_servers = __esm({
33891
+ "packages/server/src/backend/project-mcp-servers.ts"() {
33892
+ "use strict";
33893
+ ENV_REF = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
33894
+ }
33895
+ });
33896
+
33660
33897
  // packages/server/src/worker-plugin-dir.ts
33661
33898
  import { existsSync as existsSync8 } from "node:fs";
33662
- import { dirname as dirname10, join as join25 } from "node:path";
33899
+ import { dirname as dirname10, join as join26 } from "node:path";
33663
33900
  import { fileURLToPath as fileURLToPath2 } from "node:url";
33664
33901
  function resolveWorkerPluginDir(moduleUrl = import.meta.url, env = process.env) {
33665
33902
  const override = env.FRIZZ_WORKER_PLUGIN_DIR;
33666
- if (override && existsSync8(join25(override, ".claude-plugin", "plugin.json")))
33903
+ if (override && existsSync8(join26(override, ".claude-plugin", "plugin.json")))
33667
33904
  return override;
33668
33905
  let current = dirname10(fileURLToPath2(moduleUrl));
33669
33906
  for (; ; ) {
33670
- const candidate = join25(current, "cc-worker");
33671
- if (existsSync8(join25(candidate, ".claude-plugin", "plugin.json"))) return candidate;
33907
+ const candidate = join26(current, "cc-worker");
33908
+ if (existsSync8(join26(candidate, ".claude-plugin", "plugin.json"))) return candidate;
33672
33909
  const parent = dirname10(current);
33673
33910
  if (parent === current) return void 0;
33674
33911
  current = parent;
@@ -33697,6 +33934,7 @@ function buildWorkerPrompt(kind = "claude", opts = {}) {
33697
33934
  AGENT_COMPLETION,
33698
33935
  VISUAL_EVIDENCE,
33699
33936
  QUALITY_BAR,
33937
+ TOKEN_ECONOMY,
33700
33938
  QUESTIONS,
33701
33939
  STOP_CRITERION,
33702
33940
  lean ? null : TRIVIAL_PROMPTS
@@ -33706,7 +33944,7 @@ function buildWorkerPrompt(kind = "claude", opts = {}) {
33706
33944
  out = out.replaceAll("{{FRIZZ_MONITORS_DIR}}", opts.monitorsDir?.trim() || MONITORS_DIR_FALLBACK);
33707
33945
  return out;
33708
33946
  }
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;
33947
+ 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
33948
  var init_workerPrompt = __esm({
33711
33949
  "packages/server/src/workerPrompt.ts"() {
33712
33950
  "use strict";
@@ -33914,7 +34152,14 @@ exactly ONE of them.
33914
34152
  Register FIRST: this line states the wait, it does not create one. That tool wakes you on CI
33915
34153
  turning green or red, and on every later review, approval or comment, for as long as the PR lives.
33916
34154
  - \`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.
34155
+ out frizz brings you back to re-check everything; re-parking is fine and uncapped. Capped at a day,
34156
+ except on a park naming ONLY \`prs:\`, where it runs to a year.
34157
+ - **A PULL REQUEST IN SOMEONE ELSE'S REPO TAKES MONTHS, SO ASK FOR MONTHS** \u2014 \`for: 180d\`, and give
34158
+ \`mcp__frizz__watch_pr\` the same. It moves on its maintainers' clock, not yours, so a short \`for:\`
34159
+ expires against a PR nothing has touched: a wake carrying no news, and a re-arm, once per expiry
34160
+ until somebody notices. Long is free \u2014 real activity wakes you the moment it lands either way, and
34161
+ the human snoozes or archives the thread when they want it gone. Hours are for a PR you control and
34162
+ a check you expect to settle today.
33918
34163
  - \`title:\` \u2014 OPTIONAL, the card's heading: **40 characters or fewer**, sentence case, naming THIS
33919
34164
  wait ("Fork-CI approval", "Nightly bench, arm 3 of 3"); longer is trimmed on a word boundary.
33920
34165
  Without it the card is headed "Awaiting", which is true of every park and specific to none.
@@ -34001,6 +34246,19 @@ reads when images are unavailable.`;
34001
34246
  formatter, a rename sweep) \u2014 run it, then name the files it touched in your handoff.
34002
34247
  - Ground every load-bearing claim in code, a command, or a doc you actually read \u2014 never memory. State
34003
34248
  plainly what failed, what you skipped, and what you could not verify.`;
34249
+ TOKEN_ECONOMY = `## Every message you send re-reads your whole context
34250
+
34251
+ The API call is the unit of cost, not the tokens you type: each message re-reads your entire context
34252
+ (150k\u2013450k tokens for most of an effort) whether it carries one tool call or ten.
34253
+
34254
+ - **Batch independent tool calls into ONE message.** List privately what you need next; then request
34255
+ every item that does not depend on another's result in the same response.
34256
+ - **One Bash call, several commands.** Chain the steps of one job with \`&&\` / \`;\` and print
34257
+ labelled sections. Use absolute paths; never spend a call on \`cd\` alone.
34258
+ - **Never poll from the turn.** A \`sleep\`-and-check loop is a full re-read every iteration. A
34259
+ background shell, a sub-agent or a registered watch wakes you when the thing is done.
34260
+ - **Finish the turn's whole job before you rest.** Every wake is a new turn on the full context, so
34261
+ do not rest on a step you could take now.`;
34004
34262
  QUESTIONS = `## Questions for the human
34005
34263
 
34006
34264
  You run under a dashboard, not a live chat, so your FINAL MESSAGE is the whole interface. Open with 2-4
@@ -34055,10 +34313,11 @@ fence is answerable only while its message is on screen, so a question written i
34055
34313
  the transcript scrolls or your context is compacted, while a registration is still there tomorrow. It
34056
34314
  takes \`questions\` \u2014 several at once, each with \`question\`, \`kind\` (\`question\` = pick one,
34057
34315
  \`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
34316
+ carrying a \`label\`, a \`description\` (one line for a simple fork, or FULL MARKDOWN over several
34317
+ lines \u2014 a list of consequences, a code block, the diff an option would produce, the message that would
34318
+ be posted; it renders inside the option, so the human sees it BEFORE choosing), an optional
34319
+ \`recommended\`, and optional \`followUps\`: questions that become live ONLY if that option is taken,
34320
+ 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
34321
  it carries no timeout and waits on a person, so keep doing everything that does not depend on the
34063
34322
  answer. The answer comes back as its own wake, restating what was asked. Withdraw one you no longer
34064
34323
  need with \`mcp__frizz__unask\`, above all when you work the answer out yourself. **ON A THREAD
@@ -34793,12 +35052,12 @@ var init_claude_broker_client = __esm({
34793
35052
  });
34794
35053
 
34795
35054
  // packages/server/src/backend/claude-permission-interactions.ts
34796
- import { homedir as homedir10 } from "node:os";
35055
+ import { homedir as homedir11 } from "node:os";
34797
35056
  function clip(text2, max) {
34798
35057
  return text2.length > max ? `${text2.slice(0, max - 1)}\u2026` : text2;
34799
35058
  }
34800
35059
  function tildePath(path) {
34801
- const home = homedir10();
35060
+ const home = homedir11();
34802
35061
  if (!path.startsWith(home)) return path;
34803
35062
  const rest = path.slice(home.length);
34804
35063
  return rest === "" ? "~" : rest.startsWith("/") ? `~${rest}` : path;
@@ -35044,7 +35303,7 @@ var init_claude_permission_interactions = __esm({
35044
35303
  import { randomUUID as randomUUID12 } from "node:crypto";
35045
35304
  function withFrizzThreadSlug(servers, slug) {
35046
35305
  const frizz = servers?.[FRIZZ_MCP.name];
35047
- if (!frizz) return servers;
35306
+ if (!frizz || !("command" in frizz)) return servers;
35048
35307
  return { ...servers, [FRIZZ_MCP.name]: { ...frizz, env: { ...frizz.env, FRIZZ_THREAD_SLUG: slug } } };
35049
35308
  }
35050
35309
  function claudeBrokerBridgeEnabled(env = process.env) {
@@ -35172,6 +35431,7 @@ function createClaudeAgentBrokerBridge(deps) {
35172
35431
  // The auto-compact ceiling, from Settings (500K by default). Without it a `[1m]` worker compacts
35173
35432
  // only near 1M, re-sending up to 5x a TUI session's conversation on every turn past 200K.
35174
35433
  ...claudeCompactionEnv(deps.getSettings?.()),
35434
+ ...claudePromptCacheEnv(deps.getSettings?.()),
35175
35435
  ...we?.permDir ? { FRIZZ_PERM_DIR: we.permDir } : {},
35176
35436
  // The cc-worker plugin's PreToolUse hook DENIES AskUserQuestion, because without frizz in the
35177
35437
  // loop a blocking question freezes a headless worker where nobody can answer it. On the broker path
@@ -35447,19 +35707,19 @@ var init_claude_agent_broker_bridge = __esm({
35447
35707
  });
35448
35708
 
35449
35709
  // 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";
35710
+ import { join as join27 } from "node:path";
35711
+ import { homedir as homedir12, platform as platform2 } from "node:os";
35712
+ import { readFileSync as readFileSync17, statSync as statSync11 } from "node:fs";
35453
35713
  import { execFile as execFile4 } from "node:child_process";
35454
35714
  import { promisify as promisify4 } from "node:util";
35455
35715
  function claudeConfigDir2() {
35456
35716
  const override = process.env.CLAUDE_CONFIG_DIR;
35457
- return override && override.trim() ? override : join26(homedir11(), ".claude");
35717
+ return override && override.trim() ? override : join27(homedir12(), ".claude");
35458
35718
  }
35459
35719
  function claudeFileState(configDir) {
35460
35720
  let raw2;
35461
35721
  try {
35462
- raw2 = readFileSync16(join26(configDir, ".credentials.json"), "utf8");
35722
+ raw2 = readFileSync17(join27(configDir, ".credentials.json"), "utf8");
35463
35723
  } catch (err) {
35464
35724
  return err.code === "ENOENT" ? "absent" : "error";
35465
35725
  }
@@ -35492,7 +35752,7 @@ function readCodexAuthState(codexHome = defaultCodexHome()) {
35492
35752
  if (process.env.OPENAI_API_KEY || process.env.CODEX_API_KEY || process.env.CODEX_ACCESS_TOKEN) return "authed";
35493
35753
  let raw2;
35494
35754
  try {
35495
- raw2 = readFileSync16(join26(codexHome, "auth.json"), "utf8");
35755
+ raw2 = readFileSync17(join27(codexHome, "auth.json"), "utf8");
35496
35756
  } catch (err) {
35497
35757
  return err.code === "ENOENT" ? "signed-out" : "unknown";
35498
35758
  }
@@ -35557,7 +35817,7 @@ function asEmail(value) {
35557
35817
  }
35558
35818
  function claudeAccountFile() {
35559
35819
  const override = process.env.CLAUDE_CONFIG_DIR;
35560
- return override && override.trim() ? join26(override, ".claude.json") : join26(homedir11(), ".claude.json");
35820
+ return override && override.trim() ? join27(override, ".claude.json") : join27(homedir12(), ".claude.json");
35561
35821
  }
35562
35822
  function readClaudeAccountEmail(path = claudeAccountFile()) {
35563
35823
  let stat2;
@@ -35570,7 +35830,7 @@ function readClaudeAccountEmail(path = claudeAccountFile()) {
35570
35830
  if (memo3 && memo3.path === path && memo3.mtimeMs === stat2.mtimeMs && memo3.size === stat2.size) return memo3.email;
35571
35831
  let email;
35572
35832
  try {
35573
- const doc = JSON.parse(readFileSync16(path, "utf8"));
35833
+ const doc = JSON.parse(readFileSync17(path, "utf8"));
35574
35834
  email = asEmail(doc?.oauthAccount?.emailAddress);
35575
35835
  } catch {
35576
35836
  email = void 0;
@@ -35581,7 +35841,7 @@ function readClaudeAccountEmail(path = claudeAccountFile()) {
35581
35841
  function readCodexAccountEmail(codexHome = defaultCodexHome()) {
35582
35842
  let doc;
35583
35843
  try {
35584
- doc = JSON.parse(readFileSync16(join26(codexHome, "auth.json"), "utf8"));
35844
+ doc = JSON.parse(readFileSync17(join27(codexHome, "auth.json"), "utf8"));
35585
35845
  } catch {
35586
35846
  return void 0;
35587
35847
  }
@@ -35649,8 +35909,8 @@ var init_auth_status = __esm({
35649
35909
  });
35650
35910
 
35651
35911
  // 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";
35912
+ 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";
35913
+ import { basename as basename6, join as join28, dirname as dirname11 } from "node:path";
35654
35914
  import { createHash as createHash11, randomUUID as randomUUID13 } from "node:crypto";
35655
35915
  function fallbackTitle(prompt) {
35656
35916
  const firstLine2 = prompt.trim().split("\n", 1)[0].trim();
@@ -35669,7 +35929,7 @@ function fallbackTitle(prompt) {
35669
35929
  }
35670
35930
  function resolveSlug(frizzDir, base, taken) {
35671
35931
  base = ThreadSlug.parse(base);
35672
- const isTaken = (slug) => existsSync9(join27(frizzDir, `${slug}.md`)) || (taken?.(slug) ?? false);
35932
+ const isTaken = (slug) => existsSync9(join28(frizzDir, `${slug}.md`)) || (taken?.(slug) ?? false);
35673
35933
  if (!isTaken(base)) return base;
35674
35934
  for (let n = 2; ; n++) {
35675
35935
  const suffix = `-${n}`;
@@ -35686,12 +35946,12 @@ function resolveLegacyThreadFile(projectDir, value) {
35686
35946
  if (!parsed.success) return null;
35687
35947
  try {
35688
35948
  const projectRoot = realpathSync7(projectDir);
35689
- const frizzPath = join27(projectRoot, ".frizz");
35949
+ const frizzPath = join28(projectRoot, ".frizz");
35690
35950
  const frizzStat = lstatSync3(frizzPath);
35691
35951
  if (!frizzStat.isDirectory() || frizzStat.isSymbolicLink()) return null;
35692
35952
  const realFrizz = realpathSync7(frizzPath);
35693
35953
  if (dirname11(realFrizz) !== projectRoot || basename6(realFrizz) !== ".frizz") return null;
35694
- const path = join27(realFrizz, `${parsed.data}.md`);
35954
+ const path = join28(realFrizz, `${parsed.data}.md`);
35695
35955
  const before = lstatSync3(path);
35696
35956
  if (!before.isFile() || before.isSymbolicLink()) return null;
35697
35957
  const realPath = realpathSync7(path);
@@ -35702,7 +35962,7 @@ function resolveLegacyThreadFile(projectDir, value) {
35702
35962
  const fd = openSync13(path, constants.O_RDONLY | constants.O_NOFOLLOW);
35703
35963
  try {
35704
35964
  openedBefore = fstatSync2(fd);
35705
- contents = readFileSync17(fd);
35965
+ contents = readFileSync18(fd);
35706
35966
  openedAfter = fstatSync2(fd);
35707
35967
  } finally {
35708
35968
  closeSync12(fd);
@@ -35735,7 +35995,7 @@ function boardAuthorizesAdoption(board, slug) {
35735
35995
  return !board.errorItems.some((item) => item.file === `${slug}.md`);
35736
35996
  }
35737
35997
  function ensureSafeDirectDirectory(parent, name) {
35738
- const path = join27(parent, name);
35998
+ const path = join28(parent, name);
35739
35999
  try {
35740
36000
  mkdirSync13(path);
35741
36001
  } catch (error) {
@@ -35765,8 +36025,8 @@ function loadWorkerPrompt(kind = "claude") {
35765
36025
  function monitorScriptsDir() {
35766
36026
  const plugin = workerPluginDir();
35767
36027
  if (!plugin) return void 0;
35768
- const dir = join27(plugin, "skills", "gh", "scripts");
35769
- return existsSync9(join27(dir, "ci-watch.mjs")) ? dir : void 0;
36028
+ const dir = join28(plugin, "skills", "gh", "scripts");
36029
+ return existsSync9(join28(dir, "ci-watch.mjs")) ? dir : void 0;
35770
36030
  }
35771
36031
  function codexScratchpadHookConfig(hookScript, sessionId) {
35772
36032
  if (!hookScript || !sessionId) return {};
@@ -35778,7 +36038,7 @@ function codexScratchpadHookConfig(hookScript, sessionId) {
35778
36038
  }
35779
36039
  ]
35780
36040
  });
35781
- const bashBackgroundHook = join27(dirname11(hookScript), "bash-background.mjs");
36041
+ const bashBackgroundHook = join28(dirname11(hookScript), "bash-background.mjs");
35782
36042
  return {
35783
36043
  bypass_hook_trust: true,
35784
36044
  hooks: {
@@ -35804,7 +36064,7 @@ function codexScratchpadHookConfig(hookScript, sessionId) {
35804
36064
  }
35805
36065
  function scratchpadHookScript() {
35806
36066
  const plugin = workerPluginDir();
35807
- return plugin ? join27(plugin, "hooks", "scratchpad.mjs") : void 0;
36067
+ return plugin ? join28(plugin, "hooks", "scratchpad.mjs") : void 0;
35808
36068
  }
35809
36069
  function composePrompt(sessionId, prompt, kind = "claude") {
35810
36070
  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 +36080,12 @@ function scratchpadOrientation(sessionId, kind = "claude") {
35820
36080
  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
36081
  }
35822
36082
  function frizzConfigBlock(projectDir) {
35823
- const path = join27(projectDir, "FRIZZ.md");
36083
+ const path = join28(projectDir, "FRIZZ.md");
35824
36084
  let body;
35825
36085
  try {
35826
36086
  const st = statSync12(path);
35827
36087
  if (!st.isFile() || st.size > FRIZZ_MD_MAX_BYTES) return "";
35828
- body = readFileSync17(path, "utf8").trim();
36088
+ body = readFileSync18(path, "utf8").trim();
35829
36089
  } catch {
35830
36090
  return "";
35831
36091
  }
@@ -35860,7 +36120,7 @@ function resolveFrizzMcp(target, moduleUrl = import.meta.url, env = process.env,
35860
36120
  const { stateDir, serverLock, projectId } = typeof target === "string" ? { stateDir: target } : target;
35861
36121
  const pluginDir = resolveWorkerPluginDir(moduleUrl, env);
35862
36122
  if (!pluginDir) return void 0;
35863
- const scriptPath = join27(pluginDir, "bin", FRIZZ_MCP.script);
36123
+ const scriptPath = join28(pluginDir, "bin", FRIZZ_MCP.script);
35864
36124
  if (!existsSync9(scriptPath)) return void 0;
35865
36125
  return {
35866
36126
  scriptPath,
@@ -35870,7 +36130,7 @@ function resolveFrizzMcp(target, moduleUrl = import.meta.url, env = process.env,
35870
36130
  ...slug ? { slug } : {}
35871
36131
  };
35872
36132
  }
35873
- function claudeMcpConfig(mcp) {
36133
+ function claudeMcpConfig(mcp, project) {
35874
36134
  const mcpServers = {};
35875
36135
  const allowedTools = [];
35876
36136
  if (mcp) {
@@ -35878,13 +36138,16 @@ function claudeMcpConfig(mcp) {
35878
36138
  mcpServers[FRIZZ_MCP.name] = { command: process.execPath, args: [mcp.scriptPath], env };
35879
36139
  allowedTools.push(`mcp__${FRIZZ_MCP.name}`);
35880
36140
  }
35881
- return { mcpServers, allowedTools };
36141
+ return { mcpServers: workerMcpServers(project, mcpServers), allowedTools };
35882
36142
  }
35883
- function claudeMcpFlags(mcp) {
35884
- const { mcpServers, allowedTools } = claudeMcpConfig(mcp);
35885
- if (Object.keys(mcpServers).length === 0) return [];
36143
+ function claudeMcpFlags(mcp, project) {
36144
+ const { mcpServers, allowedTools } = claudeMcpConfig(mcp, project);
36145
+ const argv = ["--strict-mcp-config"];
36146
+ if (Object.keys(mcpServers).length === 0) return argv;
35886
36147
  const config = JSON.stringify({ mcpServers });
35887
- return ["--mcp-config", config, `--allowedTools=${allowedTools.join(",")}`];
36148
+ argv.push("--mcp-config", config);
36149
+ if (allowedTools.length > 0) argv.push(`--allowedTools=${allowedTools.join(",")}`);
36150
+ return argv;
35888
36151
  }
35889
36152
  function workerDisallowedToolFlags() {
35890
36153
  return [`--disallowedTools=${WORKER_DISALLOWED_TOOLS.join(",")}`];
@@ -35896,7 +36159,7 @@ function buildClaudeCommand(opts) {
35896
36159
  if (effort.effort) argv.push("--effort", effort.effort);
35897
36160
  argv.push(...claudeUltracodeFlags(effort));
35898
36161
  if (opts.pluginDir) argv.push("--plugin-dir", opts.pluginDir);
35899
- argv.push(...claudeMcpFlags(opts.frizzMcp));
36162
+ argv.push(...claudeMcpFlags(opts.frizzMcp, opts.projectMcpServers));
35900
36163
  argv.push(...workerDisallowedToolFlags());
35901
36164
  const worker = opts.workerPrompt ?? loadWorkerPrompt();
35902
36165
  const system = [worker, opts.extraSystemPrompt?.trim()].filter(Boolean).join("\n\n");
@@ -35929,7 +36192,7 @@ function buildClaudeResumeCommand(opts) {
35929
36192
  if (effort.effort) argv.push("--effort", effort.effort);
35930
36193
  argv.push(...claudeUltracodeFlags(effort));
35931
36194
  if (opts.pluginDir) argv.push("--plugin-dir", opts.pluginDir);
35932
- argv.push(...claudeMcpFlags(opts.frizzMcp));
36195
+ argv.push(...claudeMcpFlags(opts.frizzMcp, opts.projectMcpServers));
35933
36196
  argv.push(...workerDisallowedToolFlags());
35934
36197
  const worker = opts.workerPrompt ?? loadWorkerPrompt();
35935
36198
  const system = [worker, opts.extraSystemPrompt?.trim()].filter(Boolean).join("\n\n");
@@ -35940,7 +36203,7 @@ function buildClaudeResumeCommand(opts) {
35940
36203
  }
35941
36204
  function createDispatcher(deps) {
35942
36205
  const readBoardSource = deps.readBoard ?? readBoard;
35943
- const frizzDir = join27(deps.project.dir, ".frizz");
36206
+ const frizzDir = join28(deps.project.dir, ".frizz");
35944
36207
  const adoptionRuntime = deps.adoptionRuntime ?? productionRuntime;
35945
36208
  function cleanupPrewrites(built) {
35946
36209
  for (const path of new Set(built.prewrite.map((file) => file.path))) {
@@ -35953,7 +36216,7 @@ function createDispatcher(deps) {
35953
36216
  function cleanupDispatchFiles(scratchRel, built, sessionId) {
35954
36217
  cleanupPrewrites(built);
35955
36218
  try {
35956
- rmSync9(join27(deps.project.dir, scratchRel), { force: true, recursive: true });
36219
+ rmSync9(join28(deps.project.dir, scratchRel), { force: true, recursive: true });
35957
36220
  } catch {
35958
36221
  }
35959
36222
  cleanupAdoptionSessionFiles(deps.project.dir, sessionId);
@@ -36328,6 +36591,7 @@ var init_dispatch = __esm({
36328
36591
  init_src();
36329
36592
  init_logging();
36330
36593
  init_project();
36594
+ init_project_mcp_servers();
36331
36595
  init_types2();
36332
36596
  init_types2();
36333
36597
  init_worker_plugin_dir();
@@ -36385,8 +36649,8 @@ var init_dispatch = __esm({
36385
36649
  import {
36386
36650
  watch as fsWatch
36387
36651
  } from "node:fs";
36388
- import { homedir as homedir12 } from "node:os";
36389
- import { join as join28 } from "node:path";
36652
+ import { homedir as homedir13 } from "node:os";
36653
+ import { join as join29 } from "node:path";
36390
36654
  import watcher from "@parcel/watcher";
36391
36655
  function appServerTurnStalled(liveness, lastActivityAt, nowMs) {
36392
36656
  if (!liveness) return false;
@@ -36515,7 +36779,7 @@ function safeQuestionAnswer(raw2) {
36515
36779
  return void 0;
36516
36780
  }
36517
36781
  }
36518
- function answersInFlight(rows, lastUserAt) {
36782
+ function answersInFlight(rows, lastUserAt, wakeOnDismissals = false) {
36519
36783
  const userAt = lastUserAt ? Date.parse(lastUserAt) : Number.NaN;
36520
36784
  const answers = [];
36521
36785
  const dismissed = [];
@@ -36531,7 +36795,8 @@ function answersInFlight(rows, lastUserAt) {
36531
36795
  const parsed = safeQuestionAnswer(q.answer);
36532
36796
  if (parsed) answers.push(parsed);
36533
36797
  }
36534
- return answers.length > 0 ? questionAnswerMessage(answers, dismissed) : void 0;
36798
+ if (answers.length > 0) return questionAnswerMessage(answers, dismissed);
36799
+ return wakeOnDismissals && dismissed.length > 0 ? questionsCancelledWakeMessage(dismissed.length) : void 0;
36535
36800
  }
36536
36801
  function hasRegisteredBackgroundPark(tele, armedWatches, nowMs) {
36537
36802
  return armedWatches.some((w) => {
@@ -36781,7 +37046,7 @@ function sessionThreadView(projectDir, storage, row, rawTele, registeredLegacyTe
36781
37046
  const spec = safeQuestionSpec(q.spec);
36782
37047
  if (spec) questions.push({ id: q.id, spec, askedAt: new Date(q.asked_at).toISOString() });
36783
37048
  }
36784
- const inFlightAnswers = answersInFlight(questionRows, rawTele?.lastUserAt);
37049
+ const inFlightAnswers = answersInFlight(questionRows, rawTele?.lastUserAt, row.recurring_on_rest === 1 && Boolean(row.recurring_prompt?.trim()));
36785
37050
  const armedWatches = storage.listThreadWatches(row.slug, { armedOnly: true }).map((w) => ({
36786
37051
  id: w.id,
36787
37052
  kind: w.kind,
@@ -36866,6 +37131,7 @@ function sessionThreadView(projectDir, storage, row, rawTele, registeredLegacyTe
36866
37131
  // Only meaningful while the snooze is still pending — a prompt without a live deadline is an
36867
37132
  // already-delivered (or superseded) bump the row has not been swept clean of yet.
36868
37133
  snoozePrompt: snoozedUntil ? row.snooze_prompt ?? void 0 : void 0,
37134
+ pinnedAt: row.pinned_at ?? void 0,
36869
37135
  // The event-snooze, as a fact the CHAT can read. `awaitingBackground` still ignores it — that flag
36870
37136
  // states whether the thread is waiting, which the snooze does not change — so the suppression is a
36871
37137
  // presentation rule the client applies, not a second opinion about the thread's state.
@@ -37083,7 +37349,7 @@ function createBoard(project, storage, bus, tailer, bootId, deps = {}) {
37083
37349
  ...project.githubRepo ? { githubRepo: project.githubRepo } : {},
37084
37350
  projectSlug: findByPath(project.dir)?.slug,
37085
37351
  // So the client can expand a `~` a worker wrote in prose (see BoardSnapshot.homeDir).
37086
- homeDir: homedir12()
37352
+ homeDir: homedir13()
37087
37353
  };
37088
37354
  const sessionThreads = buildSessionThreads(assembledAtMs);
37089
37355
  armSnoozeWake(sessionThreads, assembledAtMs);
@@ -37184,7 +37450,7 @@ function createBoard(project, storage, bus, tailer, bootId, deps = {}) {
37184
37450
  if (parcelSub || stopped) return Promise.resolve();
37185
37451
  if (watchSetup) return watchSetup;
37186
37452
  const setup = (async () => {
37187
- const next = await subscribe(join28(project.dir, ".frizz"), () => scheduleRebuild());
37453
+ const next = await subscribe(join29(project.dir, ".frizz"), () => scheduleRebuild());
37188
37454
  if (stopped) {
37189
37455
  await next.unsubscribe();
37190
37456
  return;
@@ -37284,6 +37550,9 @@ var init_board = __esm({
37284
37550
  });
37285
37551
 
37286
37552
  // packages/server/src/wake-store.ts
37553
+ function isQuietWindowExempt(hintKey) {
37554
+ return WAKE_QUIET_EXEMPT_HINT_PREFIXES.some((prefix) => hintKey.startsWith(prefix));
37555
+ }
37287
37556
  function delivery(row) {
37288
37557
  return {
37289
37558
  id: row.id,
@@ -37334,14 +37603,22 @@ function ensureWakeDeliverySchema(db) {
37334
37603
  ON wake_delivery(project_id, state, next_attempt_at, created_at);
37335
37604
  `);
37336
37605
  }
37337
- function createWakeDeliveryStore(scope) {
37606
+ function createWakeDeliveryStore(scope, options = {}) {
37338
37607
  const db = scope.db;
37339
37608
  ensureWakeDeliverySchema(db);
37609
+ const quietWindowMs = Math.max(0, options.quietWindowMs ?? WAKE_QUIET_WINDOW_MS);
37340
37610
  const byId = scope.prepare("SELECT * FROM wake_delivery WHERE project_id = @project_id AND id = ?");
37341
37611
  const all = scope.prepare("SELECT * FROM wake_delivery WHERE project_id = @project_id ORDER BY created_at, id");
37342
37612
  const open = scope.prepare(
37343
37613
  "SELECT * FROM wake_delivery WHERE project_id = @project_id AND state IN ('pending', 'leased') ORDER BY created_at, id"
37344
37614
  );
37615
+ const pendingForStmt = scope.prepare(
37616
+ "SELECT * FROM wake_delivery WHERE project_id = @project_id AND thread_slug = ? AND session_id = ? AND state = 'pending' ORDER BY created_at, id"
37617
+ );
37618
+ const lastHandoffStmt = scope.prepare(`
37619
+ SELECT MAX(MAX(COALESCE(delivered_at, 0), COALESCE(sent_at, 0))) AS at
37620
+ FROM wake_delivery WHERE project_id = @project_id AND thread_slug = ?
37621
+ `);
37345
37622
  const insert = scope.prepare(`
37346
37623
  INSERT INTO wake_delivery (
37347
37624
  project_id, id, thread_slug, session_id, fence_id, hint_key, message, reason, state, attempts,
@@ -37349,7 +37626,7 @@ function createWakeDeliveryStore(scope) {
37349
37626
  delivered_at, terminal_at
37350
37627
  ) VALUES (
37351
37628
  @project_id, @id, @slug, @sessionId, @fenceId, @hintKey, @message, @reason, 'pending', 0,
37352
- @now, NULL, NULL, NULL, @now, @now, NULL, NULL
37629
+ @notBefore, NULL, NULL, NULL, @now, @now, NULL, NULL
37353
37630
  )
37354
37631
  ON CONFLICT DO NOTHING
37355
37632
  `);
@@ -37376,6 +37653,12 @@ function createWakeDeliveryStore(scope) {
37376
37653
  last_error = NULL, sent_at = NULL, updated_at = @now
37377
37654
  WHERE project_id = @project_id AND id = @id AND state = 'pending' AND next_attempt_at <= @now AND attempts < @maxAttempts
37378
37655
  `);
37656
+ const adoptStmt = scope.prepare(`
37657
+ UPDATE wake_delivery SET
37658
+ state = 'leased', attempts = attempts + 1, lease_owner = @owner, lease_until = @leaseUntil,
37659
+ last_error = NULL, sent_at = NULL, updated_at = @now
37660
+ WHERE project_id = @project_id AND id = @id AND state = 'pending' AND attempts < @maxAttempts
37661
+ `);
37379
37662
  const deferFailureStmt = scope.prepare(`
37380
37663
  UPDATE wake_delivery SET
37381
37664
  lease_until = @retryAt, last_error = @error, updated_at = @now
@@ -37416,7 +37699,12 @@ function createWakeDeliveryStore(scope) {
37416
37699
  WHERE project_id = @project_id AND id = @id AND state IN ('pending', 'leased')
37417
37700
  `);
37418
37701
  const enqueueTxn = db.transaction((input, now) => {
37419
- const created = insert.run({ ...input, now }).changes === 1;
37702
+ let notBefore = now;
37703
+ if (quietWindowMs > 0 && !isQuietWindowExempt(input.hintKey)) {
37704
+ const lastHandoff = lastHandoffStmt.get(input.slug)?.at ?? 0;
37705
+ if (lastHandoff > 0) notBefore = Math.max(now, lastHandoff + quietWindowMs);
37706
+ }
37707
+ const created = insert.run({ ...input, now, notBefore }).changes === 1;
37420
37708
  const row = byId.get(input.id);
37421
37709
  if (!row) throw new Error("wake delivery disappeared while it was being enqueued");
37422
37710
  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 +37720,10 @@ function createWakeDeliveryStore(scope) {
37432
37720
  if (claimStmt.run({ id: candidate.id, owner, now, leaseUntil, maxAttempts }).changes !== 1) return void 0;
37433
37721
  return delivery(byId.get(candidate.id));
37434
37722
  });
37723
+ const adoptTxn = db.transaction((id, owner, now, leaseUntil, maxAttempts) => {
37724
+ if (adoptStmt.run({ id, owner, now, leaseUntil, maxAttempts }).changes !== 1) return void 0;
37725
+ return delivery(byId.get(id));
37726
+ });
37435
37727
  return {
37436
37728
  enqueue: (input, now) => enqueueTxn.immediate(input, now),
37437
37729
  get: (id) => {
@@ -37440,7 +37732,9 @@ function createWakeDeliveryStore(scope) {
37440
37732
  },
37441
37733
  list: () => all.all().map(delivery),
37442
37734
  listOpen: () => open.all().map(delivery),
37735
+ pendingFor: (slug, sessionId) => pendingForStmt.all(slug, sessionId).map(delivery),
37443
37736
  claim: (owner, now, leaseUntil, maxAttempts) => claimTxn.immediate(owner, now, leaseUntil, maxAttempts),
37737
+ adopt: (id, owner, now, leaseUntil, maxAttempts) => adoptTxn.immediate(id, owner, now, leaseUntil, maxAttempts),
37444
37738
  deferFailure: (id, owner, now, retryAt, error) => deferFailureStmt.run({
37445
37739
  id,
37446
37740
  owner,
@@ -37459,10 +37753,12 @@ function createWakeDeliveryStore(scope) {
37459
37753
  supersede: (id, now, reason) => supersedeStmt.run({ id, now, reason: reason.slice(0, 500) }).changes === 1
37460
37754
  };
37461
37755
  }
37462
- var OUTBOX_CAP, WAKE_DELIVERY_TABLES;
37756
+ var WAKE_QUIET_WINDOW_MS, WAKE_QUIET_EXEMPT_HINT_PREFIXES, OUTBOX_CAP, WAKE_DELIVERY_TABLES;
37463
37757
  var init_wake_store = __esm({
37464
37758
  "packages/server/src/wake-store.ts"() {
37465
37759
  "use strict";
37760
+ WAKE_QUIET_WINDOW_MS = 5 * 6e4;
37761
+ WAKE_QUIET_EXEMPT_HINT_PREFIXES = ["answers:", "limit:"];
37466
37762
  OUTBOX_CAP = 2e3;
37467
37763
  WAKE_DELIVERY_TABLES = ["wake_delivery"];
37468
37764
  }
@@ -37745,9 +38041,9 @@ function createGithubReviewFetcher(deps = {}) {
37745
38041
  for (const entry of batch.values()) for (const resolve12 of entry.resolve) resolve12({ status: "deferred" });
37746
38042
  return;
37747
38043
  }
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);
38044
+ const entries2 = [...batch.values()];
38045
+ for (let offset = 0; offset < entries2.length; offset += MAX_REFS_PER_REQUEST) {
38046
+ const chunk = entries2.slice(offset, offset + MAX_REFS_PER_REQUEST);
37751
38047
  if (offset > 0 && now() < rateLimitBlockedUntilMs) {
37752
38048
  for (const entry of chunk) for (const resolve12 of entry.resolve) resolve12({ status: "deferred" });
37753
38049
  continue;
@@ -37960,11 +38256,11 @@ function failedCheckNames(rollup, runs = [], cap = 8) {
37960
38256
  return { names: names.slice(0, cap), omitted: Math.max(0, names.length - cap) };
37961
38257
  }
37962
38258
  function githubWatchStatus(pr, polledAt) {
37963
- const entries = Array.isArray(pr.rollup) ? pr.rollup.filter((c) => c && typeof c === "object") : [];
38259
+ const entries2 = Array.isArray(pr.rollup) ? pr.rollup.filter((c) => c && typeof c === "object") : [];
37964
38260
  let running = 0;
37965
38261
  let passed = 0;
37966
38262
  let failed = 0;
37967
- for (const c of entries) {
38263
+ for (const c of entries2) {
37968
38264
  const status = typeof c.status === "string" ? c.status : void 0;
37969
38265
  const state2 = typeof c.state === "string" ? c.state : void 0;
37970
38266
  const conclusion = typeof c.conclusion === "string" ? c.conclusion : void 0;
@@ -37974,19 +38270,19 @@ function githubWatchStatus(pr, polledAt) {
37974
38270
  else passed++;
37975
38271
  }
37976
38272
  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";
38273
+ const checks = entries2.length === 0 ? "none" : failed > 0 ? "failing" : running > 0 ? "running" : "passing";
37978
38274
  const merge = pr.mergeable === "CONFLICTING" ? "conflicting" : pr.mergeable !== "MERGEABLE" ? "unknown" : checks === "failing" || pr.reviewDecision === "CHANGES_REQUESTED" || pr.reviewDecision === "REVIEW_REQUIRED" ? "blocked" : "mergeable";
37979
38275
  return {
37980
38276
  checks,
37981
38277
  running,
37982
38278
  passed,
37983
38279
  failed,
37984
- failing: failedCheckNames(entries, pr.workflowRuns ?? []).names,
38280
+ failing: failedCheckNames(entries2, pr.workflowRuns ?? []).names,
37985
38281
  merge,
37986
38282
  state,
37987
38283
  polledAt,
37988
38284
  head: pr.head,
37989
- failureSig: failureSignature(entries)
38285
+ failureSig: failureSignature(entries2)
37990
38286
  };
37991
38287
  }
37992
38288
  function wakeDeliveryId(slug, sessionId, fenceId) {
@@ -38215,7 +38511,7 @@ function createScheduler(deps) {
38215
38511
  const confirmGraceMs = Math.max(1, deps.confirmGraceMs ?? 6e4);
38216
38512
  const deliveryBatchSize = Math.max(0, deps.deliveryBatchSize ?? 50);
38217
38513
  const deliveryOwner = randomUUID14();
38218
- const outbox = createWakeDeliveryStore(deps.storage.scope);
38514
+ const outbox = createWakeDeliveryStore(deps.storage.scope, { quietWindowMs: deps.wakeQuietWindowMs ?? WAKE_QUIET_WINDOW_MS });
38219
38515
  const reviewFailures = /* @__PURE__ */ new Map();
38220
38516
  let timer = null;
38221
38517
  let activeTick = null;
@@ -38311,9 +38607,14 @@ function createScheduler(deps) {
38311
38607
  }
38312
38608
  return "superseded";
38313
38609
  }
38314
- function isDeliverableNow(item, context) {
38610
+ function isDeliverableNow(item, context, nowMs) {
38315
38611
  if (context === "current-idle") return true;
38316
- return context === "current-busy" && (isHeartbeatFenceId(item.fenceId) || isTimerFenceId(item.fenceId) || isCompactFenceId(item.fenceId));
38612
+ if (context !== "current-busy") return false;
38613
+ if (isHeartbeatFenceId(item.fenceId) || isTimerFenceId(item.fenceId) || isCompactFenceId(item.fenceId)) return true;
38614
+ return nowMs - item.createdAt >= MID_TURN_HOLD_MAX_MS;
38615
+ }
38616
+ function heldPastBound(item, context, nowMs) {
38617
+ return context === "current-busy" && item.sentAt === null && item.lastError === WAKE_HOLD_DEFERRAL && nowMs - item.createdAt >= MID_TURN_HOLD_MAX_MS;
38317
38618
  }
38318
38619
  function activityLabel(a) {
38319
38620
  if (a.kind === "comment") return "comment";
@@ -38517,7 +38818,7 @@ function createScheduler(deps) {
38517
38818
  if (tele.authFault) continue;
38518
38819
  if (tele.apiFault) continue;
38519
38820
  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) {
38821
+ 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
38822
  if ((row.signoff_nudges ?? 0) > 0) deps.storage.resetSignoffNudges(row.slug);
38522
38823
  continue;
38523
38824
  }
@@ -38724,12 +39025,27 @@ ${clock}`;
38724
39025
  }
38725
39026
  }
38726
39027
  const prWatchPolledAt = /* @__PURE__ */ new Map();
39028
+ function readPrWatchHeld(raw2) {
39029
+ if (!raw2 || typeof raw2 !== "object") return void 0;
39030
+ const h = raw2;
39031
+ if (!Array.isArray(h.items)) return void 0;
39032
+ const items = h.items.flatMap((i) => GithubWakeItem.safeParse(i).success ? [i] : []);
39033
+ const c = h.checks && typeof h.checks === "object" ? h.checks : void 0;
39034
+ const checks = c && (c.verdict === "passing" || c.verdict === "failing") ? {
39035
+ verdict: c.verdict,
39036
+ passed: Number(c.passed) || 0,
39037
+ failed: Number(c.failed) || 0,
39038
+ failing: Array.isArray(c.failing) ? c.failing.filter((x) => typeof x === "string") : []
39039
+ } : void 0;
39040
+ return { items, omitted: Number(h.omitted) || 0, ...checks ? { checks } : {} };
39041
+ }
38727
39042
  function readPrWatchCursor(raw2) {
38728
39043
  try {
38729
39044
  const parsed = raw2 ? JSON.parse(raw2) : null;
38730
39045
  if (!parsed || typeof parsed !== "object") return { seen: [] };
38731
39046
  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 };
39047
+ const held = readPrWatchHeld(parsed.held);
39048
+ return { seen, checks: typeof parsed.checks === "string" ? parsed.checks : void 0, report: Number(parsed.report) || 0, ...held ? { held } : {} };
38733
39049
  } catch {
38734
39050
  return { seen: [] };
38735
39051
  }
@@ -38812,7 +39128,7 @@ ${clock}`;
38812
39128
  }), `pr-watch ${key} ${st.state}`, nowMs);
38813
39129
  continue;
38814
39130
  }
38815
- if (waiting) continue;
39131
+ if (waiting && !cursor.held) continue;
38816
39132
  const terminal = st && (st.checks === "passing" || st.checks === "failing") ? st.checks : void 0;
38817
39133
  const stamp = terminal !== void 0 ? `${st?.head ?? "?"}:${terminal}:${st?.failureSig ?? ""}` : void 0;
38818
39134
  const checksChanged = stamp !== void 0 && cursor.checks !== stamp;
@@ -38829,37 +39145,42 @@ ${clock}`;
38829
39145
  return Number.isFinite(landed) && landed > w.created_at;
38830
39146
  });
38831
39147
  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;
39148
+ const freshItems = [...named].reverse().map((a) => ({
39149
+ label: activityLabel(a),
39150
+ actor: a.actor,
39151
+ bot: isBotGithubActor(a),
39152
+ ...a.at ? { at: a.at } : {},
39153
+ ...a.url ? { url: a.url } : {}
39154
+ }));
38843
39155
  const nextCursor = {
38844
39156
  seen: acts ? [.../* @__PURE__ */ new Set([...cursor.seen, ...acts.map((a) => a.id)])].slice(-REVIEW_SEEN_CAP) : cursor.seen,
38845
39157
  // The STAMP, so the next poll compares against the commit as well as the colour.
38846
39158
  checks: stamp ?? cursor.checks,
38847
39159
  report: cursor.report ?? 0
38848
39160
  };
38849
- if (!checksChanged && !reviewSteer) {
39161
+ if (!checksChanged && fresh.length === 0) {
38850
39162
  if (JSON.stringify(nextCursor) !== JSON.stringify(cursor)) {
38851
39163
  deps.storage.setPrWatchCursor(w.id, JSON.stringify(nextCursor));
38852
39164
  }
38853
39165
  continue;
38854
39166
  }
38855
39167
  deps.storage.setSnoozedUntil(row.slug, null);
39168
+ const held = waiting ? cursor.held : void 0;
39169
+ const items = [...held?.items ?? [], ...freshItems];
39170
+ const carried = {
39171
+ items: items.slice(-REVIEW_STEER_CAP),
39172
+ omitted: (held?.omitted ?? 0) + (fresh.length - named.length) + Math.max(0, items.length - REVIEW_STEER_CAP),
39173
+ ...checksChanged && terminal ? { checks: { verdict: terminal, passed: st.passed, failed: st.failed, failing: st.failing } } : held?.checks ? { checks: held.checks } : {}
39174
+ };
39175
+ const review = carried.items.length > 0 ? formatGithubWakeSteer({ ref: key, items: carried.items, omitted: carried.omitted }) : void 0;
38856
39176
  const report = nextReport(cursor);
39177
+ if (waiting) outbox.supersede(waiting.id, nowMs, `folded into report ${report}, which says this and everything since`);
38857
39178
  enqueuePrWatchWake(row, w.id, report, prWatchWakeMessage({
38858
39179
  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 }));
39180
+ ...carried.checks ? { checks: carried.checks } : {},
39181
+ ...review ? { review } : {}
39182
+ }), `pr-watch ${key}${carried.checks ? ` CI ${carried.checks.verdict}` : ""}${review ? " review" : ""}`, nowMs);
39183
+ deps.storage.setPrWatchCursor(w.id, JSON.stringify({ ...nextCursor, report, held: carried }));
38863
39184
  }
38864
39185
  }
38865
39186
  function nextReport(cursor) {
@@ -39085,6 +39406,7 @@ ${clock}`;
39085
39406
  if (tele.authFault) continue;
39086
39407
  if (tele.apiFault) continue;
39087
39408
  if (restMessageIsSignedOff(deps.storage, row.slug, tele, registeredPrWatchesOf(deps.storage, row.slug), armedTimerIdsOf(deps.storage, row.slug))) continue;
39409
+ if (answersInFlight(deps.storage.listThreadQuestions(row.slug), tele.lastUserAt, true) !== void 0) continue;
39088
39410
  const reminder = outbox.get(wakeDeliveryId(row.slug, row.session_id, signoffFenceId(restedAt)));
39089
39411
  if (reminder && reminder.state !== "superseded" && reminder.state !== "exhausted") continue;
39090
39412
  const fenceId = stopHookFenceId(armed.armedAt, restedAt);
@@ -39155,11 +39477,8 @@ ${clock}`;
39155
39477
  const context = deliveryContext(item);
39156
39478
  if (context === "confirmed") {
39157
39479
  outbox.confirm(item.id, nowMs);
39158
- settleSnooze(item);
39159
- settleSchedulePrompt(item);
39160
- settleRestPrompt(item);
39161
- settleCompactPrompt(item);
39162
- settleTimer(item);
39480
+ settleDelivered(item);
39481
+ confirmFrame(item, nowMs);
39163
39482
  continue;
39164
39483
  }
39165
39484
  if (context === "superseded") {
@@ -39200,7 +39519,7 @@ ${clock}`;
39200
39519
  }
39201
39520
  continue;
39202
39521
  }
39203
- if (context !== "current-idle") continue;
39522
+ if (context !== "current-idle" && !heldPastBound(item, context, nowMs)) continue;
39204
39523
  const recovered = outbox.recoverExpired(
39205
39524
  item.id,
39206
39525
  nowMs,
@@ -39215,6 +39534,63 @@ ${clock}`;
39215
39534
  }
39216
39535
  }
39217
39536
  }
39537
+ function settleDelivered(item) {
39538
+ settleSnooze(item);
39539
+ settleSchedulePrompt(item);
39540
+ settleRestPrompt(item);
39541
+ settleCompactPrompt(item);
39542
+ settleTimer(item);
39543
+ }
39544
+ function adoptCompanions(carrier, claimedAt) {
39545
+ if (isQuestionAnswerFenceId(carrier.fenceId)) return [];
39546
+ const adopted = [];
39547
+ for (const sibling of outbox.pendingFor(carrier.slug, carrier.sessionId)) {
39548
+ if (sibling.id === carrier.id || isQuestionAnswerFenceId(sibling.fenceId)) continue;
39549
+ const context = deliveryContext(sibling);
39550
+ if (context === "confirmed") {
39551
+ outbox.confirm(sibling.id, now());
39552
+ settleDelivered(sibling);
39553
+ continue;
39554
+ }
39555
+ if (context === "superseded") {
39556
+ outbox.supersede(sibling.id, now(), "the exact awaiting fence or session was superseded before delivery");
39557
+ settleSnooze(sibling);
39558
+ continue;
39559
+ }
39560
+ if (!isDeliverableNow(sibling, context, claimedAt)) continue;
39561
+ const leased = outbox.adopt(sibling.id, deliveryOwner, claimedAt, claimedAt + deliveryLeaseMs, maxDeliveryAttempts);
39562
+ if (leased) adopted.push(leased);
39563
+ }
39564
+ return adopted;
39565
+ }
39566
+ function mergedWakeMessage(frame, spokeAt) {
39567
+ const sections = [];
39568
+ for (const part of frame) {
39569
+ const body2 = stripWakeTimeHeader(part.message).trim();
39570
+ const same = sections.find((s) => s.reason === part.reason && s.body === body2);
39571
+ if (same) same.count++;
39572
+ else sections.push({ reason: part.reason, body: body2, count: 1 });
39573
+ }
39574
+ 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.`;
39575
+ const body = sections.map((s, i) => `### ${i + 1}. ${s.reason}${s.count > 1 ? ` \u2014 ${s.count} identical events` : ""}
39576
+
39577
+ ${s.body}`);
39578
+ return `${lead}
39579
+
39580
+ ${body.join("\n\n")}
39581
+
39582
+ ${wakeTimeHeader(now(), spokeAt)}`;
39583
+ }
39584
+ function confirmFrame(carrier, nowMs) {
39585
+ if (carrier.sentAt === null) return;
39586
+ for (const d of outbox.listOpen()) {
39587
+ if (d.id === carrier.id || d.slug !== carrier.slug || d.sessionId !== carrier.sessionId) continue;
39588
+ if (d.state !== "leased" || d.sentAt !== carrier.sentAt) continue;
39589
+ if (!outbox.confirm(d.id, nowMs)) continue;
39590
+ settleDelivered(d);
39591
+ log3(`waker: delivered ${d.slug} \u2014 ${d.reason} (in the same frame as: ${carrier.reason})`);
39592
+ }
39593
+ }
39218
39594
  async function deliverDue() {
39219
39595
  for (let delivered = 0; delivered < deliveryBatchSize; delivered++) {
39220
39596
  const claimedAt = now();
@@ -39224,11 +39600,7 @@ ${clock}`;
39224
39600
  const context = deliveryContext(item);
39225
39601
  if (context === "confirmed") {
39226
39602
  outbox.confirm(item.id, now());
39227
- settleSnooze(item);
39228
- settleSchedulePrompt(item);
39229
- settleRestPrompt(item);
39230
- settleCompactPrompt(item);
39231
- settleTimer(item);
39603
+ settleDelivered(item);
39232
39604
  continue;
39233
39605
  }
39234
39606
  if (context === "superseded") {
@@ -39236,44 +39608,55 @@ ${clock}`;
39236
39608
  settleSnooze(item);
39237
39609
  continue;
39238
39610
  }
39239
- if (!isDeliverableNow(item, context)) {
39611
+ if (!isDeliverableNow(item, context, claimedAt)) {
39240
39612
  const deferredAt = now();
39241
39613
  outbox.deferFailure(
39242
39614
  item.id,
39243
39615
  deliveryOwner,
39244
39616
  deferredAt,
39245
39617
  deferredAt + Math.max(deliveryLeaseMs, retryDelay(item.attempts)),
39246
- "delivery deferred until exact awaiting telemetry is idle and available"
39618
+ WAKE_HOLD_DEFERRAL
39247
39619
  );
39248
39620
  continue;
39249
39621
  }
39622
+ const companions = adoptCompanions(item, claimedAt);
39623
+ const frame = [...companions, item].sort((a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id));
39624
+ const message = companions.length === 0 ? item.message : mergedWakeMessage(frame, deps.tailer.get(item.slug)?.lastAssistantAt);
39625
+ if (companions.length > 0) {
39626
+ log3(`waker: merged ${frame.length} wakes for ${item.slug} into one delivery \u2014 ${frame.map((d) => d.reason).join("; ")}`);
39627
+ }
39250
39628
  try {
39251
- await deps.resume(item.slug, item.message, item.id);
39629
+ await deps.resume(item.slug, message, item.id);
39252
39630
  } catch (error) {
39253
- const message = error instanceof Error ? error.message : String(error);
39631
+ const message2 = error instanceof Error ? error.message : String(error);
39254
39632
  const failedAt = now();
39255
39633
  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}`);
39634
+ for (const d of frame) {
39635
+ outbox.supersede(d.id, failedAt, message2);
39636
+ settleSnooze(d);
39637
+ settleTimer(d);
39638
+ }
39639
+ log3(`waker: delivery ABANDONED for ${item.slug} (terminal, no retry): ${message2}`);
39260
39640
  continue;
39261
39641
  }
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}`);
39642
+ for (const d of frame) {
39643
+ outbox.deferFailure(
39644
+ d.id,
39645
+ deliveryOwner,
39646
+ failedAt,
39647
+ failedAt + Math.max(deliveryLeaseMs, retryDelay(d.attempts)),
39648
+ message2
39649
+ );
39650
+ }
39651
+ log3(`waker: delivery FAILED for ${item.slug} (attempt ${item.attempts} of ${maxDeliveryAttempts}): ${message2}`);
39270
39652
  continue;
39271
39653
  }
39272
39654
  const runtime = deps.wakeRuntimeState?.(item.slug, item.sessionId) ?? "unknown";
39273
39655
  if (runtime !== "unknown") {
39274
39656
  const sentAt = now();
39275
39657
  if (outbox.markSent(item.id, deliveryOwner, sentAt, sentAt + confirmGraceMs)) {
39276
- settleSignoffNudge(item);
39658
+ for (const d of companions) outbox.markSent(d.id, deliveryOwner, sentAt, sentAt + confirmGraceMs);
39659
+ for (const d of frame) settleSignoffNudge(d);
39277
39660
  log3(`waker: sent ${item.slug} \u2014 ${item.reason}; confirming within ${Math.round(confirmGraceMs / 1e3)}s`);
39278
39661
  checkpoint("after-delivery", item);
39279
39662
  continue;
@@ -39282,16 +39665,15 @@ ${clock}`;
39282
39665
  const retried = item.attempts > 1 ? ` (on attempt ${item.attempts})` : "";
39283
39666
  log3(`waker: delivered ${item.slug} \u2014 ${item.reason}${retried}`);
39284
39667
  checkpoint("after-delivery", item);
39285
- if (!outbox.acknowledge(item.id, deliveryOwner, now())) {
39668
+ const acked = frame.filter((d) => outbox.acknowledge(d.id, deliveryOwner, now()));
39669
+ if (!acked.includes(item)) {
39286
39670
  log3(`waker: delivery acknowledgement lost ownership for ${item.slug}; preserving the authoritative terminal state`);
39287
- continue;
39288
39671
  }
39289
- settleSnooze(item);
39290
- settleSchedulePrompt(item);
39291
- settleRestPrompt(item);
39292
- settleCompactPrompt(item);
39293
- settleTimer(item);
39294
- settleSignoffNudge(item);
39672
+ for (const d of acked) {
39673
+ settleDelivered(d);
39674
+ settleSignoffNudge(d);
39675
+ }
39676
+ if (!acked.includes(item)) continue;
39295
39677
  const acknowledged = outbox.get(item.id);
39296
39678
  if (acknowledged) checkpoint("after-ack", acknowledged);
39297
39679
  }
@@ -39412,7 +39794,7 @@ ${clock}`;
39412
39794
  }
39413
39795
  };
39414
39796
  }
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;
39797
+ 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
39798
  var init_scheduler = __esm({
39417
39799
  "packages/server/src/scheduler.ts"() {
39418
39800
  "use strict";
@@ -39467,6 +39849,8 @@ var init_scheduler = __esm({
39467
39849
  timer: "timers",
39468
39850
  pr: "prs"
39469
39851
  };
39852
+ MID_TURN_HOLD_MAX_MS = 10 * 6e4;
39853
+ WAKE_HOLD_DEFERRAL = "delivery deferred until exact awaiting telemetry is idle and available";
39470
39854
  }
39471
39855
  });
39472
39856
 
@@ -39546,7 +39930,7 @@ var init_resume = __esm({
39546
39930
  });
39547
39931
 
39548
39932
  // packages/server/src/backend/claude.ts
39549
- import { join as join29 } from "node:path";
39933
+ import { join as join30 } from "node:path";
39550
39934
  function toolResultText2(content) {
39551
39935
  if (typeof content === "string") return content;
39552
39936
  if (!Array.isArray(content)) return "";
@@ -39615,7 +39999,8 @@ function createClaudeBackend(opts) {
39615
39999
  pluginDir: workerPluginDir(),
39616
40000
  workerPrompt: o.workerContract,
39617
40001
  extraSystemPrompt: o.extraSystemPrompt,
39618
- frizzMcp: o.frizzMcp
40002
+ frizzMcp: o.frizzMcp,
40003
+ projectMcpServers: (opts.resolveProjectMcpServers ?? projectMcpServers)(o.cwd)
39619
40004
  });
39620
40005
  return { argv, env: claudeWorkerEnvironment(), prewrite: [] };
39621
40006
  },
@@ -39630,12 +40015,13 @@ function createClaudeBackend(opts) {
39630
40015
  pluginDir: workerPluginDir(),
39631
40016
  workerPrompt: o.workerContract,
39632
40017
  extraSystemPrompt: o.extraSystemPrompt,
39633
- frizzMcp: o.frizzMcp
40018
+ frizzMcp: o.frizzMcp,
40019
+ projectMcpServers: (opts.resolveProjectMcpServers ?? projectMcpServers)(o.cwd)
39634
40020
  });
39635
40021
  return { argv, env: claudeWorkerEnvironment(), prewrite: [] };
39636
40022
  },
39637
40023
  transcriptPath(sessionId) {
39638
- return join29(opts.logDir, `${sessionId}.jsonl`);
40024
+ return join30(opts.logDir, `${sessionId}.jsonl`);
39639
40025
  },
39640
40026
  parseLine(line) {
39641
40027
  return parseClaudeLine(line);
@@ -39653,6 +40039,7 @@ function createClaudeBackend(opts) {
39653
40039
  var init_claude = __esm({
39654
40040
  "packages/server/src/backend/claude.ts"() {
39655
40041
  "use strict";
40042
+ init_project_mcp_servers();
39656
40043
  init_dispatch();
39657
40044
  init_tailer();
39658
40045
  }
@@ -39805,12 +40192,12 @@ var init_login_utility = __esm({
39805
40192
 
39806
40193
  // packages/server/src/backend/codex-app-server-diagnostics.ts
39807
40194
  import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync14, renameSync as renameSync8, statSync as statSync13 } from "node:fs";
39808
- import { join as join30 } from "node:path";
40195
+ import { join as join31 } from "node:path";
39809
40196
  function codexDiagnosticLogPath(stateDir, projectId) {
39810
- return join30(stateDir, "codex-app-server", `${projectId}.diagnostics.log`);
40197
+ return join31(stateDir, "codex-app-server", `${projectId}.diagnostics.log`);
39811
40198
  }
39812
40199
  function createCodexDiagnosticSink(stateDir, projectId, now = () => /* @__PURE__ */ new Date()) {
39813
- const dir = join30(stateDir, "codex-app-server");
40200
+ const dir = join31(stateDir, "codex-app-server");
39814
40201
  const path = codexDiagnosticLogPath(stateDir, projectId);
39815
40202
  let ensured = false;
39816
40203
  return (event) => {
@@ -39842,7 +40229,7 @@ var init_codex_app_server_diagnostics = __esm({
39842
40229
  import { execFile as execFile7 } from "node:child_process";
39843
40230
  import { readdirSync as readdirSync8, unlinkSync as unlinkSync4 } from "node:fs";
39844
40231
  import { connect as connectSocket } from "node:net";
39845
- import { join as join31 } from "node:path";
40232
+ import { join as join32 } from "node:path";
39846
40233
  function parseLsofSocketNames(stdout) {
39847
40234
  const referenced = /* @__PURE__ */ new Set();
39848
40235
  for (const line of stdout.split("\n")) {
@@ -39859,7 +40246,7 @@ function sweepStaleSockets(options, deps = {}) {
39859
40246
  const keep = new Set(options.keep ?? []);
39860
40247
  let candidates;
39861
40248
  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));
40249
+ 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
40250
  } catch {
39864
40251
  return;
39865
40252
  }
@@ -39909,7 +40296,7 @@ var init_stale_socket_sweep = __esm({
39909
40296
 
39910
40297
  // packages/server/src/orphan-reaper.ts
39911
40298
  import { execFileSync as execFileSync5 } from "node:child_process";
39912
- import { readFileSync as readFileSync18 } from "node:fs";
40299
+ import { readFileSync as readFileSync19 } from "node:fs";
39913
40300
  import { dirname as dirname12 } from "node:path";
39914
40301
  function firstTokenBasename(command) {
39915
40302
  const first = command.trimStart().split(/\s+/, 1)[0] ?? "";
@@ -40176,7 +40563,7 @@ var init_orphan_reaper = __esm({
40176
40563
  defaultExec = (file, args) => execFileSync5(file, args, { encoding: "utf8", maxBuffer: 16 * 1024 * 1024, timeout: 1e4 });
40177
40564
  defaultReadEnv = (pid) => {
40178
40565
  try {
40179
- return readFileSync18(`/proc/${pid}/environ`, "utf8");
40566
+ return readFileSync19(`/proc/${pid}/environ`, "utf8");
40180
40567
  } catch {
40181
40568
  return null;
40182
40569
  }
@@ -40323,9 +40710,9 @@ var init_thread_hibernation = __esm({
40323
40710
  });
40324
40711
 
40325
40712
  // packages/server/src/context.ts
40326
- import { join as join32 } from "node:path";
40713
+ import { join as join33 } from "node:path";
40327
40714
  import { randomUUID as randomUUID15 } from "node:crypto";
40328
- import { homedir as homedir13 } from "node:os";
40715
+ import { homedir as homedir14 } from "node:os";
40329
40716
  function reconcileSessions(storage) {
40330
40717
  for (const row of storage.allSessions()) {
40331
40718
  if (isHeadlessRow(row)) continue;
@@ -40471,9 +40858,9 @@ async function createContext(opts = {}) {
40471
40858
  }
40472
40859
  }
40473
40860
  function createContextUnchecked(opts, resources) {
40474
- const home = opts.home ?? homedir13();
40861
+ const home = opts.home ?? homedir14();
40475
40862
  const project = opts.project ?? resolveProject();
40476
- const storage = createStorage(opts.database ?? join32(project.stateDir, "ui.db"), project.id);
40863
+ const storage = createStorage(opts.database ?? join33(project.stateDir, "ui.db"), project.id);
40477
40864
  resources.storage = storage;
40478
40865
  const bus = new Bus();
40479
40866
  const transcriptChange = new Emitter();
@@ -40622,7 +41009,8 @@ function createContextUnchecked(opts, resources) {
40622
41009
  // The frizz worker environment — the SDK equivalent of the CLI argv's --plugin-dir / --mcp-config.
40623
41010
  // Computed ONCE here (constant per project) and applied on every broker fork so a broker worker
40624
41011
  // 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.
41012
+ // cc-worker hooks. The project's own `.mcp.json` servers are merged in by the broker daemon at each
41013
+ // fork (project-mcp-servers.ts): under `--strict-mcp-config` nothing mounts that frizz did not hand over.
40626
41014
  workerEnv: {
40627
41015
  pluginDir: workerPluginDir(),
40628
41016
  ...claudeMcpConfig(resolveFrizzMcp(frizzMcpTarget)),
@@ -40765,6 +41153,8 @@ ${wakeDeliveryToken(deliveryId)}`;
40765
41153
  Date.now(),
40766
41154
  (tailer.get(slug)?.subAgents ?? []).some((agent) => agent.state === "running")
40767
41155
  )
41156
+ }).then(() => {
41157
+ if (row.exited === 1) storage.setExitedIfCurrent(slug, row.session_id, row.runtime_generation ?? 0, false);
40768
41158
  });
40769
41159
  }
40770
41160
  resumeThread(
@@ -43523,7 +43913,7 @@ var init_server = __esm({
43523
43913
  });
43524
43914
 
43525
43915
  // packages/server/src/repair.ts
43526
- import { existsSync as existsSync11, readFileSync as readFileSync19, writeFileSync as writeFileSync11 } from "node:fs";
43916
+ import { existsSync as existsSync11, readFileSync as readFileSync20, writeFileSync as writeFileSync11 } from "node:fs";
43527
43917
  import { basename as basename7, dirname as dirname13, resolve as resolve7 } from "node:path";
43528
43918
  function quoteValue(v) {
43529
43919
  if (/^[\w./#:+-]+$/.test(v)) return v;
@@ -43539,7 +43929,7 @@ function repairThreadFile(frizzDir, file) {
43539
43929
  if (dirname13(abs) !== root) throw new RepairError(`refusing to repair "${file}": not directly under .frizz/`);
43540
43930
  if (!abs.endsWith(".md")) throw new RepairError(`refusing to repair "${file}": not a .md thread file`);
43541
43931
  if (!existsSync11(abs)) throw new RepairError(`no thread file to repair: ${basename7(abs)}`);
43542
- const content = readFileSync19(abs, "utf8");
43932
+ const content = readFileSync20(abs, "utf8");
43543
43933
  const firstLine2 = content.replace(/^/, "").split(/\r?\n/).find((l) => l.trim() !== "");
43544
43934
  if (firstLine2?.trim() === "---") {
43545
43935
  throw new RepairError(`${basename7(abs)} already opens with a "---" block \u2014 repair only heals a MISSING frontmatter block`);
@@ -43617,7 +44007,7 @@ var init_edited_files = __esm({
43617
44007
  });
43618
44008
 
43619
44009
  // cc-worker/hooks/bash-background.mjs
43620
- import { readFileSync as readFileSync20 } from "node:fs";
44010
+ import { readFileSync as readFileSync21 } from "node:fs";
43621
44011
  import { basename as basename8 } from "node:path";
43622
44012
  import { pathToFileURL } from "node:url";
43623
44013
  function emit(obj) {
@@ -43766,7 +44156,7 @@ var init_bash_background = __esm({
43766
44156
  if (isDirectHookExecution(process.argv[1], import.meta.url)) {
43767
44157
  try {
43768
44158
  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));
44159
+ emit(evaluateBashBackgroundHook(JSON.parse(readFileSync21(0, "utf8")), env));
43770
44160
  } catch {
43771
44161
  emit({});
43772
44162
  }
@@ -43775,11 +44165,11 @@ var init_bash_background = __esm({
43775
44165
  });
43776
44166
 
43777
44167
  // 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";
44168
+ 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
44169
  import { createHash as createHash13 } from "node:crypto";
43780
44170
  import { StringDecoder as StringDecoder4 } from "node:string_decoder";
43781
- import { join as join33 } from "node:path";
43782
- import { homedir as homedir14 } from "node:os";
44171
+ import { join as join34 } from "node:path";
44172
+ import { homedir as homedir15 } from "node:os";
43783
44173
  function formatTokens(n) {
43784
44174
  if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
43785
44175
  if (n >= 1e3) return `${Math.round(n / 1e3)}k`;
@@ -44329,18 +44719,18 @@ function looksLikeImage(buf, ext) {
44329
44719
  }
44330
44720
  function pruneScreenshotCache() {
44331
44721
  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) => {
44722
+ const entries2 = readdirSync9(SCREENSHOT_CACHE_DIR).filter((n) => !n.startsWith("."));
44723
+ if (entries2.length <= SCREENSHOT_CACHE_MAX) return;
44724
+ const byMtime = entries2.map((n) => {
44335
44725
  try {
44336
- return { n, m: statSync14(join33(SCREENSHOT_CACHE_DIR, n)).mtimeMs };
44726
+ return { n, m: statSync14(join34(SCREENSHOT_CACHE_DIR, n)).mtimeMs };
44337
44727
  } catch {
44338
44728
  return { n, m: 0 };
44339
44729
  }
44340
44730
  }).sort((a, b) => b.m - a.m);
44341
44731
  for (const { n } of byMtime.slice(SCREENSHOT_CACHE_MAX)) {
44342
44732
  try {
44343
- unlinkSync5(join33(SCREENSHOT_CACHE_DIR, n));
44733
+ unlinkSync5(join34(SCREENSHOT_CACHE_DIR, n));
44344
44734
  } catch {
44345
44735
  }
44346
44736
  }
@@ -44370,14 +44760,14 @@ function persistBase64Image(mediaType, data, idKey) {
44370
44760
  const ext = IMAGE_MEDIA_EXT[typeof mediaType === "string" ? mediaType.toLowerCase() : ""];
44371
44761
  if (!ext || !data) return void 0;
44372
44762
  const name = createHash13("sha256").update(idKey).digest("hex").slice(0, 32);
44373
- const path = join33(SCREENSHOT_CACHE_DIR, `${name}.${ext}`);
44763
+ const path = join34(SCREENSHOT_CACHE_DIR, `${name}.${ext}`);
44374
44764
  try {
44375
44765
  if (existsSync12(path)) return path;
44376
44766
  if (data.length > SCREENSHOT_MAX_BASE64) return void 0;
44377
44767
  const buf = Buffer.from(data, "base64");
44378
44768
  if (buf.length === 0 || !looksLikeImage(buf, ext)) return void 0;
44379
44769
  mkdirSync15(SCREENSHOT_CACHE_DIR, { recursive: true });
44380
- const tmp = join33(SCREENSHOT_CACHE_DIR, `.${name}.${process.pid}.${screenshotTmpSeq++}.tmp`);
44770
+ const tmp = join34(SCREENSHOT_CACHE_DIR, `.${name}.${process.pid}.${screenshotTmpSeq++}.tmp`);
44381
44771
  writeFileSync12(tmp, buf);
44382
44772
  renameSync9(tmp, path);
44383
44773
  pruneScreenshotCache();
@@ -44392,14 +44782,14 @@ function persistSentFile(srcPath, idKey) {
44392
44782
  const outExt = ext === "jpeg" ? "jpg" : ext;
44393
44783
  try {
44394
44784
  const name = createHash13("sha256").update(idKey).digest("hex").slice(0, 32);
44395
- const dest = join33(SCREENSHOT_CACHE_DIR, `${name}.${outExt}`);
44785
+ const dest = join34(SCREENSHOT_CACHE_DIR, `${name}.${outExt}`);
44396
44786
  if (existsSync12(dest)) return dest;
44397
44787
  const size = statSync14(srcPath).size;
44398
44788
  if (size === 0 || size > SENT_IMAGE_MAX_BYTES) return void 0;
44399
- const buf = readFileSync21(srcPath);
44789
+ const buf = readFileSync22(srcPath);
44400
44790
  if (!looksLikeImage(buf, outExt)) return void 0;
44401
44791
  mkdirSync15(SCREENSHOT_CACHE_DIR, { recursive: true });
44402
- const tmp = join33(SCREENSHOT_CACHE_DIR, `.${name}.${process.pid}.${screenshotTmpSeq++}.tmp`);
44792
+ const tmp = join34(SCREENSHOT_CACHE_DIR, `.${name}.${process.pid}.${screenshotTmpSeq++}.tmp`);
44403
44793
  writeFileSync12(tmp, buf);
44404
44794
  renameSync9(tmp, dest);
44405
44795
  pruneScreenshotCache();
@@ -44837,7 +45227,7 @@ function readTranscript(project, sessionId) {
44837
45227
  }
44838
45228
  function verifyIncrementalParse(path, identityPrefix, incremental) {
44839
45229
  try {
44840
- const fresh = parseTranscript(readFileSync21(path, "utf8"), identityPrefix);
45230
+ const fresh = parseTranscript(readFileSync22(path, "utf8"), identityPrefix);
44841
45231
  const a = JSON.stringify(incremental);
44842
45232
  const b = JSON.stringify(fresh);
44843
45233
  if (a !== b) {
@@ -44857,16 +45247,16 @@ function verifyIncrementalParse(path, identityPrefix, incremental) {
44857
45247
  }
44858
45248
  }
44859
45249
  function logDirOf(project) {
44860
- return join33(homedir14(), ".claude", "projects", project.cwdSlug);
45250
+ return join34(homedir15(), ".claude", "projects", project.cwdSlug);
44861
45251
  }
44862
45252
  function resolveTranscriptPath(project, sessionId) {
44863
- const path = join33(logDirOf(project), `${sessionId}.jsonl`);
45253
+ const path = join34(logDirOf(project), `${sessionId}.jsonl`);
44864
45254
  try {
44865
45255
  if (statSync14(path).size > 0) return path;
44866
45256
  } catch {
44867
45257
  }
44868
45258
  const stranded = discoverTranscriptDir(logDirOf(project), sessionId);
44869
- return stranded ? join33(stranded, `${sessionId}.jsonl`) : path;
45259
+ return stranded ? join34(stranded, `${sessionId}.jsonl`) : path;
44870
45260
  }
44871
45261
  function projectCodexTranscript(raw2, identityPrefix = "codex") {
44872
45262
  const out = [];
@@ -45395,10 +45785,10 @@ function wrappedInvocations(source) {
45395
45785
  let open = nameStart + name.length;
45396
45786
  while (/\s/.test(source[open] ?? "")) open++;
45397
45787
  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;
45788
+ const close2 = matchingParen(source, open);
45789
+ if (close2 === -1) break;
45790
+ out.push({ name, args: source.slice(open + 1, close2).trim() });
45791
+ i = close2;
45402
45792
  }
45403
45793
  return out;
45404
45794
  }
@@ -46074,7 +46464,7 @@ function readEarlierThreadTranscriptPage(project, storage, slug, cursor, backend
46074
46464
  }
46075
46465
  function readCodexTranscriptFile(absPath, nativeId = absPath) {
46076
46466
  try {
46077
- return parseCodexTranscript(readFileSync21(absPath, "utf8"), `codex:${nativeId}`);
46467
+ return parseCodexTranscript(readFileSync22(absPath, "utf8"), `codex:${nativeId}`);
46078
46468
  } catch {
46079
46469
  return [];
46080
46470
  }
@@ -46172,7 +46562,7 @@ function projectTranscriptPageAgentLifecycles(page, lookup, peerLookup) {
46172
46562
  function readTranscriptFile(absPath) {
46173
46563
  try {
46174
46564
  const pathKey = createHash13("sha256").update(absPath).digest("base64url").slice(0, 16);
46175
- return parseTranscript(readFileSync21(absPath, "utf8"), `claude-file:${pathKey}`);
46565
+ return parseTranscript(readFileSync22(absPath, "utf8"), `claude-file:${pathKey}`);
46176
46566
  } catch {
46177
46567
  return [];
46178
46568
  }
@@ -46271,9 +46661,9 @@ var init_open_external = __esm({
46271
46661
 
46272
46662
  // packages/server/src/local-file.ts
46273
46663
  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";
46664
+ import { readFileSync as readFileSync23, realpathSync as realpathSync8, statSync as statSync15 } from "node:fs";
46665
+ import { homedir as homedir16 } from "node:os";
46666
+ import { isAbsolute as isAbsolute4, join as join35, resolve as resolve8, sep as sep2 } from "node:path";
46277
46667
  function isUnder(real, root) {
46278
46668
  let rootReal;
46279
46669
  try {
@@ -46300,10 +46690,10 @@ function resolveLocalFile(rawPath, roots) {
46300
46690
  }
46301
46691
  return real;
46302
46692
  }
46303
- function resolveOpenableFile(raw2, projectDir, roots, home = homedir15()) {
46693
+ function resolveOpenableFile(raw2, projectDir, roots, home = homedir16()) {
46304
46694
  const trimmed = raw2.trim().replace(/:\d+(?::\d+)?$/, "");
46305
46695
  if (!trimmed) return null;
46306
- const abs = trimmed === "~" ? home : trimmed.startsWith("~/") ? join34(home, trimmed.slice(2)) : isAbsolute4(trimmed) ? trimmed : resolve8(projectDir, trimmed);
46696
+ const abs = trimmed === "~" ? home : trimmed.startsWith("~/") ? join35(home, trimmed.slice(2)) : isAbsolute4(trimmed) ? trimmed : resolve8(projectDir, trimmed);
46307
46697
  try {
46308
46698
  return resolveLocalFile(abs, roots);
46309
46699
  } catch {
@@ -46312,7 +46702,7 @@ function resolveOpenableFile(raw2, projectDir, roots, home = homedir15()) {
46312
46702
  }
46313
46703
  function readLocalTextFile(rawPath, roots) {
46314
46704
  const path = resolveLocalFile(rawPath, roots);
46315
- const bytes = readFileSync22(path);
46705
+ const bytes = readFileSync23(path);
46316
46706
  if (bytes.subarray(0, 8192).includes(0)) throw new Error("Local file is not a text file");
46317
46707
  if (bytes.length <= MARKDOWN_READ_LIMIT) return { path, text: bytes.toString("utf8"), truncated: false };
46318
46708
  const head = bytes.subarray(0, MARKDOWN_READ_LIMIT);
@@ -46323,12 +46713,19 @@ function readLocalMarkdown(rawPath, roots) {
46323
46713
  if (!MARKDOWN_FILE_EXT.test(rawPath.trim())) throw new Error("Local file is not a Markdown file");
46324
46714
  const path = resolveLocalFile(rawPath, roots);
46325
46715
  if (!MARKDOWN_FILE_EXT.test(path)) throw new Error("Local file is not a Markdown file");
46326
- const bytes = readFileSync22(path);
46716
+ const bytes = readFileSync23(path);
46327
46717
  if (bytes.length <= MARKDOWN_READ_LIMIT) return { path, markdown: bytes.toString("utf8"), truncated: false };
46328
46718
  const head = bytes.subarray(0, MARKDOWN_READ_LIMIT);
46329
46719
  const lastBreak = head.lastIndexOf(10);
46330
46720
  return { path, markdown: head.subarray(0, lastBreak > 0 ? lastBreak : head.length).toString("utf8"), truncated: true };
46331
46721
  }
46722
+ function resolveWatchableLocalFile(rawPath, projectDir, openRoots) {
46723
+ if (MARKDOWN_FILE_EXT.test(rawPath.trim())) {
46724
+ const path = resolveLocalFile(rawPath, openRoots);
46725
+ if (MARKDOWN_FILE_EXT.test(path)) return path;
46726
+ }
46727
+ return resolveLocalFile(rawPath, [projectDir]);
46728
+ }
46332
46729
  function defaultSpawn(command, args, options) {
46333
46730
  return spawn6(command, [...args], options);
46334
46731
  }
@@ -47022,12 +47419,12 @@ var init_directory_picker = __esm({
47022
47419
  });
47023
47420
 
47024
47421
  // 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";
47422
+ import { readFileSync as readFileSync24, statSync as statSync17 } from "node:fs";
47423
+ import { join as join36, resolve as resolve9 } from "node:path";
47027
47424
  import { randomUUID as randomUUID16 } from "node:crypto";
47028
47425
  import { basename as basename9, dirname as dirname14 } from "node:path";
47029
47426
  import { existsSync as existsSync13, mkdirSync as mkdirSync16, rmSync as rmSync10, writeFileSync as writeFileSync13 } from "node:fs";
47030
- import { homedir as homedir16 } from "node:os";
47427
+ import { homedir as homedir17 } from "node:os";
47031
47428
  function validateGithubDispatchProfile(input) {
47032
47429
  validateThreadProfile(input.backend, input.model, input.effort);
47033
47430
  }
@@ -47122,6 +47519,10 @@ function holdOps(ops) {
47122
47519
  state: op.state
47123
47520
  }));
47124
47521
  }
47522
+ function cutOffHold(telemetry) {
47523
+ if (telemetry?.turn !== "in-flight") return void 0;
47524
+ return { turnInFlight: true, cutOff: true, unobservable: false, subAgents: [], subAgentCount: 0, bgShells: [], bgShellCount: 0 };
47525
+ }
47125
47526
  async function completeRegisteredThread(storage, row, terminateLive, runtime = cachedLivenessTerminator, telemetry, codex, claudeBroker) {
47126
47527
  const binding = adoptionRuntimeBinding(storage, row);
47127
47528
  if (binding.kind === "conflict") {
@@ -47130,7 +47531,7 @@ async function completeRegisteredThread(storage, row, terminateLive, runtime = c
47130
47531
  const appServerCodex = isAppServerCodexRow(row);
47131
47532
  const brokerClaude = isBrokerClaudeRow(row);
47132
47533
  const live = brokerClaude ? claudeBroker?.isDaemonAlive(row.session_id) ?? false : appServerCodex ? appServerCodexTurnLive(codex, row) : false;
47133
- const hold = live && !terminateLive ? completionConfirmationHold(telemetry) : void 0;
47534
+ const hold = terminateLive ? void 0 : live ? completionConfirmationHold(telemetry) : cutOffHold(telemetry);
47134
47535
  if (hold) return { needsConfirmation: true, hold };
47135
47536
  if (live) {
47136
47537
  await stopThreadRuntime(storage, row, runtime, codex, claudeBroker);
@@ -47183,10 +47584,10 @@ function projectCard(entry, stale) {
47183
47584
  iconIsCustom: entry.iconSource === "custom" ? true : void 0
47184
47585
  };
47185
47586
  }
47186
- function addProjectAtPath(input) {
47587
+ function addProjectAtPath(input, home = homedir17()) {
47187
47588
  const typed = input.trim();
47188
47589
  if (!typed) throw new Error("Enter a folder path.");
47189
- const expanded = typed === "~" || typed.startsWith("~/") ? join35(homedir16(), typed.slice(1)) : typed;
47590
+ const expanded = typed === "~" || typed.startsWith("~/") ? join36(home, typed.slice(1)) : typed;
47190
47591
  const absolute = resolve9(expanded);
47191
47592
  let stats;
47192
47593
  try {
@@ -47195,12 +47596,13 @@ function addProjectAtPath(input) {
47195
47596
  throw new Error(`No folder at ${absolute}`);
47196
47597
  }
47197
47598
  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));
47599
+ const root = chosenProjectRoot(absolute, home);
47600
+ if (isHomeDirectory(root, home)) throw new Error("The home folder cannot be a project \u2014 choose a folder inside it.");
47601
+ const id = ensureProjectIdFile(root, home, existingProjectId(root));
47200
47602
  const remoteOwner = resolveProjectLabel(root)?.split("/")[0];
47201
- let registered = registerProject({ dir: root, id, remoteOwner });
47603
+ let registered = registerProject({ dir: root, id, remoteOwner }, home);
47202
47604
  if (registered.action === "duplicate") {
47203
- registered = registerProject({ dir: root, id: writeProjectIdFile(root, randomUUID16()), remoteOwner });
47605
+ registered = registerProject({ dir: root, id: writeProjectIdFile(root, randomUUID16()), remoteOwner }, home);
47204
47606
  }
47205
47607
  if (!registered.entry) throw new Error("Could not register that folder.");
47206
47608
  return projectCard(registered.entry, false);
@@ -47231,7 +47633,7 @@ function storeProjectIcon(id, name, bytes) {
47231
47633
  function setProjectIconFromFile(id, file) {
47232
47634
  let bytes;
47233
47635
  try {
47234
- bytes = readFileSync23(file);
47636
+ bytes = readFileSync24(file);
47235
47637
  } catch {
47236
47638
  throw new Error("That file could not be read.");
47237
47639
  }
@@ -47239,7 +47641,7 @@ function setProjectIconFromFile(id, file) {
47239
47641
  return storeProjectIcon(id, file, bytes);
47240
47642
  }
47241
47643
  function createRouter(ctx) {
47242
- const frizzDir = join35(ctx.project.dir, ".frizz");
47644
+ const frizzDir = join36(ctx.project.dir, ".frizz");
47243
47645
  const openRoots = openableFileRoots(ctx.project);
47244
47646
  function isAutoTitledSession(slug) {
47245
47647
  return ctx.storage.getSession(slug)?.title_auto === 1;
@@ -47390,6 +47792,9 @@ function createRouter(ctx) {
47390
47792
  if (row.claude_runtime !== "broker" || !ctx.claudeBroker) {
47391
47793
  return blocked("Steering a sub-agent needs the Claude session broker; this thread predates it.");
47392
47794
  }
47795
+ if (ctx.tailer.get(slug)?.turn === "in-flight") {
47796
+ 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.");
47797
+ }
47393
47798
  return { sessionId: row.session_id };
47394
47799
  }
47395
47800
  function codexShellTarget(slug, id) {
@@ -47644,9 +48049,10 @@ function createRouter(ctx) {
47644
48049
  //
47645
48050
  // WHY THE GATE IS STRICT. Measured live: addressing a child that has ALREADY SETTLED does not
47646
48051
  // 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.
48052
+ // parent obeys it as if the operator had typed it into the thread composer. A steer sent while
48053
+ // the parent's own turn is IN FLIGHT misdelivers the same way (absorbed into that turn — see the
48054
+ // predicate). So an ungated steer is not a no-op, it is a misdelivery. `subAgentSteerable` is the
48055
+ // single predicate that decides, and the drawer's prompt box is rendered off the same answer.
47650
48056
  subAgentSteer: mutation({
47651
48057
  input: external_exports.object({ slug: ThreadSlug, id: external_exports.string(), message: external_exports.string().min(1), deliveryId: external_exports.string().optional() }).strict(),
47652
48058
  output: external_exports.object({ delivered: external_exports.boolean() }),
@@ -47915,6 +48321,7 @@ ${gapNote}` : input.message;
47915
48321
  mayHaveLiveBackgroundWork(ctx.tailer.get(input.slug))
47916
48322
  )
47917
48323
  });
48324
+ if (row.exited === 1) ctx.storage.setExitedIfCurrent(input.slug, row.session_id, row.runtime_generation ?? 0, false);
47918
48325
  if (input.freshProcess) retireOutstandingDeliveries(ctx.storage, input.slug);
47919
48326
  const preempted = input.interrupt === true && bridge.interruptTurn({ threadSlug: input.slug, sessionId: row.session_id });
47920
48327
  if (input.deliveryId) {
@@ -48191,6 +48598,19 @@ ${gapNote}` : input.message;
48191
48598
  ctx.board.refresh();
48192
48599
  }
48193
48600
  }),
48601
+ // Pin/unpin: the human lifts a thread out of the rail's band system into the pinned band at the
48602
+ // very top (or drops it back in). Same editability gate as the snooze, but NO archived refusal —
48603
+ // the pin deliberately outranks Done, so a pinned thread that finishes stays pinned until unpinned.
48604
+ setThreadPinned: mutation({
48605
+ input: SetThreadPinnedInput,
48606
+ handler: async ({ input }) => {
48607
+ currentOwnedSession(input.slug, input.sessionId);
48608
+ const thread = (await ctx.board.snapshot()).threads.find((candidate) => candidate.id === input.slug);
48609
+ if (!thread || thread.kind !== "session" || thread.foreign) throw new Error(`thread ${input.slug} is not editable`);
48610
+ ctx.storage.setPinnedAt(input.slug, input.pinned ? (/* @__PURE__ */ new Date()).toISOString() : null);
48611
+ ctx.board.refresh();
48612
+ }
48613
+ }),
48194
48614
  // Re-read the worker plugin closure INTO the live session: hooks, skills, agent profiles and MCP
48195
48615
  // servers, without restarting the process. This is `/reload-plugins` driven from the board.
48196
48616
  //
@@ -48239,7 +48659,7 @@ ${gapNote}` : input.message;
48239
48659
  throw new Error("This thread moved on; reopen it and try again");
48240
48660
  }
48241
48661
  if (input.stopHook && input.prompt?.trim() && autonomousGoal(row) === void 0) {
48242
- cancelQuestionsForAutonomy(input.slug);
48662
+ if (cancelQuestionsForAutonomy(input.slug) > 0) ctx.scheduler.kick();
48243
48663
  }
48244
48664
  ctx.board.refresh();
48245
48665
  }
@@ -48271,7 +48691,7 @@ ${gapNote}` : input.message;
48271
48691
  throw new Error(`thread ${input.slug} could not be updated`);
48272
48692
  }
48273
48693
  if (input.stopHook && input.prompt?.trim() && autonomousGoal(row) === void 0) {
48274
- cancelQuestionsForAutonomy(input.slug);
48694
+ if (cancelQuestionsForAutonomy(input.slug) > 0) ctx.scheduler.kick();
48275
48695
  }
48276
48696
  ctx.board.refresh();
48277
48697
  return { replaced };
@@ -48431,14 +48851,19 @@ ${gapNote}` : input.message;
48431
48851
  const armed = ctx.storage.listPrWatches(input.slug, { armedOnly: true });
48432
48852
  const existing = armed.find((w) => w.owner === ref.owner && w.repo === ref.repo && w.number === ref.number);
48433
48853
  const target = `${ref.owner}/${ref.repo}#${ref.number}`;
48434
- if (existing) return { id: existing.id, target, alreadyArmed: true, watches: armedPrWatchViews(input.slug) };
48854
+ if (existing) {
48855
+ const expiresAt = new Date(existing.expires_at ?? Date.now()).toISOString();
48856
+ return { id: existing.id, target, alreadyArmed: true, expiresAt, watches: armedPrWatchViews(input.slug) };
48857
+ }
48435
48858
  if (armed.length >= PR_WATCH_MAX_ARMED) {
48436
48859
  throw new Error(`this thread already watches ${armed.length} pull requests (the limit is ${PR_WATCH_MAX_ARMED}) \u2014 drop one first`);
48437
48860
  }
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)`);
48861
+ const asked = input.for === void 0 ? PR_WATCH_DEFAULT_FOR_MS : parseAwaitingDurationRaw(input.for);
48862
+ if (asked === null) {
48863
+ throw new Error(`\`for: ${input.for}\` is not a duration \u2014 give one like \`2h\`, \`3d\` or \`180d\` (max 365d)`);
48441
48864
  }
48865
+ const forMs = Math.min(asked, PR_WATCH_FOR_MAX_MS);
48866
+ const clampedFrom = input.for !== void 0 && asked > PR_WATCH_FOR_MAX_MS ? { clampedFrom: input.for } : {};
48442
48867
  const probe = await ctx.probePr(ref);
48443
48868
  if (!probe.ok) {
48444
48869
  throw new Error(
@@ -48450,7 +48875,7 @@ ${gapNote}` : input.message;
48450
48875
  ctx.storage.clearThreadDone(input.slug);
48451
48876
  ctx.storage.armPrWatch({ id, slug: input.slug, owner: ref.owner, repo: ref.repo, number: ref.number, createdAtMs: now, expiresAtMs: now + forMs });
48452
48877
  ctx.board.refresh();
48453
- return { id, target, alreadyArmed: false, watches: armedPrWatchViews(input.slug) };
48878
+ return { id, target, alreadyArmed: false, expiresAt: new Date(now + forMs).toISOString(), ...clampedFrom, watches: armedPrWatchViews(input.slug) };
48454
48879
  }
48455
48880
  }),
48456
48881
  dropOwnPrWatch: mutation({
@@ -48495,16 +48920,18 @@ ${gapNote}` : input.message;
48495
48920
  if (armed.length >= OWN_WATCH_MAX_ARMED) {
48496
48921
  throw new Error(`this thread already holds ${armed.length} watches (the limit is ${OWN_WATCH_MAX_ARMED}) \u2014 drop one first`);
48497
48922
  }
48498
- const forMs = parseAwaitingDuration(input.for);
48499
- if (forMs === null) {
48923
+ const asked = parseAwaitingDurationRaw(input.for);
48924
+ if (asked === null) {
48500
48925
  throw new Error(`\`for: ${input.for}\` is not a duration \u2014 give one like \`30m\`, \`2h\` or \`3d\` (max 24h)`);
48501
48926
  }
48927
+ const forMs = Math.min(asked, AWAITING_FOR_MAX_MS);
48928
+ const clampedFrom = asked > AWAITING_FOR_MAX_MS ? { clampedFrom: input.for } : {};
48502
48929
  const now = Date.now();
48503
48930
  const id = `wch_${randomUUID16().replace(/-/g, "").slice(0, 12)}`;
48504
48931
  ctx.storage.clearThreadDone(input.slug);
48505
48932
  ctx.storage.armThreadWatch({ id, slug: input.slug, kind: input.kind, target, createdAtMs: now, expiresAtMs: now + forMs });
48506
48933
  ctx.board.refresh();
48507
- return { id, kind: input.kind, target, alreadyArmed: false, watches: armedOwnWatchViews(input.slug) };
48934
+ return { id, kind: input.kind, target, alreadyArmed: false, ...clampedFrom, watches: armedOwnWatchViews(input.slug) };
48508
48935
  }
48509
48936
  }),
48510
48937
  dropOwnWatch: mutation({
@@ -48547,12 +48974,6 @@ Say which way you went and why in your write-up, so the human can course-correct
48547
48974
  }
48548
48975
  const faults = input.questions.flatMap((q) => askedQuestionFaults(q));
48549
48976
  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
48977
  const now = Date.now();
48557
48978
  const registered = input.questions.map((spec) => {
48558
48979
  const id = `qst_${randomUUID16().replace(/-/g, "").slice(0, 12)}`;
@@ -49191,8 +49612,8 @@ Say which way you went and why in your write-up, so the human can course-correct
49191
49612
  * Register a directory as a project, from the grid's phantom card.
49192
49613
  *
49193
49614
  * 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.
49615
+ * resolves an id, it dispatches nothing. The root comes from chosenProjectRoot — a folder inside
49616
+ * a checkout adds the checkout, but an adopted plain-directory ancestor never captures the pick.
49196
49617
  */
49197
49618
  projectAdd: mutation({
49198
49619
  input: external_exports.object({ path: external_exports.string().min(1) }),
@@ -49573,7 +49994,7 @@ var init_compression = __esm({
49573
49994
  });
49574
49995
 
49575
49996
  // packages/server/src/local-image.ts
49576
- import { readFileSync as readFileSync24, realpathSync as realpathSync9, statSync as statSync18 } from "node:fs";
49997
+ import { readFileSync as readFileSync25, realpathSync as realpathSync9, statSync as statSync18 } from "node:fs";
49577
49998
  import { extname as extname2, isAbsolute as isAbsolute5 } from "node:path";
49578
49999
  function resolveLocalImage(rawPath) {
49579
50000
  if (!rawPath || !isAbsolute5(rawPath)) return { status: 400 };
@@ -49587,7 +50008,7 @@ function resolveLocalImage(rawPath) {
49587
50008
  }
49588
50009
  try {
49589
50010
  if (!statSync18(real).isFile()) return { status: 404 };
49590
- return { status: 200, contentType, body: readFileSync24(real) };
50011
+ return { status: 200, contentType, body: readFileSync25(real) };
49591
50012
  } catch {
49592
50013
  return { status: 404 };
49593
50014
  }
@@ -49607,9 +50028,9 @@ var init_local_image = __esm({
49607
50028
  });
49608
50029
 
49609
50030
  // 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";
50031
+ import { readFileSync as readFileSync26, readdirSync as readdirSync10, realpathSync as realpathSync10, statSync as statSync19 } from "node:fs";
50032
+ import { homedir as homedir18 } from "node:os";
50033
+ import { basename as basename10, dirname as dirname15, extname as extname3, join as join37, relative as relative2, sep as sep3 } from "node:path";
49613
50034
  function normalizeStem(stem) {
49614
50035
  const sized = stem.toLowerCase().replace(/[@-]\d+(\.\d+)?x(\d+)?$/u, "").replace(/[-_]\d{2,4}$/u, "");
49615
50036
  const normalized = sized.replace(VARIANT_SUFFIX, "");
@@ -49627,7 +50048,7 @@ function sizeScore(dimensions) {
49627
50048
  return Math.max(0, Math.min(40, Math.round(Math.log2(edge / 24) * 8)));
49628
50049
  }
49629
50050
  function under(root, relativeDirectory) {
49630
- return relativeDirectory ? join36(root, relativeDirectory) : root;
50051
+ return relativeDirectory ? join37(root, relativeDirectory) : root;
49631
50052
  }
49632
50053
  function listFiles(directory) {
49633
50054
  try {
@@ -49649,19 +50070,19 @@ function iconDirectories(root) {
49649
50070
  for (const asset of ASSET_DIRECTORIES) directories.push(under(root, asset));
49650
50071
  for (const host of HOST_DIRECTORIES) {
49651
50072
  if (!children2.has(host)) continue;
49652
- for (const asset of ASSET_DIRECTORIES) directories.push(under(join36(root, host), asset));
50073
+ for (const asset of ASSET_DIRECTORIES) directories.push(under(join37(root, host), asset));
49653
50074
  }
49654
50075
  for (const extra of EXTRA_DIRECTORIES) directories.push(under(root, extra));
49655
50076
  for (const container of WORKSPACE_CONTAINERS) {
49656
50077
  if (!children2.has(container)) continue;
49657
50078
  let members;
49658
50079
  try {
49659
- members = readdirSync10(join36(root, container), { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name);
50080
+ members = readdirSync10(join37(root, container), { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name);
49660
50081
  } catch {
49661
50082
  continue;
49662
50083
  }
49663
50084
  for (const member of members) {
49664
- for (const asset of ASSET_DIRECTORIES) directories.push(under(join36(root, container, member), asset));
50085
+ for (const asset of ASSET_DIRECTORIES) directories.push(under(join37(root, container, member), asset));
49665
50086
  }
49666
50087
  }
49667
50088
  return [...new Set(directories)];
@@ -49669,13 +50090,13 @@ function iconDirectories(root) {
49669
50090
  function manifestIcons(manifestPath) {
49670
50091
  let parsed;
49671
50092
  try {
49672
- parsed = JSON.parse(readFileSync25(manifestPath, "utf8"));
50093
+ parsed = JSON.parse(readFileSync26(manifestPath, "utf8"));
49673
50094
  } catch {
49674
50095
  return [];
49675
50096
  }
49676
50097
  if (!Array.isArray(parsed?.icons)) return [];
49677
50098
  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, "")));
50099
+ 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
50100
  }
49680
50101
  function projectIconCandidates(root) {
49681
50102
  const projectStem = normalizeStem(basename10(root)).normalized;
@@ -49694,7 +50115,7 @@ function projectIconCandidates(root) {
49694
50115
  for (const name of listFiles(directory)) {
49695
50116
  const extension2 = extname3(name).toLowerCase();
49696
50117
  if (MANIFEST_NAMES.includes(name.toLowerCase())) {
49697
- for (const icon of manifestIcons(join36(directory, name))) {
50118
+ for (const icon of manifestIcons(join37(directory, name))) {
49698
50119
  if (MEASURABLE_IMAGE_EXTENSIONS.has(extname3(icon).toLowerCase())) {
49699
50120
  fromManifest.add(icon);
49700
50121
  files.push({ path: icon, directory: dirname15(icon) });
@@ -49705,7 +50126,7 @@ function projectIconCandidates(root) {
49705
50126
  if (!MEASURABLE_IMAGE_EXTENSIONS.has(extension2)) continue;
49706
50127
  const stem = name.slice(0, name.length - extension2.length);
49707
50128
  if (normalizeStem(stem).normalized === "favicon") iconHomes.add(directory);
49708
- files.push({ path: join36(directory, name), directory });
50129
+ files.push({ path: join37(directory, name), directory });
49709
50130
  if (files.length >= MAX_CANDIDATES) break;
49710
50131
  }
49711
50132
  }
@@ -49747,7 +50168,7 @@ function iconMediaType(path) {
49747
50168
  }
49748
50169
  function resolveProjectIconResponse(id, options = {}) {
49749
50170
  if (!id) return { status: 400 };
49750
- const home = options.home ?? homedir17();
50171
+ const home = options.home ?? homedir18();
49751
50172
  const entry = readRegistry(home).projects.find((project) => project.id === id);
49752
50173
  if (!entry) return { status: 404 };
49753
50174
  const path = resolveProjectIcon(id, (root) => detectProjectIcon(root)?.path, { home });
@@ -49770,7 +50191,7 @@ function resolveProjectIconResponse(id, options = {}) {
49770
50191
  });
49771
50192
  if (!roots.some((root) => real === root || real.startsWith(root + sep3))) return { status: 404 };
49772
50193
  try {
49773
- return { status: 200, contentType, body: readFileSync25(real) };
50194
+ return { status: 200, contentType, body: readFileSync26(real) };
49774
50195
  } catch {
49775
50196
  return { status: 404 };
49776
50197
  }
@@ -49882,8 +50303,8 @@ var init_project_icon = __esm({
49882
50303
  });
49883
50304
 
49884
50305
  // 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";
50306
+ import { readdirSync as readdirSync11, readFileSync as readFileSync27, realpathSync as realpathSync11, statSync as statSync20 } from "node:fs";
50307
+ import { join as join38, sep as sep4 } from "node:path";
49887
50308
  function children(path, pattern) {
49888
50309
  try {
49889
50310
  return readdirSync11(path, { withFileTypes: true }).filter((entry) => entry.isDirectory() && pattern.test(entry.name)).map((entry) => entry.name).sort().reverse();
@@ -49895,7 +50316,7 @@ function isUnder2(path, root) {
49895
50316
  return path === root || path.startsWith(root.endsWith(sep4) ? root : root + sep4);
49896
50317
  }
49897
50318
  function resolveFragment(projectDir, sessionId, file) {
49898
- const base = join37(projectDir, ".codex", "visualizations");
50319
+ const base = join38(projectDir, ".codex", "visualizations");
49899
50320
  let projectReal;
49900
50321
  let baseReal;
49901
50322
  try {
@@ -49906,12 +50327,12 @@ function resolveFragment(projectDir, sessionId, file) {
49906
50327
  }
49907
50328
  if (!isUnder2(baseReal, projectReal)) return null;
49908
50329
  for (const year of children(base, YEAR_PART)) {
49909
- const yearDir = join37(base, year);
50330
+ const yearDir = join38(base, year);
49910
50331
  for (const month of children(yearDir, DATE_PART)) {
49911
- const monthDir = join37(yearDir, month);
50332
+ const monthDir = join38(yearDir, month);
49912
50333
  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);
50334
+ const sessionRoot = join38(baseReal, year, month, day, sessionId);
50335
+ const candidate = join38(monthDir, day, sessionId, file);
49915
50336
  try {
49916
50337
  const real = realpathSync11(candidate);
49917
50338
  if (isUnder2(real, sessionRoot) && statSync20(real).isFile()) return real;
@@ -49997,7 +50418,7 @@ function resolveLocalVisualization(projectDir, sessionId, file) {
49997
50418
  try {
49998
50419
  const size = statSync20(path).size;
49999
50420
  if (size > MAX_FRAGMENT_BYTES) return { status: 413 };
50000
- fragment = readFileSync26(path, "utf8");
50421
+ fragment = readFileSync27(path, "utf8");
50001
50422
  } catch {
50002
50423
  return { status: 404 };
50003
50424
  }
@@ -50040,7 +50461,7 @@ var init_local_visualization = __esm({
50040
50461
 
50041
50462
  // packages/server/src/app.ts
50042
50463
  import { mkdirSync as mkdirSync17, writeFileSync as writeFileSync14 } from "node:fs";
50043
- import { join as join38 } from "node:path";
50464
+ import { join as join39 } from "node:path";
50044
50465
  import { randomUUID as randomUUID17 } from "node:crypto";
50045
50466
  function createApp(ctx, options = {}) {
50046
50467
  const app = new Hono2();
@@ -50144,10 +50565,10 @@ function createApp(ctx, options = {}) {
50144
50565
  if (typeof body.data !== "string" || body.data.length > ATTACHMENT_MAX_BASE64_CHARS) return c.json({ error: "bad payload" }, 400);
50145
50566
  const ext = `.${attachmentExtension(name)}`;
50146
50567
  const buf = Buffer.from(body.data, "base64");
50147
- const dir = join38(ctx.project.stateDir, "attachments");
50568
+ const dir = join39(ctx.project.stateDir, "attachments");
50148
50569
  mkdirSync17(dir, { recursive: true });
50149
50570
  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}`);
50571
+ const path = join39(dir, `${Date.now()}-${randomUUID17().slice(0, 8)}-${base}${ext}`);
50151
50572
  writeFileSync14(path, buf);
50152
50573
  return c.json({ path });
50153
50574
  });
@@ -50634,6 +51055,7 @@ function createAppSocketServer(deps) {
50634
51055
  const maxOutputBufferBytes = deps.maxOutputBufferBytes ?? APP_SOCKET_MAX_OUTPUT_BUFFER_BYTES;
50635
51056
  const maxConnections = boundedCount(deps.maxConnections, APP_SOCKET_MAX_CONNECTIONS);
50636
51057
  const maxSubscriptions = deps.maxSubscriptionsPerConnection ?? APP_SOCKET_MAX_SUBSCRIPTIONS;
51058
+ const maxFileWatches = boundedCount(deps.maxFileWatchesPerConnection, APP_SOCKET_MAX_FILE_WATCHES);
50637
51059
  const maxMessagesPerWindow = deps.maxMessagesPerWindow ?? APP_SOCKET_MAX_MESSAGES_PER_WINDOW;
50638
51060
  const messageWindowMs = deps.messageWindowMs ?? APP_SOCKET_MESSAGE_WINDOW_MS;
50639
51061
  const maxBufferedKeyframeEvents = deps.maxBufferedKeyframeEvents ?? APP_SOCKET_MAX_BUFFERED_KEYFRAME_EVENTS;
@@ -50673,6 +51095,55 @@ function createAppSocketServer(deps) {
50673
51095
  let pendingUpgrades = 0;
50674
51096
  const socketOrigins = /* @__PURE__ */ new WeakMap();
50675
51097
  const lastSig = /* @__PURE__ */ new Map();
51098
+ const fileWatches = /* @__PURE__ */ new WeakMap();
51099
+ let fileWatchCount = 0;
51100
+ function subscribeFile(ws, path) {
51101
+ let held = fileWatches.get(ws);
51102
+ if (!held) {
51103
+ held = /* @__PURE__ */ new Map();
51104
+ fileWatches.set(ws, held);
51105
+ }
51106
+ if (held.has(path)) return;
51107
+ if (held.size >= maxFileWatches) {
51108
+ closeSocket(ws, 1008, "subscription limit exceeded");
51109
+ return;
51110
+ }
51111
+ const watchFile = deps.watchFile;
51112
+ if (!watchFile) return;
51113
+ let release;
51114
+ try {
51115
+ release = watchFile(path, () => {
51116
+ sendMsg(ws, { t: "file-changed", path });
51117
+ });
51118
+ } catch {
51119
+ return;
51120
+ }
51121
+ held.set(path, release);
51122
+ fileWatchCount++;
51123
+ }
51124
+ function unsubscribeFile(ws, path) {
51125
+ const held = fileWatches.get(ws);
51126
+ const release = held?.get(path);
51127
+ if (!held || !release) return;
51128
+ held.delete(path);
51129
+ fileWatchCount--;
51130
+ try {
51131
+ release();
51132
+ } catch {
51133
+ }
51134
+ }
51135
+ function dropFileWatches(ws) {
51136
+ const held = fileWatches.get(ws);
51137
+ if (!held) return;
51138
+ fileWatches.delete(ws);
51139
+ for (const release of held.values()) {
51140
+ fileWatchCount--;
51141
+ try {
51142
+ release();
51143
+ } catch {
51144
+ }
51145
+ }
51146
+ }
50676
51147
  function closeSocket(ws, code, reason) {
50677
51148
  if (ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) return;
50678
51149
  try {
@@ -51145,6 +51616,11 @@ function createAppSocketServer(deps) {
51145
51616
  return;
51146
51617
  }
51147
51618
  const msg = parsed.data;
51619
+ if (msg.topic === "file") {
51620
+ if (msg.t === "sub") subscribeFile(ws, msg.path);
51621
+ else unsubscribeFile(ws, msg.path);
51622
+ return;
51623
+ }
51148
51624
  if (msg.t === "sub") {
51149
51625
  if (registry.isSubscribed(ws, msg.slug)) return;
51150
51626
  if (registry.slugsFor(ws).length >= maxSubscriptions) {
@@ -51165,6 +51641,7 @@ function createAppSocketServer(deps) {
51165
51641
  }
51166
51642
  clearInterval(heartbeat);
51167
51643
  cancelPendingSubscriptionPush(ws);
51644
+ dropFileWatches(ws);
51168
51645
  const terminationTimer = terminationTimers.get(ws);
51169
51646
  if (terminationTimer) clearTimeout(terminationTimer);
51170
51647
  terminationTimers.delete(ws);
@@ -51288,10 +51765,13 @@ function createAppSocketServer(deps) {
51288
51765
  },
51289
51766
  get pendingTranscriptRefreshes() {
51290
51767
  return pendingTranscriptRefreshes.size;
51768
+ },
51769
+ get fileWatchCount() {
51770
+ return fileWatchCount;
51291
51771
  }
51292
51772
  };
51293
51773
  }
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;
51774
+ 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
51775
  var init_app_socket = __esm({
51296
51776
  "packages/server/src/app-socket.ts"() {
51297
51777
  "use strict";
@@ -51374,6 +51854,7 @@ var init_app_socket = __esm({
51374
51854
  APP_SOCKET_MAX_OUTPUT_BUFFER_BYTES = 4 * 1024 * 1024;
51375
51855
  APP_SOCKET_MAX_CONNECTIONS = 64;
51376
51856
  APP_SOCKET_MAX_SUBSCRIPTIONS = 32;
51857
+ APP_SOCKET_MAX_FILE_WATCHES = 16;
51377
51858
  APP_SOCKET_MAX_MESSAGES_PER_WINDOW = 120;
51378
51859
  APP_SOCKET_MESSAGE_WINDOW_MS = 1e3;
51379
51860
  APP_SOCKET_MAX_BUFFERED_KEYFRAME_EVENTS = 256;
@@ -51385,11 +51866,60 @@ var init_app_socket = __esm({
51385
51866
  }
51386
51867
  });
51387
51868
 
51869
+ // packages/server/src/local-file-watch.ts
51870
+ import { watch } from "node:fs";
51871
+ import { basename as basename11, dirname as dirname16 } from "node:path";
51872
+ function close(path) {
51873
+ const entry = entries.get(path);
51874
+ if (!entry) return;
51875
+ entries.delete(path);
51876
+ if (entry.timer) clearTimeout(entry.timer);
51877
+ try {
51878
+ entry.watcher.close();
51879
+ } catch {
51880
+ }
51881
+ }
51882
+ function watchLocalFile(path, onChange) {
51883
+ let entry = entries.get(path);
51884
+ if (!entry) {
51885
+ const name = basename11(path);
51886
+ const watcher2 = watch(dirname16(path), { persistent: false }, (_event, changed) => {
51887
+ if (changed !== null && changed !== void 0 && changed.toString() !== name) return;
51888
+ const current = entries.get(path);
51889
+ if (!current) return;
51890
+ if (current.timer) clearTimeout(current.timer);
51891
+ current.timer = setTimeout(() => {
51892
+ current.timer = null;
51893
+ for (const listener of current.listeners) listener();
51894
+ }, SETTLE_MS);
51895
+ current.timer.unref?.();
51896
+ });
51897
+ entry = { watcher: watcher2, listeners: /* @__PURE__ */ new Set(), timer: null };
51898
+ watcher2.on("error", () => close(path));
51899
+ entries.set(path, entry);
51900
+ }
51901
+ entry.listeners.add(onChange);
51902
+ return () => {
51903
+ const current = entries.get(path);
51904
+ if (!current) return;
51905
+ current.listeners.delete(onChange);
51906
+ if (current.listeners.size === 0) close(path);
51907
+ };
51908
+ }
51909
+ var SETTLE_MS, entries;
51910
+ var init_local_file_watch = __esm({
51911
+ "packages/server/src/local-file-watch.ts"() {
51912
+ "use strict";
51913
+ SETTLE_MS = 50;
51914
+ entries = /* @__PURE__ */ new Map();
51915
+ }
51916
+ });
51917
+
51388
51918
  // 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";
51919
+ import { mkdirSync as mkdirSync18, readFileSync as readFileSync28, renameSync as renameSync10, rmSync as rmSync11, writeFileSync as writeFileSync15 } from "node:fs";
51920
+ import { basename as basename12, dirname as dirname17, join as join40 } from "node:path";
51391
51921
  function bootProgressPath(stateDir) {
51392
- return join39(stateDir, BOOT_PROGRESS_NAME);
51922
+ return join40(stateDir, BOOT_PROGRESS_NAME);
51393
51923
  }
51394
51924
  function createBootProgressPublisher(stateDir, minIntervalMs = 200) {
51395
51925
  if (!stateDir) {
@@ -51409,8 +51939,8 @@ function createBootProgressPublisher(stateDir, minIntervalMs = 200) {
51409
51939
  lastAt = now;
51410
51940
  step++;
51411
51941
  try {
51412
- mkdirSync18(dirname16(path), { recursive: true, mode: 448 });
51413
- const temp = join39(dirname16(path), `.${basename11(path)}.${process.pid}.tmp`);
51942
+ mkdirSync18(dirname17(path), { recursive: true, mode: 448 });
51943
+ const temp = join40(dirname17(path), `.${basename12(path)}.${process.pid}.tmp`);
51414
51944
  writeFileSync15(temp, `${JSON.stringify({ pid: process.pid, step, phase, at: new Date(now).toISOString() })}
51415
51945
  `, "utf8");
51416
51946
  renameSync10(temp, path);
@@ -51963,17 +52493,17 @@ var init_legacy_project_db = __esm({
51963
52493
 
51964
52494
  // packages/server/src/frizz-db.ts
51965
52495
  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";
52496
+ import { basename as basename13, dirname as dirname18, join as join41 } from "node:path";
51967
52497
  function frizzDatabaseLocation(options) {
51968
52498
  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 };
52499
+ const projectsDir = dirname18(options.stateDir);
52500
+ if (basename13(projectsDir) === "projects") {
52501
+ return { path: options.path ?? join41(dirname18(projectsDir), "ui.db"), projectsDir };
51972
52502
  }
51973
- return { path: options.path ?? join40(options.stateDir, "ui.db"), projectsDir: void 0 };
52503
+ return { path: options.path ?? join41(options.stateDir, "ui.db"), projectsDir: void 0 };
51974
52504
  }
51975
52505
  const data = options.home ? frizzPaths({ home: options.home }).data : frizzRoots().data;
51976
- return { path: options.path ?? join40(data, "ui.db"), projectsDir: join40(data, "projects") };
52506
+ return { path: options.path ?? join41(data, "ui.db"), projectsDir: join41(data, "projects") };
51977
52507
  }
51978
52508
  function ensureFrizzSchema(db) {
51979
52509
  db.exec(`
@@ -51992,7 +52522,7 @@ function ensureFrizzSchema(db) {
51992
52522
  }
51993
52523
  function openFrizzDatabase(options = {}) {
51994
52524
  const { path, projectsDir } = frizzDatabaseLocation(options);
51995
- mkdirSync19(dirname17(path), { recursive: true });
52525
+ mkdirSync19(dirname18(path), { recursive: true });
51996
52526
  const db = new sqlite_default(path);
51997
52527
  db.pragma("busy_timeout = 5000");
51998
52528
  db.pragma("journal_mode = WAL");
@@ -52004,10 +52534,10 @@ function openFrizzDatabase(options = {}) {
52004
52534
  );
52005
52535
  for (const projectId of retiredLegacyProjectIds(projectsDir)) {
52006
52536
  if (alreadyImported.has(projectId)) continue;
52007
- const dir = join40(projectsDir, projectId);
52537
+ const dir = join41(projectsDir, projectId);
52008
52538
  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());
52539
+ renameSync11(join41(dir, "ui.db.imported"), join41(dir, "ui.db"));
52540
+ 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
52541
  alreadyImported.add(projectId);
52012
52542
  log.info("frizz-db", `${projectId}: restored ui.db.imported to ui.db and recorded it as imported`);
52013
52543
  } catch (error) {
@@ -52017,7 +52547,7 @@ function openFrizzDatabase(options = {}) {
52017
52547
  }
52018
52548
  for (const projectId of legacyProjectIds(projectsDir)) {
52019
52549
  if (alreadyImported.has(projectId)) continue;
52020
- const legacyPath = join40(projectsDir, projectId, "ui.db");
52550
+ const legacyPath = join41(projectsDir, projectId, "ui.db");
52021
52551
  try {
52022
52552
  const rows = importLegacyProject(db, projectId, legacyPath);
52023
52553
  imported.push({ projectId, rows });
@@ -52050,7 +52580,7 @@ function projectIdsWithFile(projectsDir, file) {
52050
52580
  if (!existsSync14(projectsDir)) return [];
52051
52581
  return readdirSync12(projectsDir).filter((name) => {
52052
52582
  try {
52053
- return statSync21(join40(projectsDir, name, file)).isFile();
52583
+ return statSync21(join41(projectsDir, name, file)).isFile();
52054
52584
  } catch {
52055
52585
  return false;
52056
52586
  }
@@ -52060,7 +52590,7 @@ function legacyProjectIds(projectsDir) {
52060
52590
  return projectIdsWithFile(projectsDir, "ui.db");
52061
52591
  }
52062
52592
  function retiredLegacyProjectIds(projectsDir) {
52063
- return projectIdsWithFile(projectsDir, "ui.db.imported").filter((id) => !existsSync14(join40(projectsDir, id, "ui.db")));
52593
+ return projectIdsWithFile(projectsDir, "ui.db.imported").filter((id) => !existsSync14(join41(projectsDir, id, "ui.db")));
52064
52594
  }
52065
52595
  function tableColumns(db, schema, table) {
52066
52596
  return db.prepare(`PRAGMA ${schema}.table_info(${table})`).all().map((c) => c.name);
@@ -52213,8 +52743,8 @@ var init_tenant_prime = __esm({
52213
52743
 
52214
52744
  // packages/server/src/project-teardown.ts
52215
52745
  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";
52746
+ import { homedir as homedir19 } from "node:os";
52747
+ import { dirname as dirname19, resolve as resolve10 } from "node:path";
52218
52748
  async function stopProjectWorkers(ctx) {
52219
52749
  let stopped = 0;
52220
52750
  for (const row of ctx.storage.allSessions()) {
@@ -52229,12 +52759,12 @@ async function stopProjectWorkers(ctx) {
52229
52759
  }
52230
52760
  return stopped;
52231
52761
  }
52232
- function deleteProjectState(projectId, home = homedir18()) {
52762
+ function deleteProjectState(projectId, home = homedir19()) {
52233
52763
  const target = projectStateDir(projectId, home);
52234
52764
  if (!/^[\w.-]+$/u.test(projectId) || projectId === "." || projectId === "..") {
52235
52765
  throw new Error(`Refusing to delete state for a malformed project id: ${projectId}`);
52236
52766
  }
52237
- if (dirname18(resolve10(target)) !== resolve10(dirname18(projectStateDir("x", home)))) {
52767
+ if (dirname19(resolve10(target)) !== resolve10(dirname19(projectStateDir("x", home)))) {
52238
52768
  throw new Error(`Refusing to delete ${target} \u2014 it is not a project state directory`);
52239
52769
  }
52240
52770
  rmSync12(target, { recursive: true, force: true });
@@ -52262,11 +52792,11 @@ __export(index_exports, {
52262
52792
  unknownProjectPage: () => unknownProjectPage
52263
52793
  });
52264
52794
  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";
52795
+ import { readFileSync as readFileSync29, existsSync as existsSync15 } from "node:fs";
52796
+ import { dirname as dirname20, join as join42, resolve as resolve11, extname as extname4, normalize } from "node:path";
52267
52797
  function readServerAddressHolder(path) {
52268
52798
  try {
52269
- const value = JSON.parse(readFileSync28(path, "utf8"));
52799
+ const value = JSON.parse(readFileSync29(path, "utf8"));
52270
52800
  if (typeof value?.pid !== "number") return void 0;
52271
52801
  return { pid: value.pid, port: typeof value.port === "number" ? value.port : -1 };
52272
52802
  } catch {
@@ -52366,11 +52896,11 @@ function createShutdownSignalHandler(options) {
52366
52896
  }
52367
52897
  function serveStatic(distDir, req, res) {
52368
52898
  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");
52899
+ let file = join42(distDir, rel === "/" ? "index.html" : rel);
52900
+ if (!file.startsWith(distDir)) file = join42(distDir, "index.html");
52901
+ if (!existsSync15(file)) file = join42(distDir, "index.html");
52372
52902
  try {
52373
- const body = readFileSync28(file);
52903
+ const body = readFileSync29(file);
52374
52904
  res.writeHead(200, { "content-type": MIME[extname4(file)] ?? "application/octet-stream" });
52375
52905
  res.end(body);
52376
52906
  } catch {
@@ -52472,7 +53002,11 @@ async function startServer(opts = {}) {
52472
53002
  // The /ws producer is the one the live UI actually renders, so a dead owner has to reach it
52473
53003
  // too — projecting only the RPC is the exact half-fix the × already had to correct once.
52474
53004
  (slug) => c.tailer.ownerGone?.(slug) ?? false
52475
- )
53005
+ ),
53006
+ // A reader's live watch on the file it shows. Gated as the read is (Markdown through the reader
53007
+ // roots, anything else through the project directory alone) and watched by CANONICAL path, so a
53008
+ // symlinked `.md` and its target are one watch.
53009
+ watchFile: (path, onChange) => watchLocalFile(resolveWatchableLocalFile(path, c.project.dir, openableFileRoots(c.project)), onChange)
52476
53010
  });
52477
53011
  let githubInit;
52478
53012
  let terminal;
@@ -52616,7 +53150,7 @@ async function startServer(opts = {}) {
52616
53150
  if (statusPath && statusIdentity) runtime.removeStatus(statusPath, statusIdentity);
52617
53151
  if (statusPath && statusIdentity) {
52618
53152
  try {
52619
- runtime.removeStatus(serverAddressPathForStateDir(dirname19(statusPath)), statusIdentity);
53153
+ runtime.removeStatus(serverAddressPathForStateDir(dirname20(statusPath)), statusIdentity);
52620
53154
  } catch {
52621
53155
  }
52622
53156
  }
@@ -52745,7 +53279,7 @@ async function startServer(opts = {}) {
52745
53279
  }
52746
53280
  statusPath = serverLockPathFor(ctx.project);
52747
53281
  const webRoot = resolve11(import.meta.dirname, "..", "..", "web");
52748
- const distDir = opts.webDistDir ? resolve11(opts.webDistDir) : join41(webRoot, "dist");
53282
+ const distDir = opts.webDistDir ? resolve11(opts.webDistDir) : join42(webRoot, "dist");
52749
53283
  startupPhase = "Vite";
52750
53284
  if (opts.dev) {
52751
53285
  try {
@@ -52821,7 +53355,7 @@ async function startServer(opts = {}) {
52821
53355
  if (vite) {
52822
53356
  vite.middlewares(req, res, () => {
52823
53357
  try {
52824
- const html = readFileSync28(join41(webRoot, "index.html"), "utf8");
53358
+ const html = readFileSync29(join42(webRoot, "index.html"), "utf8");
52825
53359
  void vite.transformIndexHtml(url, html).then((out) => {
52826
53360
  res.writeHead(200, { "content-type": "text/html" });
52827
53361
  res.end(out);
@@ -52975,6 +53509,8 @@ var init_index = __esm({
52975
53509
  init_app_socket();
52976
53510
  init_shutdown();
52977
53511
  init_project();
53512
+ init_local_file();
53513
+ init_local_file_watch();
52978
53514
  init_project_launch();
52979
53515
  init_frizz_paths();
52980
53516
  init_project_identity();
@@ -53026,7 +53562,7 @@ var init_index = __esm({
53026
53562
  }
53027
53563
  };
53028
53564
  isApiUrl = (url) => url === FRIZZ_ROUTE_PREFIX || url.startsWith(`${FRIZZ_ROUTE_PREFIX}/`);
53029
- serverLockPathFor = (project) => join41(project.stateDir, "server.lock");
53565
+ serverLockPathFor = (project) => join42(project.stateDir, "server.lock");
53030
53566
  MIME = {
53031
53567
  ".html": "text/html",
53032
53568
  ".js": "text/javascript",
@@ -53045,7 +53581,7 @@ init_project_launch();
53045
53581
  init_shutdown();
53046
53582
  init_logging();
53047
53583
  import { existsSync as existsSync16 } from "node:fs";
53048
- import { join as join42 } from "node:path";
53584
+ import { join as join43 } from "node:path";
53049
53585
  process.on("uncaughtException", (error) => {
53050
53586
  log.error("dev-child", `uncaught exception: ${error instanceof Error ? error.stack ?? error.message : error}`);
53051
53587
  process.exit(1);
@@ -53085,7 +53621,7 @@ try {
53085
53621
  ];
53086
53622
  if (!existsSync16(stableWebDist)) throw new Error("stable artifact launch is missing its verified web directory");
53087
53623
  for (const [name, directory, requiredFile] of required) {
53088
- if (!directory || !existsSync16(join42(directory, requiredFile)))
53624
+ if (!directory || !existsSync16(join43(directory, requiredFile)))
53089
53625
  throw new Error(`stable artifact launch is missing verified ${name}`);
53090
53626
  }
53091
53627
  }