frizz-server 0.13.7 → 0.13.9

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 (93) hide show
  1. package/dist/claude-agent-broker.js +14659 -14376
  2. package/dist/dev-child.js +1978 -350
  3. package/package.json +1 -1
  4. package/web-dist/assets/{TerminalPane-P7iAI_rU.js → TerminalPane-uhdLUx67.js} +1 -1
  5. package/web-dist/assets/{abnfDiagram-VRR7QNED-B0JxaKjB.js → abnfDiagram-VRR7QNED-gpWvs3KP.js} +1 -1
  6. package/web-dist/assets/architecture-TIHT7OUA-DjmwW7vl.js +1 -0
  7. package/web-dist/assets/{architectureDiagram-ZJ3FMSHR-E8bWaWhF.js → architectureDiagram-ZJ3FMSHR-Di1xsjNG.js} +1 -1
  8. package/web-dist/assets/{blockDiagram-677ZJIJ3-BNg7FcxS.js → blockDiagram-677ZJIJ3-CO85-Qea.js} +1 -1
  9. package/web-dist/assets/{c4Diagram-LMCZKHZV-BHkry-JG.js → c4Diagram-LMCZKHZV-B32PSJlR.js} +1 -1
  10. package/web-dist/assets/channel-2QEC6BP8.js +1 -0
  11. package/web-dist/assets/{chunk-32BRIVSS-BJLx0sIU.js → chunk-32BRIVSS-B-7ltfTH.js} +1 -1
  12. package/web-dist/assets/{chunk-52WLFC77-DmMATfuh.js → chunk-52WLFC77-B68qDE3i.js} +1 -1
  13. package/web-dist/assets/{chunk-C7G6YPKG-Ceg42lXA.js → chunk-C7G6YPKG-9I1vJXzs.js} +1 -1
  14. package/web-dist/assets/{chunk-EX3LRPZG-CAx16zp2.js → chunk-EX3LRPZG-50O1GLJq.js} +1 -1
  15. package/web-dist/assets/{chunk-FWX5IMBZ-DWb22U8j.js → chunk-FWX5IMBZ-DavZMlKl.js} +2 -2
  16. package/web-dist/assets/{chunk-HOUHSVGY-bKZVUQyT.js → chunk-HOUHSVGY-BGjNDQbr.js} +1 -1
  17. package/web-dist/assets/{chunk-ICXQ74PX-C3i_eW5g.js → chunk-ICXQ74PX-D4yFEGU8.js} +1 -1
  18. package/web-dist/assets/{chunk-MOJQB5TN-Bt6x6J8D.js → chunk-MOJQB5TN-CsQKxzjy.js} +1 -1
  19. package/web-dist/assets/{chunk-OGEWGWER-BwJ73hmt.js → chunk-OGEWGWER-B2ujmhTF.js} +1 -1
  20. package/web-dist/assets/{chunk-PUDLZKDR-DfcEHpjL.js → chunk-PUDLZKDR-B_Cnk_kp.js} +1 -1
  21. package/web-dist/assets/{chunk-Q4XR5HBZ-VYeS43tP.js → chunk-Q4XR5HBZ-CNJi7ivq.js} +1 -1
  22. package/web-dist/assets/{chunk-V7JOEXUC-Cs1_YKkF.js → chunk-V7JOEXUC-qL-RXxQy.js} +1 -1
  23. package/web-dist/assets/{chunk-VAUOI2AC-CCOn0B9D.js → chunk-VAUOI2AC-C_AO-SE_.js} +1 -1
  24. package/web-dist/assets/{chunk-VR4S4FIN-CL_PLrUe.js → chunk-VR4S4FIN-NU1W8e1J.js} +1 -1
  25. package/web-dist/assets/{chunk-WYO6CB5R-Dupt_rk_.js → chunk-WYO6CB5R-B3l7AohL.js} +1 -1
  26. package/web-dist/assets/{chunk-ZGVPDNZ5-UwgE4khW.js → chunk-ZGVPDNZ5-gw3883_4.js} +1 -1
  27. package/web-dist/assets/classDiagram-OUVF2IWQ-BZmKD91m.js +1 -0
  28. package/web-dist/assets/classDiagram-v2-EOCWNBFH-BZmKD91m.js +1 -0
  29. package/web-dist/assets/{cynefin-VYW2F7L2-DGZJYJNa.js → cynefin-VYW2F7L2-IYSCSMTe.js} +1 -1
  30. package/web-dist/assets/{cynefinDiagram-TSTJHNR4-B34DgCmC.js → cynefinDiagram-TSTJHNR4-CP8LReQc.js} +1 -1
  31. package/web-dist/assets/{dagre-VKFMJZFB-C7HODJG3.js → dagre-VKFMJZFB-C_cDOY4h.js} +1 -1
  32. package/web-dist/assets/{diagram-FQU43EPY-CunP-lqf.js → diagram-FQU43EPY-Bdt-bbPx.js} +1 -1
  33. package/web-dist/assets/{diagram-G47NLZAW-C3O84bOk.js → diagram-G47NLZAW-D4bM_nDs.js} +1 -1
  34. package/web-dist/assets/{diagram-NH7WQ7WH-Dv0TR4Ic.js → diagram-NH7WQ7WH-_TSAwrFN.js} +1 -1
  35. package/web-dist/assets/{diagram-OA4YK3LP-CUvcZIxp.js → diagram-OA4YK3LP-DWpUifZA.js} +1 -1
  36. package/web-dist/assets/{diagram-WEI45ONY-CwPr97NN.js → diagram-WEI45ONY-29lN_-MQ.js} +1 -1
  37. package/web-dist/assets/{ebnfDiagram-CCIWWBDH-Dq2O1mOC.js → ebnfDiagram-CCIWWBDH-vNac3Nkb.js} +1 -1
  38. package/web-dist/assets/{erDiagram-Q63AITRT-C6dJ8Dpx.js → erDiagram-Q63AITRT-Q8Lyeen0.js} +1 -1
  39. package/web-dist/assets/eventmodeling-45OFAUF4-DOaq-8am.js +1 -0
  40. package/web-dist/assets/flowDiagram-23GEKE2U-DIB-knUj.js +1 -0
  41. package/web-dist/assets/{ganttDiagram-NO4QXBWP-sa-TSsB5.js → ganttDiagram-NO4QXBWP-wqnUf4eS.js} +1 -1
  42. package/web-dist/assets/{gitGraph-TEB2WS4Q-BdZIuH-o.js → gitGraph-TEB2WS4Q-Dp2PnWq2.js} +1 -1
  43. package/web-dist/assets/{gitGraphDiagram-IHSO6WYX-C__OynR9.js → gitGraphDiagram-IHSO6WYX-BxGiqVQZ.js} +1 -1
  44. package/web-dist/assets/index-CzN-tCOl.css +1 -0
  45. package/web-dist/assets/index-DqXGLIwz.js +319 -0
  46. package/web-dist/assets/{info-DKCQHKI2-BEaDQqRu.js → info-DKCQHKI2-BhzY4Uni.js} +1 -1
  47. package/web-dist/assets/{infoDiagram-FWYZ7A6U-RJlw-Bzq.js → infoDiagram-FWYZ7A6U-DOAJbOl-.js} +1 -1
  48. package/web-dist/assets/{ishikawaDiagram-FXEZZL3T-xop_QrWs.js → ishikawaDiagram-FXEZZL3T-mBZHeiuC.js} +1 -1
  49. package/web-dist/assets/{journeyDiagram-5HDEW3XC-O6nmpoje.js → journeyDiagram-5HDEW3XC-BIN13T_S.js} +1 -1
  50. package/web-dist/assets/{kanban-definition-HUTT4EX6-IXwPs_L0.js → kanban-definition-HUTT4EX6-BXIeyjSB.js} +1 -1
  51. package/web-dist/assets/{line-BMRguCzE.js → line-CXrqIQ_6.js} +1 -1
  52. package/web-dist/assets/{mermaid-parser.core-BCXFwFN1.js → mermaid-parser.core-B-ewc93S.js} +3 -3
  53. package/web-dist/assets/{mermaid.core-Cqq_9ya3.js → mermaid.core-4Ulv5sd_.js} +3 -3
  54. package/web-dist/assets/{mindmap-definition-LN4V7U3C-DEfWfnOg.js → mindmap-definition-LN4V7U3C-CUlmsgpu.js} +1 -1
  55. package/web-dist/assets/{packet-7NZHBO7P-CQGAnngT.js → packet-7NZHBO7P-DOU_OPIg.js} +1 -1
  56. package/web-dist/assets/{pegDiagram-2B236MQR-tOrSsHlA.js → pegDiagram-2B236MQR-ClVC15zM.js} +1 -1
  57. package/web-dist/assets/{pie-RZYD4A2V-C0KP3XF8.js → pie-RZYD4A2V-9fj9kCZg.js} +1 -1
  58. package/web-dist/assets/{pieDiagram-ENE6RG2P-FzmIWC1K.js → pieDiagram-ENE6RG2P-BQAuzbqi.js} +1 -1
  59. package/web-dist/assets/{quadrantDiagram-ABIIQ3AL-N6zuxKqV.js → quadrantDiagram-ABIIQ3AL-B6v8OzIX.js} +1 -1
  60. package/web-dist/assets/{radar-I7S5WNFK-CB3WQRMp.js → radar-I7S5WNFK-ggR6yaoH.js} +1 -1
  61. package/web-dist/assets/{railroad-3IZDKUUU-Ck8x0o2o.js → railroad-3IZDKUUU-NRPKG8Tz.js} +1 -1
  62. package/web-dist/assets/railroad-abnf-AHOZXSZD-icGR3Mej.js +1 -0
  63. package/web-dist/assets/railroad-ebnf-EBAXGLYW-7uKDbF2e.js +1 -0
  64. package/web-dist/assets/railroad-peg-LSFZ7HO6-CJwY09fs.js +1 -0
  65. package/web-dist/assets/{railroadDiagram-RFXS5EU6-6TyF6de4.js → railroadDiagram-RFXS5EU6-CIJqRzYi.js} +1 -1
  66. package/web-dist/assets/{requirementDiagram-TGXJPOKE-D6n7xpc4.js → requirementDiagram-TGXJPOKE-7Dvam68t.js} +1 -1
  67. package/web-dist/assets/{sankeyDiagram-HTMAVEWB-B7R6Natf.js → sankeyDiagram-HTMAVEWB-BYX8p13G.js} +1 -1
  68. package/web-dist/assets/{sequenceDiagram-DBY2YBRQ-CFbbuWF6.js → sequenceDiagram-DBY2YBRQ-Bmqmc3Hh.js} +1 -1
  69. package/web-dist/assets/{stateDiagram-2N3HPSRC-DwXu0kos.js → stateDiagram-2N3HPSRC-Cv1jr6R9.js} +1 -1
  70. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-CWOLQn_o.js +1 -0
  71. package/web-dist/assets/{swimlanes-5IMT3BWC-Clie2Qk3.js → swimlanes-5IMT3BWC-BHlN3gYO.js} +1 -1
  72. package/web-dist/assets/swimlanesDiagram-G3AALYLV-cd1Z89Mn.js +8 -0
  73. package/web-dist/assets/{timeline-definition-FHXFAJF6-DWDsPzuQ.js → timeline-definition-FHXFAJF6-DO8eivQs.js} +1 -1
  74. package/web-dist/assets/{treeView-QDETBFTQ-Ddd-Wxpb.js → treeView-QDETBFTQ-DOmZ_jrX.js} +1 -1
  75. package/web-dist/assets/{treemap-6X3UGDF4-CuZ3jD4K.js → treemap-6X3UGDF4-BsObxxTm.js} +1 -1
  76. package/web-dist/assets/{vennDiagram-L72KCM5P-BoE4aTMb.js → vennDiagram-L72KCM5P-BRy9Xi2q.js} +1 -1
  77. package/web-dist/assets/{wardley-OPB4EBWU-CfaztQL0.js → wardley-OPB4EBWU-BYP-hY4S.js} +1 -1
  78. package/web-dist/assets/{wardleyDiagram-EHGQE667-pxcV16qp.js → wardleyDiagram-EHGQE667-BQVbSUex.js} +1 -1
  79. package/web-dist/assets/{xychartDiagram-FW5EYKEG--d5N7oDL.js → xychartDiagram-FW5EYKEG-tK-MC6wE.js} +1 -1
  80. package/web-dist/index.html +2 -2
  81. package/web-dist/assets/architecture-TIHT7OUA-ZNmKXuTL.js +0 -1
  82. package/web-dist/assets/channel-CTEEc7oT.js +0 -1
  83. package/web-dist/assets/classDiagram-OUVF2IWQ-D5na7mob.js +0 -1
  84. package/web-dist/assets/classDiagram-v2-EOCWNBFH-D5na7mob.js +0 -1
  85. package/web-dist/assets/eventmodeling-45OFAUF4-B8cYCwrU.js +0 -1
  86. package/web-dist/assets/flowDiagram-23GEKE2U-C-EWCSnf.js +0 -1
  87. package/web-dist/assets/index-CGrszN1x.js +0 -319
  88. package/web-dist/assets/index-d_g5kIKJ.css +0 -1
  89. package/web-dist/assets/railroad-abnf-AHOZXSZD-QzrMOCpt.js +0 -1
  90. package/web-dist/assets/railroad-ebnf-EBAXGLYW-CwEnUu4A.js +0 -1
  91. package/web-dist/assets/railroad-peg-LSFZ7HO6-BoDEbNAA.js +0 -1
  92. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-CQctakxF.js +0 -1
  93. package/web-dist/assets/swimlanesDiagram-G3AALYLV-CGOfs7yz.js +0 -8
package/dist/dev-child.js CHANGED
@@ -12362,7 +12362,7 @@ var init_interactions = __esm({
12362
12362
  InteractionFieldId = external_exports.string().min(1).max(128).regex(/^[A-Za-z_][A-Za-z0-9_.:-]*$/).refine((value) => !["__proto__", "constructor", "prototype"].includes(value), "reserved field id");
12363
12363
  InteractionDecisionId = external_exports.string().min(1).max(80).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/);
12364
12364
  InteractionProvider = external_exports.object({
12365
- kind: external_exports.enum(["claude", "codex", "frizz"]),
12365
+ kind: external_exports.enum(["claude", "codex", "acp", "frizz"]),
12366
12366
  name: InteractionLabel.optional(),
12367
12367
  version: InteractionOpaqueId.optional()
12368
12368
  }).strict();
@@ -13200,6 +13200,20 @@ function attachmentExtension(name) {
13200
13200
  function isAllowedAttachmentName(name) {
13201
13201
  return ATTACHMENT_EXT_SET.has(attachmentExtension(name));
13202
13202
  }
13203
+ function acpSlugParts(model) {
13204
+ if (typeof model !== "string" || !model.startsWith(ACP_MODEL_PREFIX)) return void 0;
13205
+ const rest = model.slice(ACP_MODEL_PREFIX.length);
13206
+ const at = rest.indexOf("@");
13207
+ const agent = at === -1 ? rest : rest.slice(0, at);
13208
+ const id = at === -1 ? "" : rest.slice(at + 1);
13209
+ return agent ? { agent, ...id ? { model: id } : {} } : void 0;
13210
+ }
13211
+ function acpAgentIdFromModel(model) {
13212
+ return acpSlugParts(model)?.agent;
13213
+ }
13214
+ function acpModelIdFromModel(model) {
13215
+ return acpSlugParts(model)?.model;
13216
+ }
13203
13217
  function isDirectSubAgent(agent) {
13204
13218
  return (agent.depth ?? 1) === 1;
13205
13219
  }
@@ -13268,9 +13282,9 @@ function trimAwaitingTitle(raw2) {
13268
13282
  }
13269
13283
  function splitAwaitingFrontmatter(raw2) {
13270
13284
  const lines = raw2.split("\n").map((l) => l.replace(/\r$/, ""));
13271
- const delimiter2 = lines.findIndex((l) => /^\s*---+\s*$/.test(l));
13272
- const frontmatter = delimiter2 === -1 ? lines : lines.slice(0, delimiter2);
13273
- const after = delimiter2 === -1 ? [] : lines.slice(delimiter2 + 1);
13285
+ const delimiter3 = lines.findIndex((l) => /^\s*---+\s*$/.test(l));
13286
+ const frontmatter = delimiter3 === -1 ? lines : lines.slice(0, delimiter3);
13287
+ const after = delimiter3 === -1 ? [] : lines.slice(delimiter3 + 1);
13274
13288
  const rest = [];
13275
13289
  const yamlLines = [];
13276
13290
  const titleLines = [];
@@ -13554,6 +13568,11 @@ function questionsCancelledWakeMessage(count) {
13554
13568
  function queuedThread(t) {
13555
13569
  return t.kind === "session" && t.foreign !== true && t.needsYou === true && t.state !== "archived";
13556
13570
  }
13571
+ function requireEffortOutsideAcp(profile, ctx) {
13572
+ if (profile.backend !== "acp" && profile.effort === void 0) {
13573
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["effort"], message: `effort is required for a ${profile.backend} profile` });
13574
+ }
13575
+ }
13557
13576
  function formatElapsed(ms) {
13558
13577
  if (!Number.isFinite(ms) || ms < 0) return "an unknown time";
13559
13578
  const s = Math.floor(ms / 1e3);
@@ -13704,7 +13723,7 @@ function parseGithubWakeSteer(text2) {
13704
13723
  function frizzRoute(path3) {
13705
13724
  return `${FRIZZ_ROUTE_PREFIX}${path3.startsWith("/") ? path3 : `/${path3}`}`;
13706
13725
  }
13707
- var import_yaml, ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_TEXT_EXTENSIONS, ATTACHMENT_OFFICE_EXTENSIONS, ATTACHMENT_DATA_EXTENSIONS, ATTACHMENT_ARCHIVE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrizzStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, AwaitingHint, RETIRED_AWAITING_KINDS, RETIRED_LINE_RE, RETIRED_AWAITING_REPLACEMENT, AWAITING_KEY_RE, AWAITING_YAML_KEYS, AWAITING_SEQUENCE_KEYS, AWAITING_HINT_MAX, AWAITING_HINT_VALUE_MAX, AWAITING_TITLE_MAX, AWAITING_ITEM_KINDS, AWAITING_DURATION_RE, DURATION_UNIT_MS, AWAITING_FOR_MAX_MS, SnoozeUntil, SNOOZE_PROMPT_MAX, SnoozePrompt, RECURRING_PROMPT_MAX, RECURRING_MIN_INTERVAL_SECONDS, RECURRING_MAX_INTERVAL_SECONDS, RecurringPromptText, RecurringIntervalSeconds, ThreadRecurringPrompt, INTERRUPT_MARKERS, ALLDONE_SENTINEL, OPT_OUT_NOTE, OVER_QUESTION_NOTE, TIMER_PROMPT_MAX, TIMER_MAX_DELAY_SECONDS, TIMER_MAX_ARMED, TimerPromptText, PR_WATCH_SPENT_TRAILER, PR_WATCH_ARMED_TRAILER, SHELL_DONE_TRAILER, ISSUE_WATCH_SPENT_TRAILER, ISSUE_WATCH_ARMED_TRAILER, SIGNOFF_NUDGE_MARKER, SIGNOFF_NUDGE_MESSAGE, PARK_CORRECTION_NAMES_LEAD, PARK_CORRECTION_RETIRED_LEAD, PARK_CORRECTION_QUESTION_LEAD, ThreadWatchKind, GithubChecksState, GithubMergeState, GithubWatchStatus, GithubIssueStatus, ThreadWatchView, PR_WATCH_FOR_MAX_MS, PR_WATCH_DEFAULT_FOR_MS, PR_WATCH_MAX_ARMED, PrWatchView, AddOwnPrWatchInput, AddOwnPrWatchResult, DropOwnPrWatchInput, DropOwnPrWatchResult, OwnWatchKind, AddOwnWatchInput, OwnWatchView, AddOwnWatchResult, DropOwnWatchInput, DropOwnWatchResult, OWN_WATCH_MAX_ARMED, QUESTION_FENCE_RETIRED_AT, AskQuestionKind, ASK_MAX_DEPTH, AskedOptionSchema, AskedQuestionSchema, AskInput, RegisteredQuestionView, AskResult, UnaskInput, UnaskResult, QuestionAnswerSchema, AnswerQuestionsInput, AnswerQuestionsResult, DismissQuestionsInput, DismissQuestionsResult, BURIED_ANSWERS_HEADER, ANSWER_FOLLOW_UP_MARKER, DISMISSED_ANSWER, MarkOwnDoneInput, MarkOwnDoneResult, ListOwnPrWatchesInput, OwnPrWatchesResult, ThreadTimerView, ThreadFence, LimitWindow, LimitPause, ProviderError, ThreadLinkView, UpsertOwnLinkInput, UpsertOwnLinkResult, DropOwnLinkInput, DropOwnLinkResult, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AccountEmails, AuthSnapshot, AccountLogoutInput, AccountLogoutResult, AccountLoginStartInput, AccountLoginStartResult, AccountLoginStatusInput, AccountLoginStatusResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, AdoptSessionInput, FollowUpInput, UnqueueFollowUpInput, UnqueueFollowUpResult, DeliverQueuedNowInput, DeliverQueuedNowResult, SetThreadSnoozeInput, SetThreadPinnedInput, SetThreadRecurringPromptInput, SetOwnThreadRecurringPromptInput, SetOwnThreadRecurringPromptResult, GetOwnThreadRecurringPromptInput, OwnThreadRecurringPromptResult, SetOwnThreadTimerInput, CancelOwnThreadTimerInput, ListOwnThreadTimersInput, OwnThreadTimersResult, ThreadActivityItem, ListOwnThreadActivityInput, OwnThreadActivityResult, SetOwnThreadTimerResult, CancelOwnThreadTimerResult, SetOwnThreadStopHookInput, SetOwnThreadHeartbeatInput, ThreadPluginReloadResult, RenameThreadInput, AiRenameThreadInput, AiRenameThreadResult, SetOwnThreadTitleInput, SetOwnThreadTitleResult, SetThreadPermissionInput, SetThreadPermissionResult, ThreadProfileOptionsInput, ThreadProfileOptionsResult, ThreadSkillSource, ThreadSkill, ThreadSkillsInput, ThreadSkillsResult, SetThreadProfileInput, SetThreadProfileResult, DISPATCH_TASK_BANNER, DISPATCH_TASK_BANNER_MARKER, GITHUB_DISPATCH_UI_BOUNDARY, WAKE_TIME_HEADER_TAIL, WAKE_TRAILERS, HUMAN_GAP_FLOOR_MS, HUMAN_GAP_NOTE_TAIL, WAKE_DELIVERY_TOKEN_TAIL, WAKE_DELIVERY_TOKEN_LINE, NOISE_PREFIXES, AGENT_MESSAGE_WRAPPER, GithubWakeItem, GithubWakeSteer, WAKE_SCOPE, WAKE_REVIEW_LEAD, WAKE_BACKLOG_TAIL, WAKE_REF, WAKE_SINGLE, WAKE_MULTI_HEAD, WAKE_ITEM, WAKE_MORE, PR_WATCH_FINISHED, PR_WATCH_CI_PASSED, PR_WATCH_CI_GATED, PR_WATCH_STATE, PR_WATCH_CI_FAILED, GithubStatus, GithubRefCard, GithubRefPreviewInput, GithubRefPreviewResult, GithubItem, GithubListInput, GithubListResult, GithubBatchInput, GithubBatchResult, BoardMeta, ServerEvent, TranscriptEdit, TranscriptTodo, TranscriptToolCall, TranscriptPart, TranscriptMessage, TranscriptPageCursor, EditedFile, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketFilePath, SocketTranscriptClientMsg, SocketFileClientMsg, SocketClientMsg, ProjectCard, PROJECT_ICON_EXTENSIONS, PROJECT_ICON_MAX_BASE64_CHARS, DirectoryPickResult, ThreadLocation, FRIZZ_ROUTE_PREFIX, DEFAULT_PORT, threadIdentityName;
13726
+ 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, AcpAgent, ACP_MODEL_PREFIX, AcpAgentModel, AcpAgentModels, AcpAgentModelsInput, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, AwaitingHint, RETIRED_AWAITING_KINDS, RETIRED_LINE_RE, RETIRED_AWAITING_REPLACEMENT, AWAITING_KEY_RE, AWAITING_YAML_KEYS, AWAITING_SEQUENCE_KEYS, AWAITING_HINT_MAX, AWAITING_HINT_VALUE_MAX, AWAITING_TITLE_MAX, AWAITING_ITEM_KINDS, AWAITING_DURATION_RE, DURATION_UNIT_MS, AWAITING_FOR_MAX_MS, SnoozeUntil, SNOOZE_PROMPT_MAX, SnoozePrompt, RECURRING_PROMPT_MAX, RECURRING_MIN_INTERVAL_SECONDS, RECURRING_MAX_INTERVAL_SECONDS, RecurringPromptText, RecurringIntervalSeconds, ThreadRecurringPrompt, INTERRUPT_MARKERS, ALLDONE_SENTINEL, OPT_OUT_NOTE, OVER_QUESTION_NOTE, TIMER_PROMPT_MAX, TIMER_MAX_DELAY_SECONDS, TIMER_MAX_ARMED, TimerPromptText, PR_WATCH_SPENT_TRAILER, PR_WATCH_ARMED_TRAILER, SHELL_DONE_TRAILER, ISSUE_WATCH_SPENT_TRAILER, ISSUE_WATCH_ARMED_TRAILER, SIGNOFF_NUDGE_MARKER, SIGNOFF_NUDGE_MESSAGE, PARK_CORRECTION_NAMES_LEAD, PARK_CORRECTION_RETIRED_LEAD, PARK_CORRECTION_QUESTION_LEAD, ThreadWatchKind, GithubChecksState, GithubMergeState, GithubWatchStatus, GithubIssueStatus, ThreadWatchView, PR_WATCH_FOR_MAX_MS, PR_WATCH_DEFAULT_FOR_MS, PR_WATCH_MAX_ARMED, PrWatchView, AddOwnPrWatchInput, AddOwnPrWatchResult, DropOwnPrWatchInput, DropOwnPrWatchResult, OwnWatchKind, AddOwnWatchInput, OwnWatchView, AddOwnWatchResult, DropOwnWatchInput, DropOwnWatchResult, OWN_WATCH_MAX_ARMED, QUESTION_FENCE_RETIRED_AT, AskQuestionKind, ASK_MAX_DEPTH, AskedOptionSchema, AskedQuestionSchema, AskInput, RegisteredQuestionView, AskResult, UnaskInput, UnaskResult, QuestionAnswerSchema, AnswerQuestionsInput, AnswerQuestionsResult, DismissQuestionsInput, DismissQuestionsResult, BURIED_ANSWERS_HEADER, ANSWER_FOLLOW_UP_MARKER, DISMISSED_ANSWER, MarkOwnDoneInput, MarkOwnDoneResult, ListOwnPrWatchesInput, OwnPrWatchesResult, ThreadTimerView, ThreadFence, LimitWindow, LimitPause, ProviderError, ThreadLinkView, UpsertOwnLinkInput, UpsertOwnLinkResult, DropOwnLinkInput, DropOwnLinkResult, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AccountEmails, AuthSnapshot, AccountBackend, AccountLogoutInput, AccountLogoutResult, AccountLoginStartInput, AccountLoginStartResult, AccountLoginStatusInput, AccountLoginStatusResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshotShape, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, AdoptSessionInput, FollowUpInput, UnqueueFollowUpInput, UnqueueFollowUpResult, DeliverQueuedNowInput, DeliverQueuedNowResult, SetThreadSnoozeInput, SetThreadPinnedInput, SetThreadRecurringPromptInput, SetOwnThreadRecurringPromptInput, SetOwnThreadRecurringPromptResult, GetOwnThreadRecurringPromptInput, OwnThreadRecurringPromptResult, SetOwnThreadTimerInput, CancelOwnThreadTimerInput, ListOwnThreadTimersInput, OwnThreadTimersResult, ThreadActivityItem, ListOwnThreadActivityInput, OwnThreadActivityResult, SetOwnThreadTimerResult, CancelOwnThreadTimerResult, SetOwnThreadStopHookInput, SetOwnThreadHeartbeatInput, ThreadPluginReloadResult, RenameThreadInput, AiRenameThreadInput, AiRenameThreadResult, SetOwnThreadTitleInput, SetOwnThreadTitleResult, SetThreadPermissionInput, SetThreadPermissionResult, ThreadProfileOptionsInput, ThreadProfileOptionsResult, ThreadSkillSource, ThreadSkill, ThreadSkillsInput, ThreadSkillsResult, SetThreadProfileInput, SetThreadProfileResult, DISPATCH_TASK_BANNER, DISPATCH_TASK_BANNER_MARKER, GITHUB_DISPATCH_UI_BOUNDARY, WAKE_TIME_HEADER_TAIL, WAKE_TRAILERS, HUMAN_GAP_FLOOR_MS, HUMAN_GAP_NOTE_TAIL, WAKE_DELIVERY_TOKEN_TAIL, WAKE_DELIVERY_TOKEN_LINE, NOISE_PREFIXES, AGENT_MESSAGE_WRAPPER, GithubWakeItem, GithubWakeSteer, WAKE_SCOPE, WAKE_REVIEW_LEAD, WAKE_BACKLOG_TAIL, WAKE_REF, WAKE_SINGLE, WAKE_MULTI_HEAD, WAKE_ITEM, WAKE_MORE, PR_WATCH_FINISHED, PR_WATCH_CI_PASSED, PR_WATCH_CI_GATED, PR_WATCH_STATE, PR_WATCH_CI_FAILED, GithubStatus, GithubRefCard, GithubRefPreviewInput, GithubRefPreviewResult, GithubItem, GithubListInput, GithubListResult, GithubBatchInput, GithubBatchResult, BoardMeta, ServerEvent, TranscriptEdit, TranscriptTodo, TranscriptToolCall, TranscriptPart, TranscriptMessage, TranscriptPageCursor, EditedFile, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketFilePath, SocketTranscriptClientMsg, SocketFileClientMsg, SocketClientMsg, ProjectCard, PROJECT_ICON_EXTENSIONS, PROJECT_ICON_MAX_BASE64_CHARS, DirectoryPickResult, ThreadLocation, FRIZZ_ROUTE_PREFIX, DEFAULT_PORT, threadIdentityName;
13708
13727
  var init_src = __esm({
13709
13728
  "packages/shared/src/index.ts"() {
13710
13729
  "use strict";
@@ -13823,7 +13842,7 @@ var init_src = __esm({
13823
13842
  "exited"
13824
13843
  // the worker process that owned this session is gone, or is no longer driving its turn
13825
13844
  ]);
13826
- Backend = external_exports.enum(["claude", "codex"]);
13845
+ Backend = external_exports.enum(["claude", "codex", "acp"]);
13827
13846
  CodexModel = external_exports.object({
13828
13847
  slug: external_exports.string(),
13829
13848
  displayName: external_exports.string(),
@@ -13837,6 +13856,24 @@ var init_src = __esm({
13837
13856
  contextWindow: external_exports.number().int().positive().optional(),
13838
13857
  maxContextWindow: external_exports.number().int().positive().optional()
13839
13858
  });
13859
+ AcpAgent = external_exports.object({
13860
+ id: external_exports.string(),
13861
+ label: external_exports.string(),
13862
+ command: external_exports.string(),
13863
+ available: external_exports.boolean()
13864
+ });
13865
+ ACP_MODEL_PREFIX = "acp:";
13866
+ AcpAgentModel = external_exports.object({ id: external_exports.string(), name: external_exports.string() });
13867
+ AcpAgentModels = external_exports.object({
13868
+ agentId: external_exports.string(),
13869
+ models: external_exports.array(AcpAgentModel),
13870
+ /** The model the agent opens a session on when Frizz asks for none. */
13871
+ current: external_exports.string().optional(),
13872
+ /** Why the list is empty, when the probe could not open a session (not installed, not logged in…). */
13873
+ error: external_exports.string().optional(),
13874
+ probedAt: external_exports.string()
13875
+ });
13876
+ AcpAgentModelsInput = external_exports.object({ agentId: external_exports.string().min(1).max(100), refresh: external_exports.boolean().optional() }).strict();
13840
13877
  ThreadProfileOption = external_exports.object({
13841
13878
  model: external_exports.string().min(1),
13842
13879
  label: external_exports.string().min(1),
@@ -14848,14 +14885,15 @@ var init_src = __esm({
14848
14885
  codex: ProviderAuth,
14849
14886
  emails: AccountEmails
14850
14887
  });
14851
- AccountLogoutInput = external_exports.object({ backend: external_exports.enum(["claude", "codex"]) }).strict();
14888
+ AccountBackend = external_exports.enum(["claude", "codex"]);
14889
+ AccountLogoutInput = external_exports.object({ backend: AccountBackend }).strict();
14852
14890
  AccountLogoutResult = external_exports.object({
14853
14891
  status: external_exports.enum(["done", "blocked", "failed"]),
14854
14892
  auth: ProviderAuth,
14855
14893
  activeThreads: external_exports.number().int().positive().optional(),
14856
14894
  detail: external_exports.string().max(200).optional()
14857
14895
  });
14858
- AccountLoginStartInput = external_exports.object({ backend: external_exports.enum(["claude", "codex"]) }).strict();
14896
+ AccountLoginStartInput = external_exports.object({ backend: AccountBackend }).strict();
14859
14897
  AccountLoginStartResult = external_exports.object({ attemptId: ThreadSlug });
14860
14898
  AccountLoginStatusInput = external_exports.object({ attemptId: ThreadSlug }).strict();
14861
14899
  AccountLoginStatusResult = external_exports.object({
@@ -14930,6 +14968,15 @@ var init_src = __esm({
14930
14968
  // it (codex compacts at 90% of the resolved window). Unset ⇒ nothing is sent and the model's stock
14931
14969
  // window applies. Optional so an old blob parses; defaultSettings leaves it unset.
14932
14970
  codexContextWindow: external_exports.number().int().positive().optional(),
14971
+ // The operator's OWN Agent Client Protocol agents, merged over the built-in catalogue by id
14972
+ // (server/backend/acp-agents.ts). `command` is an executable name on PATH or an absolute path;
14973
+ // `args` is what puts it into ACP mode (`["acp"]`, `["--acp"]`). Optional so an old blob parses.
14974
+ acpAgents: external_exports.array(external_exports.object({
14975
+ id: external_exports.string().trim().min(1).max(64).regex(/^[a-z0-9][a-z0-9._-]*$/i),
14976
+ label: external_exports.string().trim().min(1).max(80),
14977
+ command: external_exports.string().trim().min(1).max(1024),
14978
+ args: external_exports.array(external_exports.string().max(1024)).max(32).optional()
14979
+ })).max(32).optional(),
14933
14980
  // The GitHub batch-dispatch prompt template (the picker's per-item worker prompt). Optional: when
14934
14981
  // unset OR blank the server falls back to its exported DEFAULT_GITHUB_PROMPT. Substitution tokens
14935
14982
  // the server fills: {repo} {n} {title} {url} {labels} {body}. The leading `THREAD: <slug>` tag is
@@ -14956,28 +15003,38 @@ var init_src = __esm({
14956
15003
  DispatchPreferences = external_exports.object({
14957
15004
  backend: Backend,
14958
15005
  claude: DispatchProviderPreferences,
14959
- codex: DispatchProviderPreferences
15006
+ codex: DispatchProviderPreferences,
15007
+ // Optional because every record written before the ACP backend existed lacks it, and a required
15008
+ // key would fail those records' parse and silently reset the operator's saved profile.
15009
+ acp: DispatchProviderPreferences.optional()
14960
15010
  });
14961
- DispatchProfileSnapshot = external_exports.object({
15011
+ DispatchProfileSnapshotShape = external_exports.object({
14962
15012
  backend: Backend,
14963
15013
  model: external_exports.string().trim().min(1).max(200),
14964
- effort: Settings.shape.effort.unwrap(),
15014
+ // Required for Claude and Codex (the refinement below); absent for an ACP agent, which runs with
15015
+ // whatever model and effort its own CLI is configured for — Frizz has no effort axis to offer there.
15016
+ effort: Settings.shape.effort,
14965
15017
  // IGNORED: dispatch permission is decided server-side (workerDispatchPermission) from the
14966
15018
  // non-interactive floor plus the operator's Settings choice, never per dispatch. Optional so old
14967
15019
  // clients that still send it parse.
14968
15020
  permissionMode: PermissionMode.optional()
14969
15021
  }).strict();
15022
+ DispatchProfileSnapshot = DispatchProfileSnapshotShape.superRefine(requireEffortOutsideAcp);
14970
15023
  SetDispatchPreferenceInput = external_exports.discriminatedUnion("field", [
14971
15024
  external_exports.object({ field: external_exports.literal("backend"), value: Backend }),
14972
15025
  external_exports.object({
14973
15026
  field: external_exports.literal("profile"),
14974
15027
  backend: Backend,
14975
15028
  model: external_exports.string().trim().min(1).max(200),
14976
- effort: Settings.shape.effort.unwrap()
15029
+ // Optional ONLY for an ACP profile (see DispatchProfileSnapshot); a Claude/Codex profile without
15030
+ // an effort is refused below rather than silently stored as "default".
15031
+ effort: Settings.shape.effort
14977
15032
  }),
14978
15033
  external_exports.object({ field: external_exports.literal("model"), backend: Backend, value: external_exports.string().trim().min(1).max(200) }),
14979
15034
  external_exports.object({ field: external_exports.literal("effort"), backend: Backend, value: Settings.shape.effort.unwrap() })
14980
- ]);
15035
+ ]).superRefine((update, ctx) => {
15036
+ if (update.field === "profile") requireEffortOutsideAcp(update, ctx);
15037
+ });
14981
15038
  DispatchInput = external_exports.object({
14982
15039
  // Optional: when omitted, dispatch derives a fallback title from the prompt (Claude later renames
14983
15040
  // the session via ai-title, which the UI prefers for display). The thread FILE always gets a
@@ -15233,7 +15290,9 @@ var init_src = __esm({
15233
15290
  SetThreadProfileInput = external_exports.object({
15234
15291
  slug: ThreadSlug,
15235
15292
  model: external_exports.string().trim().min(1).max(200),
15236
- effort: external_exports.string().trim().min(1).max(100)
15293
+ // Absent ONLY for an ACP thread, whose model slug (`acp:<agent>@<model>`) carries no effort axis; the
15294
+ // router refuses a Claude/Codex profile without one rather than storing "".
15295
+ effort: external_exports.string().trim().min(1).max(100).optional()
15237
15296
  }).strict();
15238
15297
  SetThreadProfileResult = external_exports.object({
15239
15298
  effect: external_exports.enum(["applied", "next-resume"])
@@ -15379,9 +15438,9 @@ ${DISPATCH_TASK_BANNER}
15379
15438
  page: external_exports.number().int().positive(),
15380
15439
  pageCount: external_exports.number().int().positive()
15381
15440
  });
15382
- GithubBatchInput = DispatchProfileSnapshot.extend({
15441
+ GithubBatchInput = DispatchProfileSnapshotShape.extend({
15383
15442
  items: external_exports.array(external_exports.object({ kind: external_exports.enum(["issue", "pr"]), number: external_exports.number().int().positive() })).min(1)
15384
- }).strict();
15443
+ }).strict().superRefine(requireEffortOutsideAcp);
15385
15444
  GithubBatchResult = external_exports.object({
15386
15445
  dispatched: external_exports.array(external_exports.object({ number: external_exports.number(), kind: external_exports.string(), slug: ThreadSlug })),
15387
15446
  failed: external_exports.array(external_exports.object({ number: external_exports.number(), kind: external_exports.string(), error: external_exports.string() }))
@@ -19055,7 +19114,7 @@ var init_project_scope = __esm({
19055
19114
 
19056
19115
  // packages/server/src/storage.ts
19057
19116
  function isHeadlessRow(row) {
19058
- return row.backend === "codex" && row.codex_runtime === "app-server" || row.backend === "claude" && row.claude_runtime === "broker";
19117
+ return row.backend === "codex" && row.codex_runtime === "app-server" || row.backend === "claude" && row.claude_runtime === "broker" || row.backend === "acp";
19059
19118
  }
19060
19119
  function isBrokerClaudeRow(row) {
19061
19120
  return row.backend === "claude" && row.claude_runtime === "broker";
@@ -19078,7 +19137,7 @@ function groupBySlug(rows) {
19078
19137
  }
19079
19138
  function ensureStorageSchema(db) {
19080
19139
  db.exec(STORAGE_SCHEMA);
19081
- for (const column of ["pinned_at TEXT"]) {
19140
+ for (const column of ["pinned_at TEXT", "acp_agent TEXT"]) {
19082
19141
  try {
19083
19142
  db.exec(`ALTER TABLE session ADD COLUMN ${column}`);
19084
19143
  } catch {
@@ -19673,6 +19732,7 @@ function createStorage(source, projectId) {
19673
19732
  const agentSessionStmt = scope.prepare("UPDATE session SET agent_session_id = ? WHERE project_id = @project_id AND slug = ?");
19674
19733
  const codexRuntimeStmt = scope.prepare("UPDATE session SET codex_runtime = ? WHERE project_id = @project_id AND slug = ?");
19675
19734
  const claudeRuntimeStmt = scope.prepare("UPDATE session SET claude_runtime = ? WHERE project_id = @project_id AND slug = ?");
19735
+ const acpAgentStmt = scope.prepare("UPDATE session SET acp_agent = ? WHERE project_id = @project_id AND slug = ?");
19676
19736
  const profileStmt = scope.prepare("UPDATE session SET model = ?, effort = ?, profile_set_at = ? WHERE project_id = @project_id AND slug = ?");
19677
19737
  const permissionModeStmt = scope.prepare("UPDATE session SET permission_mode = ?, permission_set_at = ? WHERE project_id = @project_id AND slug = ?");
19678
19738
  const permissionPendingStmt = scope.prepare("UPDATE session SET permission_pending = ? WHERE project_id = @project_id AND slug = ?");
@@ -19802,7 +19862,8 @@ function createStorage(source, projectId) {
19802
19862
  runtime_control: row.runtime_control ?? null,
19803
19863
  runtime_control_revision: row.runtime_control_revision ?? 0,
19804
19864
  codex_runtime: row.codex_runtime ?? null,
19805
- claude_runtime: row.claude_runtime ?? null
19865
+ claude_runtime: row.claude_runtime ?? null,
19866
+ acp_agent: row.acp_agent ?? null
19806
19867
  });
19807
19868
  const getAdoptionRuntimeSnapshot = db.transaction((slug) => ({
19808
19869
  // Claim first is intentional: a finalized claim disappearing before the current-row validation
@@ -19834,7 +19895,7 @@ function createStorage(source, projectId) {
19834
19895
  const retireAdoptionAttempt = (claim, retiredAtMs = Date.now()) => {
19835
19896
  putRetiredAdoptionAttempt.run(claim.attempt_token, claim.slug, claim.session_id, retiredAtMs);
19836
19897
  };
19837
- const withAdoptionSpawnFence = (slug, attemptToken, leaseExpiresAtMs, spawn7) => {
19898
+ const withAdoptionSpawnFence = (slug, attemptToken, leaseExpiresAtMs, spawn8) => {
19838
19899
  ThreadSlug.parse(slug);
19839
19900
  if (!Number.isSafeInteger(leaseExpiresAtMs)) return { acquired: false };
19840
19901
  db.exec("BEGIN IMMEDIATE");
@@ -19866,7 +19927,7 @@ function createStorage(source, projectId) {
19866
19927
  bound = true;
19867
19928
  return true;
19868
19929
  };
19869
- const value = spawn7(bindPane);
19930
+ const value = spawn8(bindPane);
19870
19931
  if (!bound) {
19871
19932
  if (db.inTransaction) db.exec("ROLLBACK");
19872
19933
  throw new Error("adoption spawn returned without binding its exact pane");
@@ -20185,6 +20246,7 @@ function createStorage(source, projectId) {
20185
20246
  setAgentSession: (slug, agentSessionId) => void agentSessionStmt.run(agentSessionId, slug),
20186
20247
  setCodexRuntime: (slug, runtime) => void codexRuntimeStmt.run(runtime, slug),
20187
20248
  setClaudeRuntime: (slug, runtime) => void claudeRuntimeStmt.run(runtime, slug),
20249
+ setAcpAgent: (slug, agentId) => void acpAgentStmt.run(agentId, slug),
20188
20250
  setProfile: (slug, model, effort) => void profileStmt.run(model, effort, (/* @__PURE__ */ new Date()).toISOString(), slug),
20189
20251
  setPermissionMode: (slug, permissionMode) => void permissionModeStmt.run(permissionMode, (/* @__PURE__ */ new Date()).toISOString(), slug),
20190
20252
  setPermissionPending: (slug, permissionMode) => void permissionPendingStmt.run(permissionMode, slug),
@@ -20413,6 +20475,8 @@ var init_storage = __esm({
20413
20475
  -- Claude transport discriminator: 'broker' = a session-broker-owned Agent SDK session; NULL/'tmux'
20414
20476
  -- is the pre-broker legacy value, same story.
20415
20477
  claude_runtime TEXT,
20478
+ -- ACP agent id (backend/acp-agents.ts) for backend='acp' rows; also in the ALTER list below.
20479
+ acp_agent TEXT,
20416
20480
  -- THE RECURRING PROMPT (scheduler.ts SOURCES 4, 5 and 7): one text, three independent triggers \u2014
20417
20481
  -- every time the thread rests, every N ms on a clock, and/or every time its context is compacted.
20418
20482
  -- All flags 0 = off; there is no separate enable column, because another flag could only ever
@@ -20865,9 +20929,16 @@ function defaultDispatchPreferences(settings, codexModels = []) {
20865
20929
  return {
20866
20930
  backend,
20867
20931
  claude: backend === "claude" ? selected : { permissionMode: "auto" },
20868
- codex: backend === "codex" ? selected : { permissionMode: "default" }
20932
+ codex: backend === "codex" ? selected : { permissionMode: "default" },
20933
+ // The ACP slot exists only once the operator has chosen an agent: it has no permission axis (the
20934
+ // agent's own CLI decides), so there is no default worth writing, and every record written before
20935
+ // the backend existed lacks the key.
20936
+ ...backend === "acp" ? { acp: selected } : {}
20869
20937
  };
20870
20938
  }
20939
+ function profileOf(prefs, backend) {
20940
+ return prefs[backend] ?? {};
20941
+ }
20871
20942
  function getDispatchPreferences(storage, settings, home, codexModels = []) {
20872
20943
  const machine = readMachineDispatchPreferences(home);
20873
20944
  if (machine) return machine;
@@ -20884,13 +20955,13 @@ function setDispatchPreference(storage, settings, home, update, codexModels = []
20884
20955
  ...current,
20885
20956
  backend: update.backend,
20886
20957
  [update.backend]: {
20887
- ...current[update.backend],
20958
+ ...profileOf(current, update.backend),
20888
20959
  model: update.model,
20889
20960
  effort: update.effort
20890
20961
  }
20891
20962
  };
20892
20963
  } else {
20893
- const profile = current[update.backend];
20964
+ const profile = profileOf(current, update.backend);
20894
20965
  next = {
20895
20966
  ...current,
20896
20967
  ...update.field === "model" ? { backend: update.backend } : {},
@@ -20928,8 +20999,8 @@ function userInfoParts(raw2) {
20928
20999
  const encoded = encodedMatch?.index ?? -1;
20929
21000
  const index = literal === -1 ? encoded : encoded === -1 ? literal : Math.min(literal, encoded);
20930
21001
  if (index <= 0) return null;
20931
- const delimiter2 = value.slice(index, index + (index === encoded ? 3 : 1));
20932
- return { username: value.slice(0, index), delimiter: delimiter2, password: value.slice(index + delimiter2.length) };
21002
+ const delimiter3 = value.slice(index, index + (index === encoded ? 3 : 1));
21003
+ return { username: value.slice(0, index), delimiter: delimiter3, password: value.slice(index + delimiter3.length) };
20933
21004
  }
20934
21005
  function redactUserInfoToken(raw2, options) {
20935
21006
  const parts = userInfoParts(raw2);
@@ -20952,11 +21023,11 @@ function redactCredentialSyntax(raw2, options = {}) {
20952
21023
  let value = raw2;
20953
21024
  value = value.replace(
20954
21025
  URL_USERINFO_RE,
20955
- (_whole, scheme, username, delimiter2, password) => `${scheme}${username}${delimiter2}${replacement(options, password)}@`
21026
+ (_whole, scheme, username, delimiter3, password) => `${scheme}${username}${delimiter3}${replacement(options, password)}@`
20956
21027
  );
20957
21028
  value = value.replace(
20958
21029
  URL_ENCODED_USERINFO_RE,
20959
- (_whole, scheme, username, delimiter2, password) => `${scheme}${username}${delimiter2}${replacement(options, password)}@`
21030
+ (_whole, scheme, username, delimiter3, password) => `${scheme}${username}${delimiter3}${replacement(options, password)}@`
20960
21031
  );
20961
21032
  value = value.replace(
20962
21033
  USER_FLAG_RE,
@@ -21605,6 +21676,7 @@ function threadProfileOptions(backend) {
21605
21676
  }))
21606
21677
  };
21607
21678
  }
21679
+ if (backend === "acp") return { backend, options: [] };
21608
21680
  throw new Error("This thread has an unknown backend; its runtime profile cannot be changed");
21609
21681
  }
21610
21682
  function validateThreadProfile(backend, model, effort) {
@@ -24897,7 +24969,7 @@ ${ask}`;
24897
24969
  void nowMs;
24898
24970
  void backend;
24899
24971
  if (state.foreign) return { permPrompt: false };
24900
- if (turn === "in-flight" && row.backend !== "codex" && permMarkerBlocks(state, row)) {
24972
+ if (turn === "in-flight" && row.backend !== "codex" && row.backend !== "acp" && permMarkerBlocks(state, row)) {
24901
24973
  return { permPrompt: true };
24902
24974
  }
24903
24975
  return { permPrompt: false };
@@ -25221,7 +25293,7 @@ ${ask}`;
25221
25293
  if (state.stallLogged) return;
25222
25294
  if (row.exited && rowIsArchived(row)) return;
25223
25295
  state.stallLogged = true;
25224
- const detail2 = isBrokerClaudeRow(row) && deps.project.stateDir ? `no worker terminal to capture (broker runtime). Daemon diagnostics: ${claudeBrokerDiagnosticLogPath(deps.project.stateDir, row.session_id)}` : isHeadlessRow(row) ? `no worker terminal to capture (headless ${row.backend === "codex" ? "codex app-server" : "claude broker"} runtime)` : "no worker output captured (this build captures no terminal)";
25296
+ const detail2 = isBrokerClaudeRow(row) && deps.project.stateDir ? `no worker terminal to capture (broker runtime). Daemon diagnostics: ${claudeBrokerDiagnosticLogPath(deps.project.stateDir, row.session_id)}` : isHeadlessRow(row) ? `no worker terminal to capture (headless ${row.backend === "codex" ? "codex app-server" : row.backend === "acp" ? "ACP agent" : "claude broker"} runtime)` : "no worker output captured (this build captures no terminal)";
25225
25297
  log.error(
25226
25298
  "tailer",
25227
25299
  `thread ${row.slug} (session ${row.session_id}): no transcript ${DISCOVERY_GRACE_MS / 1e3}s after dispatch \u2014 likely a boot failure. ${detail2.slice(0, 4e3)}`
@@ -25367,7 +25439,7 @@ ${detail2}
25367
25439
  if (primeProgress && primed % PRIME_PROGRESS_EVERY === 0) primeProgress(primed, rows.length);
25368
25440
  primed++;
25369
25441
  const backend = resolveBackend(row.backend);
25370
- const nativeId = row.agent_session_id ?? row.transcript_id ?? row.session_id;
25442
+ const nativeId = row.backend === "acp" ? row.session_id : row.agent_session_id ?? row.transcript_id ?? row.session_id;
25371
25443
  const known = states.get(row.slug);
25372
25444
  const runtimeGeneration = row.runtime_generation ?? 0;
25373
25445
  const keepable = known !== void 0 && known.sessionId === row.session_id && known.nativeSessionId === nativeId && known.runtimeGeneration === runtimeGeneration;
@@ -25388,7 +25460,7 @@ ${detail2}
25388
25460
  states.set(row.slug, state);
25389
25461
  }
25390
25462
  const prevNoTranscript = state.noTranscript;
25391
- if (row.backend !== "codex" && !resolveTranscript(state, row, nowMs)) continue;
25463
+ if (row.backend !== "codex" && row.backend !== "acp" && !resolveTranscript(state, row, nowMs)) continue;
25392
25464
  if (!state.primed) {
25393
25465
  if (primeDeferred(row)) {
25394
25466
  primeIncomplete = true;
@@ -26546,19 +26618,19 @@ function searchPath2(env) {
26546
26618
  function vendoredNativeBeside(launcherJs, arch, access) {
26547
26619
  const triple = WINDOWS_TRIPLE[arch];
26548
26620
  if (!triple) return void 0;
26549
- const { dirname: dirname25, join: join47 } = win322;
26550
- const pkgRoot = dirname25(dirname25(launcherJs));
26621
+ const { dirname: dirname27, join: join49 } = win322;
26622
+ const pkgRoot = dirname27(dirname27(launcherJs));
26551
26623
  const platformPkg = `codex-win32-${arch}`;
26552
26624
  const vendorRoots = [
26553
- join47(pkgRoot, "node_modules", "@openai", platformPkg, "vendor"),
26625
+ join49(pkgRoot, "node_modules", "@openai", platformPkg, "vendor"),
26554
26626
  // nested under the package
26555
- join47(dirname25(pkgRoot), platformPkg, "vendor"),
26627
+ join49(dirname27(pkgRoot), platformPkg, "vendor"),
26556
26628
  // hoisted beside it: <prefix>\node_modules\@openai\<pkg>
26557
- join47(pkgRoot, "vendor")
26629
+ join49(pkgRoot, "vendor")
26558
26630
  // the pre-platform-package layout, binaries inside @openai/codex itself
26559
26631
  ];
26560
26632
  for (const root of vendorRoots) {
26561
- const exe = join47(root, triple, "bin", "codex.exe");
26633
+ const exe = join49(root, triple, "bin", "codex.exe");
26562
26634
  if (access(exe, "exists")) return exe;
26563
26635
  }
26564
26636
  return void 0;
@@ -31659,6 +31731,62 @@ var init_auth_status = __esm({
31659
31731
  }
31660
31732
  });
31661
31733
 
31734
+ // packages/server/src/backend/codex-execution-health.ts
31735
+ import { execFile as execFile5 } from "node:child_process";
31736
+ function probeHostMacOSServices() {
31737
+ return new Promise((resolve12) => {
31738
+ execFile5(MACOS_SERVICE_PROBE[0], MACOS_SERVICE_PROBE.slice(1), {
31739
+ timeout: MACOS_SERVICE_PROBE_TIMEOUT_MS,
31740
+ killSignal: "SIGKILL",
31741
+ maxBuffer: 64 * 1024
31742
+ }, (error) => resolve12(!error));
31743
+ });
31744
+ }
31745
+ var MACOS_SERVICE_PROBE, MACOS_SERVICE_PROBE_TIMEOUT_MS, CodexExecutionHealth;
31746
+ var init_codex_execution_health = __esm({
31747
+ "packages/server/src/backend/codex-execution-health.ts"() {
31748
+ "use strict";
31749
+ MACOS_SERVICE_PROBE = ["/bin/launchctl", "print", "system/com.apple.configd"];
31750
+ MACOS_SERVICE_PROBE_TIMEOUT_MS = 3e3;
31751
+ CodexExecutionHealth = class {
31752
+ constructor(platform3 = process.platform, hostProbe = probeHostMacOSServices) {
31753
+ this.platform = platform3;
31754
+ this.hostProbe = hostProbe;
31755
+ }
31756
+ checking = /* @__PURE__ */ new WeakMap();
31757
+ check(connection, scope) {
31758
+ if (this.platform !== "darwin") return Promise.resolve();
31759
+ const pending = this.checking.get(connection);
31760
+ if (pending) return pending;
31761
+ const check = this.probe(connection, scope);
31762
+ this.checking.set(connection, check);
31763
+ void check.finally(() => this.checking.delete(connection)).catch(() => void 0);
31764
+ return check;
31765
+ }
31766
+ async probe(connection, scope) {
31767
+ let detail2;
31768
+ try {
31769
+ const raw2 = await connection.request("command/exec", {
31770
+ command: MACOS_SERVICE_PROBE,
31771
+ cwd: "/",
31772
+ timeoutMs: MACOS_SERVICE_PROBE_TIMEOUT_MS,
31773
+ sandboxPolicy: { type: "dangerFullAccess" }
31774
+ }, MACOS_SERVICE_PROBE_TIMEOUT_MS + 1e3);
31775
+ const result = raw2;
31776
+ if (result?.exitCode === 0 && typeof result.stdout === "string" && result.stdout.includes("system/com.apple.configd = {")) return;
31777
+ detail2 = typeof result?.exitCode === "number" ? `launchctl exited ${result.exitCode}` : "invalid probe response";
31778
+ } catch {
31779
+ detail2 = "the local service probe failed or timed out";
31780
+ }
31781
+ const hostHealthy = await this.hostProbe().catch(() => false);
31782
+ throw new Error(
31783
+ `Codex cannot access macOS system services (${detail2}; ${scope}). New conversations and turns are blocked; existing work and conversation history were not changed. ` + (hostHealthy ? "Frizz's host context is healthy, but this backend is not. Stop this project's Codex turns before replacing its shared backend; restarting Frizz alone can reuse it." : "Frizz's host context also failed the check. Relaunch Frizz from a fresh terminal after stopping this project's Codex turns; its shared backend must also be replaced.")
31784
+ );
31785
+ }
31786
+ };
31787
+ }
31788
+ });
31789
+
31662
31790
  // packages/server/src/backend/codex-app-server.ts
31663
31791
  import { createHash as createHash10, randomUUID as randomUUID11 } from "node:crypto";
31664
31792
  import { StringDecoder as StringDecoder3 } from "node:string_decoder";
@@ -32320,15 +32448,16 @@ var init_codex_app_server = __esm({
32320
32448
  init_auth_status();
32321
32449
  init_codex_mcp();
32322
32450
  init_codex_error();
32451
+ init_codex_execution_health();
32323
32452
  init_types2();
32324
32453
  init_logging();
32325
32454
  CODEX_APP_SERVER_PROVIDER = "codex-app-server";
32326
32455
  CODEX_NATIVE_LISTEN_FLAG = "FRIZZ_CODEX_NATIVE_LISTEN";
32327
- CODEX_APP_SERVER_SUPPORTED_VERSION = "0.154.0";
32456
+ CODEX_APP_SERVER_SUPPORTED_VERSION = "0.155.0";
32328
32457
  CODEX_APP_SERVER_PROTOCOL_REVISION = Object.freeze({
32329
32458
  packageVersion: CODEX_APP_SERVER_SUPPORTED_VERSION,
32330
- sourceTag: "rust-v0.154.0",
32331
- sourceCommit: "6b9826e3aa83b1a5947db50f4332cb9c65f1b340"
32459
+ sourceTag: "rust-v0.155.0",
32460
+ sourceCommit: "f0a1b8f0849d90960bc406b848f32e5a129b0457"
32332
32461
  });
32333
32462
  PROTOCOL_FINGERPRINT = [
32334
32463
  CODEX_APP_SERVER_PROTOCOL_REVISION.sourceTag,
@@ -32603,7 +32732,7 @@ var init_codex_app_server = __esm({
32603
32732
  child.on("exit", () => this.fail("exit", new Error("Codex app-server exited")));
32604
32733
  child.on("error", () => this.fail("error", new Error("Codex app-server process failed")));
32605
32734
  }
32606
- async request(method, params) {
32735
+ async request(method, params, timeoutMs = this.timeoutMs) {
32607
32736
  if (this.closed) throw new Error("Codex app-server connection is closed");
32608
32737
  if (this.pending.size >= MAX_OUTBOUND_REQUESTS) throw new Error("Codex app-server outbound request queue is full");
32609
32738
  if (!Number.isSafeInteger(this.nextId)) throw new Error("Codex app-server request id space is exhausted");
@@ -32612,7 +32741,7 @@ var init_codex_app_server = __esm({
32612
32741
  const timer = setTimeout(() => {
32613
32742
  this.pending.delete(id);
32614
32743
  reject(new Error(`Codex app-server request timed out: ${method}`));
32615
- }, this.timeoutMs);
32744
+ }, timeoutMs);
32616
32745
  this.pending.set(id, { resolve: resolve12, reject, timer });
32617
32746
  });
32618
32747
  try {
@@ -32887,6 +33016,8 @@ var init_codex_app_server = __esm({
32887
33016
  host;
32888
33017
  codexBin;
32889
33018
  timeoutMs;
33019
+ executionHealth;
33020
+ executionScope = "";
32890
33021
  /** Identity of the app-server PROCESS behind the current connection (see CodexAppServerAttachment). */
32891
33022
  daemonGeneration = "";
32892
33023
  /** ChatGPT account the current app-server cached when its process started. */
@@ -32936,6 +33067,7 @@ var init_codex_app_server = __esm({
32936
33067
  operationWaiters = /* @__PURE__ */ new Set();
32937
33068
  shutdownPromise = null;
32938
33069
  constructor(options) {
33070
+ this.executionHealth = options.executionHealth ?? new CodexExecutionHealth();
32939
33071
  this.options = options;
32940
33072
  this.now = options.now ?? (() => /* @__PURE__ */ new Date());
32941
33073
  this.makeId = options.id ?? randomUUID11;
@@ -33172,6 +33304,7 @@ var init_codex_app_server = __esm({
33172
33304
  const releaseOperation = this.beginOperation();
33173
33305
  try {
33174
33306
  const connection = await this.ensureConnected();
33307
+ await this.checkExecutionHealth(connection);
33175
33308
  const binding = this.bindingForScope(input.threadSlug, input.sessionId);
33176
33309
  if (!binding) throw new Error("Codex app-server steer requires a bridge-owned session");
33177
33310
  if (binding.connection_epoch !== this.connectionEpoch || binding.state !== "active") {
@@ -33365,7 +33498,8 @@ var init_codex_app_server = __esm({
33365
33498
  if (bound === 0) return;
33366
33499
  const releaseOperation = this.beginOperation();
33367
33500
  try {
33368
- await this.ensureConnected();
33501
+ const connection = await this.ensureConnected();
33502
+ await this.checkExecutionHealth(connection);
33369
33503
  await this.autoResumeInterruptedTurns();
33370
33504
  } catch {
33371
33505
  } finally {
@@ -33609,6 +33743,7 @@ var init_codex_app_server = __esm({
33609
33743
  for (let attempt = 0; ; attempt++) {
33610
33744
  const desiredAccountId = (this.options.codexAuthAccountId ?? readCodexAccountId)();
33611
33745
  const connection = await this.ensureConnected();
33746
+ await this.checkExecutionHealth(connection);
33612
33747
  if (!desiredAccountId || this.processAuthAccountId === desiredAccountId) return connection;
33613
33748
  if (attempt >= MAX_AUTH_REPLACEMENT_ATTEMPTS) return connection;
33614
33749
  if (!this.authReplacement) {
@@ -33621,6 +33756,17 @@ var init_codex_app_server = __esm({
33621
33756
  if (!await this.awaitAuthReplacement(this.authReplacement)) return connection;
33622
33757
  }
33623
33758
  }
33759
+ async checkExecutionHealth(connection) {
33760
+ try {
33761
+ await this.executionHealth.check(connection, this.executionScope);
33762
+ } catch (error) {
33763
+ const detail2 = error.message;
33764
+ this.options.diagnostic?.({ event: "execution-health-failed", detail: detail2 });
33765
+ log.error("codex", detail2);
33766
+ throw error;
33767
+ }
33768
+ if (this.connection !== connection) throw new Error("Codex app-server changed during its execution health check");
33769
+ }
33624
33770
  /** Await an armed replacement for at most AUTH_REPLACEMENT_WAIT_MS. `false` = it is still waiting for
33625
33771
  * the project to go idle, so the caller should proceed rather than hang; a rejection propagates. */
33626
33772
  async awaitAuthReplacement(replacement2) {
@@ -33695,6 +33841,7 @@ var init_codex_app_server = __esm({
33695
33841
  authAccountId
33696
33842
  });
33697
33843
  const child = attachment.process;
33844
+ this.executionScope = `project ${this.options.projectDir}, backend PID ${attachment.daemonPid}, generation ${attachment.generation}`;
33698
33845
  let connection;
33699
33846
  connection = new JsonlRpcConnection(
33700
33847
  child,
@@ -35059,11 +35206,21 @@ var init_quota = __esm({
35059
35206
  }
35060
35207
  });
35061
35208
 
35209
+ // packages/server/src/local-path.ts
35210
+ function normalizeLocalPath(path3, platform3 = process.platform) {
35211
+ return platform3 === "win32" ? path3.replace(/^\/([a-zA-Z]:[\\/])/, "$1") : path3;
35212
+ }
35213
+ var init_local_path = __esm({
35214
+ "packages/server/src/local-path.ts"() {
35215
+ "use strict";
35216
+ }
35217
+ });
35218
+
35062
35219
  // packages/server/src/local-file.ts
35063
35220
  import { spawn as spawn5 } from "node:child_process";
35064
35221
  import { existsSync as existsSync8, readFileSync as readFileSync18, realpathSync as realpathSync6, statSync as statSync13 } from "node:fs";
35065
35222
  import { homedir as homedir11 } from "node:os";
35066
- import { isAbsolute as isAbsolute4, join as join26, resolve as resolve5, sep as sep2 } from "node:path";
35223
+ import { dirname as dirname10, isAbsolute as isAbsolute4, join as join26, resolve as resolve5, sep as sep2 } from "node:path";
35067
35224
  function isUnder(real, root) {
35068
35225
  let rootReal;
35069
35226
  try {
@@ -35071,9 +35228,25 @@ function isUnder(real, root) {
35071
35228
  } catch {
35072
35229
  return false;
35073
35230
  }
35074
- return real === rootReal || real.startsWith(rootReal.endsWith(sep2) ? rootReal : rootReal + sep2);
35231
+ if (process.platform !== "win32") {
35232
+ return real === rootReal || real.startsWith(rootReal.endsWith(sep2) ? rootReal : rootReal + sep2);
35233
+ }
35234
+ try {
35235
+ const trusted = statSync13(rootReal, { bigint: true });
35236
+ if (!trusted.isDirectory() || trusted.ino === 0n) return false;
35237
+ for (let ancestor = real; ; ) {
35238
+ const actual = statSync13(ancestor, { bigint: true });
35239
+ if (actual.dev === trusted.dev && actual.ino === trusted.ino) return true;
35240
+ const parent = dirname10(ancestor);
35241
+ if (parent === ancestor) return false;
35242
+ ancestor = parent;
35243
+ }
35244
+ } catch {
35245
+ return false;
35246
+ }
35075
35247
  }
35076
35248
  function resolveLocalFile(rawPath, roots) {
35249
+ rawPath = normalizeLocalPath(rawPath);
35077
35250
  if (!isAbsolute4(rawPath)) throw new Error("Local path must be absolute");
35078
35251
  let real;
35079
35252
  try {
@@ -35178,6 +35351,7 @@ var MARKDOWN_FILE_EXT, MARKDOWN_READ_LIMIT, WINDOWS_EDITOR_EXE;
35178
35351
  var init_local_file = __esm({
35179
35352
  "packages/server/src/local-file.ts"() {
35180
35353
  "use strict";
35354
+ init_local_path();
35181
35355
  MARKDOWN_FILE_EXT = /\.(?:md|mdx|markdown)$/i;
35182
35356
  MARKDOWN_READ_LIMIT = 1024 * 1024;
35183
35357
  WINDOWS_EDITOR_EXE = {
@@ -35219,9 +35393,9 @@ var init_thread_links = __esm({
35219
35393
  });
35220
35394
 
35221
35395
  // packages/server/src/frizz.ts
35222
- import { execFile as execFile5 } from "node:child_process";
35396
+ import { execFile as execFile6 } from "node:child_process";
35223
35397
  import { lstatSync as lstatSync2, realpathSync as realpathSync7 } from "node:fs";
35224
- import { basename as basename5, dirname as dirname10, join as join27, resolve as resolve6 } from "node:path";
35398
+ import { basename as basename5, dirname as dirname11, join as join27, resolve as resolve6 } from "node:path";
35225
35399
  import { promisify as promisify5 } from "node:util";
35226
35400
  function frizzScriptsDir() {
35227
35401
  if (process.env.FRIZZ_SCRIPTS_DIR) return process.env.FRIZZ_SCRIPTS_DIR;
@@ -35234,7 +35408,7 @@ function directFrizzRoot(projectDir) {
35234
35408
  const stat2 = lstatSync2(path3);
35235
35409
  if (!stat2.isDirectory() || stat2.isSymbolicLink()) return null;
35236
35410
  const real = realpathSync7(path3);
35237
- return dirname10(real) === projectRoot && basename5(real) === ".frizz" ? real : null;
35411
+ return dirname11(real) === projectRoot && basename5(real) === ".frizz" ? real : null;
35238
35412
  } catch {
35239
35413
  return null;
35240
35414
  }
@@ -35279,7 +35453,7 @@ var init_frizz = __esm({
35279
35453
  "packages/server/src/frizz.ts"() {
35280
35454
  "use strict";
35281
35455
  init_src();
35282
- execFileP = promisify5(execFile5);
35456
+ execFileP = promisify5(execFile6);
35283
35457
  }
35284
35458
  });
35285
35459
 
@@ -35510,17 +35684,17 @@ var init_project_mcp_servers = __esm({
35510
35684
 
35511
35685
  // packages/server/src/worker-plugin-dir.ts
35512
35686
  import { existsSync as existsSync9 } from "node:fs";
35513
- import { dirname as dirname11, join as join29 } from "node:path";
35687
+ import { dirname as dirname12, join as join29 } from "node:path";
35514
35688
  import { fileURLToPath as fileURLToPath3 } from "node:url";
35515
35689
  function resolveWorkerPluginDir(moduleUrl = import.meta.url, env = process.env) {
35516
35690
  const override = env.FRIZZ_WORKER_PLUGIN_DIR;
35517
35691
  if (override && existsSync9(join29(override, ".claude-plugin", "plugin.json")))
35518
35692
  return override;
35519
- let current = dirname11(fileURLToPath3(moduleUrl));
35693
+ let current = dirname12(fileURLToPath3(moduleUrl));
35520
35694
  for (; ; ) {
35521
35695
  const candidate = join29(current, "cc-worker");
35522
35696
  if (existsSync9(join29(candidate, ".claude-plugin", "plugin.json"))) return candidate;
35523
- const parent = dirname11(current);
35697
+ const parent = dirname12(current);
35524
35698
  if (parent === current) return void 0;
35525
35699
  current = parent;
35526
35700
  }
@@ -35571,6 +35745,12 @@ var init_workerPrompt = __esm({
35571
35745
  "codex": {
35572
35746
  "SESSION_KIND": "codex",
35573
35747
  "RESUME_CMD": "codex resume"
35748
+ },
35749
+ // An ACP agent has no resume command Frizz can name: the session lives inside the agent and Frizz
35750
+ // re-opens it over the protocol. The token still has to resolve, so it names the mechanism.
35751
+ "acp": {
35752
+ "SESSION_KIND": "coding-agent",
35753
+ "RESUME_CMD": "(Frizz re-opens the session over ACP)"
35574
35754
  }
35575
35755
  };
35576
35756
  INTRO = `You are a dispatched worker agent \u2014 a top-level \`{{FRIZZ_SESSION_KIND}}\` session frizz spawned to drive ONE
@@ -36102,7 +36282,23 @@ emptied window. Whether to keep notes, and whether to arm anything, is yours to
36102
36282
  even with \`fork_turns: "none"\`, and one undifferentiated "keep the doc current" mandate is what once
36103
36283
  made a child replace a whole shared document with its task notes and then delete the replacement as a
36104
36284
  misguided rollback. One file per writer \u2014 \`<agent>-<topic>.md\` \u2014 removes that failure entirely: there
36105
- is nothing to merge, so there is nothing to clobber. Never edit or delete a file another agent wrote.`
36285
+ is nothing to merge, so there is nothing to clobber. Never edit or delete a file another agent wrote.`,
36286
+ acp: `## Your scratch directory
36287
+
36288
+ \`.frizz/threads/<session-id>/\` (exact path in your session-start context) \u2014 a folder that is YOURS, for
36289
+ as many files as you like, in whatever format you like. It starts EMPTY and nothing is expected in it.
36290
+ Frizz reads nothing here automatically.
36291
+
36292
+ **IT IS OPTIONAL, IT IS NOT A DELIVERABLE, AND WRITING NOTES IS NOT DOING THE WORK.** It exists in case
36293
+ you want it. A single direct task usually needs nothing here: just do the task. Never let a note stand
36294
+ in for an action \u2014 recording "next: X" when the human asked for X is not progress on X, and a turn that
36295
+ ends right after a scratch write is nearly always a turn that stopped for no reason. When something is
36296
+ worth writing, write it AS YOU GO, mid-work, and then keep working.
36297
+
36298
+ **Notes here can come back after a compaction, if you want that.** Nothing points you at this directory
36299
+ once your context is summarized \u2014 but the frizz \`goal\` tool can arm a goal, and one armed with
36300
+ \`post_compaction: true\` re-sends a prompt of your choosing (which can link a file here) into the
36301
+ emptied window. Whether to keep notes, and whether to arm anything, is yours to decide per effort.`
36106
36302
  };
36107
36303
  BACKEND = {
36108
36304
  claude: `## Sub-agents
@@ -36361,7 +36557,31 @@ The sandbox governs what you may touch, and a denial is the
36361
36557
  sandbox \u2014 not a bug: \`read-only\` (inspect, never write), \`workspace-write\` (edit inside the repo,
36362
36558
  denied outside), or \`danger-full-access\` (unrestricted). Approvals are off (\`approvalPolicy: never\`), so a
36363
36559
  sandbox-denied action fails straight back to you rather than prompting a human \u2014 adapt, or surface
36364
- the blocker in your final message.`
36560
+ the blocker in your final message.`,
36561
+ acp: `## Your harness, and the frizz tools inside it
36562
+
36563
+ You are running as a coding agent that Frizz drives over the Agent Client Protocol (ACP). Frizz did
36564
+ not build your harness: your file, shell, search and sub-agent tools are your own, your permission
36565
+ prompts are your harness's (Frizz shows each one to the human as a card and relays the answer), and
36566
+ Frizz's own tools reach you as an MCP server named \`frizz\`. Your harness spells its tools with its own
36567
+ prefix \u2014 \`frizz_ask\`, \`frizz_done\`, \`frizz_watch\` \u2026 on OpenCode; \`mcp__frizz__ask\` on others \u2014 so
36568
+ wherever this contract writes \`mcp__frizz__<verb>\`, call the \`<verb>\` tool of the \`frizz\` server the
36569
+ way your harness lists it. Every verb below exists there: \`ask\`, \`unask\`, \`done\`, \`watch\`,
36570
+ \`unwatch\`, \`watch_pr\`, \`timer\`, \`goal\`, \`title\`, \`link\`, \`unlink\`, \`activity\`, \`spawn_thread\`.
36571
+
36572
+ ## Sub-agents
36573
+
36574
+ Frizz does not dispatch sub-agents for you and cannot see the ones your harness runs, so a helper you
36575
+ spawn through your own tools is invisible on the dashboard until you report what it found. Prefer to
36576
+ do the work inline; when you do fan out, collect every child's result before you rest and fold it into
36577
+ your own write-up. \`SendMessage\` and the \`Agent\` tool named elsewhere in this contract are Claude
36578
+ Code's \u2014 use your harness's equivalent, or none.
36579
+
36580
+ ## Follow-ups arrive between turns
36581
+
36582
+ A message the human sends while you are working is QUEUED and delivered as your next turn the moment
36583
+ this one ends \u2014 nothing can interrupt you mid-turn except a stop. So rest promptly when a turn's work is
36584
+ done rather than idling inside it, and read the newest human message first when a turn opens.`
36365
36585
  };
36366
36586
  SPAWN_THREAD = `## Spawning a separate frizz thread
36367
36587
 
@@ -36452,12 +36672,98 @@ Dispatches share a vocabulary for the deliverable and quality bar, not for fleet
36452
36672
  For a non-trivial change: plan, implement, run the repo's build/lint/test gates, inspect every changed
36453
36673
  call site and downstream effect, self-review the diff, fix confirmed findings, and rerun affected
36454
36674
  checks. Add fresh-context reviewer agents only under the explicit delegation policy above. Review
36455
- advice is evidence to judge, not a verdict to copy. Depth scales with blast radius.`
36675
+ advice is evidence to judge, not a verdict to copy. Depth scales with blast radius.`,
36676
+ acp: `## Thread types
36677
+
36678
+ Recognize which KIND of effort you own and match the deliverable to it:
36679
+
36680
+ - **Research** \u2014 find out what's true. Deliverable is FINDINGS: traces, measurements, exact paths and
36681
+ errors, each load-bearing claim carrying a primary-source \`file:line\` or URL you actually opened. A
36682
+ bug investigation is headed for a FIX, so close with ranked fix options and one recommendation.
36683
+ - **Audit** \u2014 adversarially verify something that exists. Check every prong against the reference,
36684
+ re-verify load-bearing verdicts, cite evidence. Complete = every prong checked.
36685
+ - **Implementation** \u2014 land a DECIDED thing. Plan briefly \u2192 implement \u2192 run the repo's gates \u2192
36686
+ self-review the diff \u2192 fold in every real finding. Complete = MERGED into the project's mainline with
36687
+ docs updated and gates green.
36688
+ - **Planning** \u2014 the DESIGN is the deliverable: a durable plan file, open questions surfaced and then
36689
+ resolved into decisions in that file. A plan that exists only in chat has not been written.
36690
+
36691
+ ## Substantive implementation
36692
+
36693
+ For a non-trivial change: plan, implement, run the repo's build/lint/test gates, inspect every changed
36694
+ call site and downstream effect, self-review the diff, fix confirmed findings, and rerun affected
36695
+ checks. Depth scales with blast radius and yields to the project's conventions.`
36456
36696
  };
36457
36697
  MONITORS_DIR_FALLBACK = "<frizz>/cc-worker/skills/gh/scripts";
36458
36698
  }
36459
36699
  });
36460
36700
 
36701
+ // packages/server/src/backend/acp-agents.ts
36702
+ import { accessSync as accessSync3, constants, statSync as statSync14 } from "node:fs";
36703
+ import { delimiter as delimiter2, isAbsolute as isAbsolute6, join as join30 } from "node:path";
36704
+ function isExecutableFile(path3) {
36705
+ try {
36706
+ if (!statSync14(path3).isFile()) return false;
36707
+ if (process.platform !== "win32") accessSync3(path3, constants.X_OK);
36708
+ return true;
36709
+ } catch {
36710
+ return false;
36711
+ }
36712
+ }
36713
+ function findOnPath(command, env = process.env) {
36714
+ if (isAbsolute6(command)) return isExecutableFile(command) ? command : void 0;
36715
+ const dirs = (env.PATH ?? env.Path ?? "").split(delimiter2).filter(Boolean);
36716
+ const exts = process.platform === "win32" ? ["", ...(env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";")] : [""];
36717
+ for (const dir of dirs) {
36718
+ for (const ext of exts) {
36719
+ const candidate = join30(dir, command + ext.toLowerCase());
36720
+ if (isExecutableFile(candidate)) return candidate;
36721
+ if (ext && isExecutableFile(join30(dir, command + ext))) return join30(dir, command + ext);
36722
+ }
36723
+ }
36724
+ return void 0;
36725
+ }
36726
+ function acpAgentSpecs(custom2) {
36727
+ const byId = /* @__PURE__ */ new Map();
36728
+ for (const spec of ACP_AGENT_CATALOGUE) byId.set(spec.id, spec);
36729
+ for (const spec of custom2 ?? []) if (spec.id && spec.command) byId.set(spec.id, { ...spec, args: [...spec.args ?? []] });
36730
+ return [...byId.values()];
36731
+ }
36732
+ function listAcpAgents(custom2, env = process.env) {
36733
+ return acpAgentSpecs(custom2).map((spec) => {
36734
+ const bin = findOnPath(spec.command, env);
36735
+ return bin ? { ...spec, bin } : { ...spec };
36736
+ });
36737
+ }
36738
+ function resolveAcpAgent(id, custom2, env = process.env) {
36739
+ const spec = acpAgentSpecs(custom2).find((s) => s.id === id);
36740
+ if (!spec) return void 0;
36741
+ const bin = findOnPath(spec.command, env);
36742
+ return bin ? { ...spec, bin } : { ...spec };
36743
+ }
36744
+ var ACP_AGENT_CATALOGUE;
36745
+ var init_acp_agents = __esm({
36746
+ "packages/server/src/backend/acp-agents.ts"() {
36747
+ "use strict";
36748
+ init_src();
36749
+ ACP_AGENT_CATALOGUE = [
36750
+ { id: "opencode", label: "OpenCode", command: "opencode", args: ["acp"] },
36751
+ { id: "cursor", label: "Cursor agent", command: "cursor-agent", args: ["acp"] },
36752
+ { id: "gemini", label: "Gemini CLI", command: "gemini", args: ["--acp"] },
36753
+ { id: "copilot", label: "GitHub Copilot CLI", command: "copilot", args: ["--acp"] },
36754
+ { id: "kilo", label: "Kilo", command: "kilo", args: ["acp"] },
36755
+ { id: "qwen", label: "Qwen Code", command: "qwen", args: ["--acp"] },
36756
+ { id: "goose", label: "goose", command: "goose", args: ["acp"] },
36757
+ { id: "kimi", label: "Kimi CLI", command: "kimi", args: ["acp"] },
36758
+ // `grok agent stdio` is ACP over stdio (registry: `npx @xai-official/grok agent stdio`); verified
36759
+ // live 2026-09-16 against grok 1.0.13 — it mounted the probe MCP server and called its tool.
36760
+ { id: "grok", label: "Grok Build", command: "grok", args: ["agent", "stdio"] },
36761
+ // pi has no native ACP mode; `pi-acp` is its registry wrapper and takes no arguments.
36762
+ { id: "pi", label: "pi", command: "pi-acp", args: [] }
36763
+ ];
36764
+ }
36765
+ });
36766
+
36461
36767
  // packages/server/src/backend/claude-broker-client.ts
36462
36768
  import net from "node:net";
36463
36769
  function connectClaudeBroker(socketPath, handlers, options = {}) {
@@ -37360,8 +37666,8 @@ var init_claude_agent_broker_bridge = __esm({
37360
37666
  });
37361
37667
 
37362
37668
  // packages/server/src/dispatch.ts
37363
- import { closeSync as closeSync12, constants, existsSync as existsSync10, fstatSync as fstatSync2, lstatSync as lstatSync3, openSync as openSync13, readFileSync as readFileSync20, realpathSync as realpathSync8, statSync as statSync14, writeFileSync as writeFileSync10, mkdirSync as mkdirSync13, rmSync as rmSync9 } from "node:fs";
37364
- import { basename as basename6, join as join30, dirname as dirname12 } from "node:path";
37669
+ import { closeSync as closeSync12, constants as constants2, existsSync as existsSync10, fstatSync as fstatSync2, lstatSync as lstatSync3, openSync as openSync13, readFileSync as readFileSync20, realpathSync as realpathSync8, statSync as statSync15, writeFileSync as writeFileSync10, mkdirSync as mkdirSync13, rmSync as rmSync9 } from "node:fs";
37670
+ import { basename as basename6, join as join31, dirname as dirname13 } from "node:path";
37365
37671
  import { createHash as createHash11, randomUUID as randomUUID13 } from "node:crypto";
37366
37672
  function fallbackTitle(prompt) {
37367
37673
  const firstLine2 = prompt.trim().split("\n", 1)[0].trim();
@@ -37380,7 +37686,7 @@ function fallbackTitle(prompt) {
37380
37686
  }
37381
37687
  function resolveSlug(frizzDir, base, taken) {
37382
37688
  base = ThreadSlug.parse(base);
37383
- const isTaken = (slug) => existsSync10(join30(frizzDir, `${slug}.md`)) || (taken?.(slug) ?? false);
37689
+ const isTaken = (slug) => existsSync10(join31(frizzDir, `${slug}.md`)) || (taken?.(slug) ?? false);
37384
37690
  if (!isTaken(base)) return base;
37385
37691
  for (let n = 2; ; n++) {
37386
37692
  const suffix = `-${n}`;
@@ -37397,20 +37703,20 @@ function resolveLegacyThreadFile(projectDir, value) {
37397
37703
  if (!parsed.success) return null;
37398
37704
  try {
37399
37705
  const projectRoot = realpathSync8(projectDir);
37400
- const frizzPath = join30(projectRoot, ".frizz");
37706
+ const frizzPath = join31(projectRoot, ".frizz");
37401
37707
  const frizzStat = lstatSync3(frizzPath);
37402
37708
  if (!frizzStat.isDirectory() || frizzStat.isSymbolicLink()) return null;
37403
37709
  const realFrizz = realpathSync8(frizzPath);
37404
- if (dirname12(realFrizz) !== projectRoot || basename6(realFrizz) !== ".frizz") return null;
37405
- const path3 = join30(realFrizz, `${parsed.data}.md`);
37710
+ if (dirname13(realFrizz) !== projectRoot || basename6(realFrizz) !== ".frizz") return null;
37711
+ const path3 = join31(realFrizz, `${parsed.data}.md`);
37406
37712
  const before = lstatSync3(path3);
37407
37713
  if (!before.isFile() || before.isSymbolicLink()) return null;
37408
37714
  const realPath = realpathSync8(path3);
37409
- if (dirname12(realPath) !== realFrizz || basename6(realPath) !== `${parsed.data}.md`) return null;
37715
+ if (dirname13(realPath) !== realFrizz || basename6(realPath) !== `${parsed.data}.md`) return null;
37410
37716
  let contents;
37411
37717
  let openedBefore;
37412
37718
  let openedAfter;
37413
- const fd = openSync13(path3, constants.O_RDONLY | constants.O_NOFOLLOW);
37719
+ const fd = openSync13(path3, constants2.O_RDONLY | constants2.O_NOFOLLOW);
37414
37720
  try {
37415
37721
  openedBefore = fstatSync2(fd);
37416
37722
  contents = readFileSync20(fd);
@@ -37446,7 +37752,7 @@ function boardAuthorizesAdoption(board, slug) {
37446
37752
  return !board.errorItems.some((item) => item.file === `${slug}.md`);
37447
37753
  }
37448
37754
  function ensureSafeDirectDirectory(parent, name) {
37449
- const path3 = join30(parent, name);
37755
+ const path3 = join31(parent, name);
37450
37756
  try {
37451
37757
  mkdirSync13(path3);
37452
37758
  } catch (error) {
@@ -37456,7 +37762,7 @@ function ensureSafeDirectDirectory(parent, name) {
37456
37762
  const stat2 = lstatSync3(path3);
37457
37763
  if (!stat2.isDirectory() || stat2.isSymbolicLink()) throw new Error("unsafe project directory");
37458
37764
  const real = realpathSync8(path3);
37459
- if (dirname12(real) !== parent || basename6(real) !== name) throw new Error("unsafe project directory");
37765
+ if (dirname13(real) !== parent || basename6(real) !== name) throw new Error("unsafe project directory");
37460
37766
  return real;
37461
37767
  }
37462
37768
  function scratchDirRelPath(sessionId) {
@@ -37476,8 +37782,8 @@ function loadWorkerPrompt(kind = "claude") {
37476
37782
  function monitorScriptsDir() {
37477
37783
  const plugin = workerPluginDir();
37478
37784
  if (!plugin) return void 0;
37479
- const dir = join30(plugin, "skills", "gh", "scripts");
37480
- return existsSync10(join30(dir, "ci-watch.mjs")) ? dir : void 0;
37785
+ const dir = join31(plugin, "skills", "gh", "scripts");
37786
+ return existsSync10(join31(dir, "ci-watch.mjs")) ? dir : void 0;
37481
37787
  }
37482
37788
  function codexScratchpadHookConfig(hookScript, sessionId) {
37483
37789
  if (!hookScript || !sessionId) return {};
@@ -37489,7 +37795,7 @@ function codexScratchpadHookConfig(hookScript, sessionId) {
37489
37795
  }
37490
37796
  ]
37491
37797
  });
37492
- const bashBackgroundHook = join30(dirname12(hookScript), "bash-background.mjs");
37798
+ const bashBackgroundHook = join31(dirname13(hookScript), "bash-background.mjs");
37493
37799
  return {
37494
37800
  bypass_hook_trust: true,
37495
37801
  hooks: {
@@ -37515,7 +37821,7 @@ function codexScratchpadHookConfig(hookScript, sessionId) {
37515
37821
  }
37516
37822
  function scratchpadHookScript() {
37517
37823
  const plugin = workerPluginDir();
37518
- return plugin ? join30(plugin, "hooks", "scratchpad.mjs") : void 0;
37824
+ return plugin ? join31(plugin, "hooks", "scratchpad.mjs") : void 0;
37519
37825
  }
37520
37826
  function composePrompt(sessionId, prompt, kind = "claude") {
37521
37827
  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.";
@@ -37531,10 +37837,10 @@ function scratchpadOrientation(sessionId, kind = "claude") {
37531
37837
  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.`;
37532
37838
  }
37533
37839
  function frizzConfigBlock(projectDir) {
37534
- const path3 = join30(projectDir, "FRIZZ.md");
37840
+ const path3 = join31(projectDir, "FRIZZ.md");
37535
37841
  let body;
37536
37842
  try {
37537
- const st = statSync14(path3);
37843
+ const st = statSync15(path3);
37538
37844
  if (!st.isFile() || st.size > FRIZZ_MD_MAX_BYTES) return "";
37539
37845
  body = readFileSync20(path3, "utf8").trim();
37540
37846
  } catch {
@@ -37575,7 +37881,7 @@ function resolveFrizzMcp(target, moduleUrl = import.meta.url, env = process.env,
37575
37881
  const { stateDir, serverLock, projectId } = typeof target === "string" ? { stateDir: target } : target;
37576
37882
  const pluginDir = resolveWorkerPluginDir(moduleUrl, env);
37577
37883
  if (!pluginDir) return void 0;
37578
- const scriptPath = join30(pluginDir, "bin", FRIZZ_MCP.script);
37884
+ const scriptPath = join31(pluginDir, "bin", FRIZZ_MCP.script);
37579
37885
  if (!existsSync10(scriptPath)) return void 0;
37580
37886
  return {
37581
37887
  scriptPath,
@@ -37658,7 +37964,7 @@ function buildClaudeResumeCommand(opts) {
37658
37964
  }
37659
37965
  function createDispatcher(deps) {
37660
37966
  const readBoardSource = deps.readBoard ?? readBoard;
37661
- const frizzDir = join30(deps.project.dir, ".frizz");
37967
+ const frizzDir = join31(deps.project.dir, ".frizz");
37662
37968
  const adoptionRuntime = deps.adoptionRuntime ?? productionRuntime;
37663
37969
  function cleanupPrewrites(built) {
37664
37970
  for (const path3 of new Set(built.prewrite.map((file) => file.path))) {
@@ -37671,7 +37977,7 @@ function createDispatcher(deps) {
37671
37977
  function cleanupDispatchFiles(scratchRel, built, sessionId) {
37672
37978
  cleanupPrewrites(built);
37673
37979
  try {
37674
- rmSync9(join30(deps.project.dir, scratchRel), { force: true, recursive: true });
37980
+ rmSync9(join31(deps.project.dir, scratchRel), { force: true, recursive: true });
37675
37981
  } catch {
37676
37982
  }
37677
37983
  cleanupAdoptionSessionFiles(deps.project.dir, sessionId);
@@ -37681,7 +37987,7 @@ function createDispatcher(deps) {
37681
37987
  input = DispatchInput.parse(input);
37682
37988
  const settings = deps.getSettings();
37683
37989
  const kind = opts?.backend ?? "claude";
37684
- if (deps.preflightAuth && await deps.preflightAuth(kind).catch(() => "unknown") === "signed-out") {
37990
+ if (kind !== "acp" && deps.preflightAuth && await deps.preflightAuth(kind).catch(() => "unknown") === "signed-out") {
37685
37991
  throw new ProviderAuthRequiredError(kind);
37686
37992
  }
37687
37993
  if (kind === "codex" && deps.preflightCodexBinary && await deps.preflightCodexBinary().catch(() => "unknown") === "missing") {
@@ -37754,6 +38060,52 @@ function createDispatcher(deps) {
37754
38060
  throw new Error(`Codex app-server could not start this thread: ${err.message}. Check that \`codex\` is installed and its app-server protocol matches the pinned revision (re-pin if you upgraded codex).`);
37755
38061
  }
37756
38062
  }
38063
+ if (kind === "acp") {
38064
+ const bridge = deps.acpBridge;
38065
+ const agentId = acpAgentIdFromModel(model);
38066
+ if (!bridge || !agentId) {
38067
+ cleanupDispatchFiles(scratchRel, { argv: [], env: {}, prewrite: [] }, sessionId);
38068
+ throw new Error(!bridge ? "The ACP bridge is unavailable; cannot start this thread." : `An ACP dispatch needs an agent: pick one in the composer (model \`acp:<agent>\`), got ${JSON.stringify(model ?? null)}.`);
38069
+ }
38070
+ const firstPrompt = [loadWorkerPrompt("acp"), scratchpadOrientation(sessionId, kind), frizzConfigBlock(deps.project.dir), prompt].filter(Boolean).join("\n\n");
38071
+ try {
38072
+ const spawned = await bridge.spawnDispatch({ threadSlug: slug, sessionId, cwd: deps.project.dir, agentId, modelId: acpModelIdFromModel(model), prompt: firstPrompt, userText: input.prompt });
38073
+ deps.storage.upsertSession({
38074
+ slug,
38075
+ session_id: sessionId,
38076
+ thread_name: threadIdentityName(slug),
38077
+ spawned_at: (/* @__PURE__ */ new Date()).toISOString(),
38078
+ last_read_at: null,
38079
+ unread: 0,
38080
+ exited: 0,
38081
+ archived: 0,
38082
+ rested_at: null,
38083
+ title_auto: input.title?.trim() ? 0 : 1,
38084
+ title_locked: 0,
38085
+ title: registryTitle,
38086
+ state: "open",
38087
+ meta: null,
38088
+ seen_at: null,
38089
+ transcript_id: null,
38090
+ model: model ?? null,
38091
+ effort: null,
38092
+ permission_mode: permissionMode
38093
+ });
38094
+ deps.storage.setBackend(slug, "acp");
38095
+ deps.storage.setAgentSession(slug, spawned.acpSessionId);
38096
+ deps.storage.setAcpAgent(slug, agentId);
38097
+ void deps.board.rebuild().catch(() => {
38098
+ });
38099
+ return { slug, sessionId };
38100
+ } catch (err) {
38101
+ try {
38102
+ bridge.releaseSession(slug, sessionId, "session-deleted");
38103
+ } catch {
38104
+ }
38105
+ cleanupDispatchFiles(scratchRel, { argv: [], env: {}, prewrite: [] }, sessionId);
38106
+ throw new Error(`The ACP agent could not start this thread: ${err.message}`);
38107
+ }
38108
+ }
37757
38109
  if (kind === "claude") {
37758
38110
  const bridge = deps.claudeBroker;
37759
38111
  if (!bridge) {
@@ -38059,6 +38411,8 @@ var init_dispatch = __esm({
38059
38411
  init_worker_plugin_dir();
38060
38412
  init_workerPrompt();
38061
38413
  init_codex();
38414
+ init_acp_agents();
38415
+ init_src();
38062
38416
  init_claude_agent_broker_bridge();
38063
38417
  init_claude_effort();
38064
38418
  init_auth_status();
@@ -38101,7 +38455,10 @@ var init_dispatch = __esm({
38101
38455
  FRIZZ_MD_MAX_BYTES = 64 * 1024;
38102
38456
  WORKER_DISPATCH_PERMISSION = {
38103
38457
  claude: "auto",
38104
- codex: "bypassPermissions"
38458
+ codex: "bypassPermissions",
38459
+ // An ACP agent's permissions are its own (plans/acp-backend.md, decision 7): Frizz sends no mode and
38460
+ // mirrors whatever the agent asks as a card. The value here is recorded on the row and nothing else.
38461
+ acp: "auto"
38105
38462
  };
38106
38463
  missingPluginReported = false;
38107
38464
  }
@@ -38113,7 +38470,7 @@ import {
38113
38470
  watch as fsWatch
38114
38471
  } from "node:fs";
38115
38472
  import { homedir as homedir14 } from "node:os";
38116
- import { dirname as dirname13, join as join31 } from "node:path";
38473
+ import { dirname as dirname14, join as join32 } from "node:path";
38117
38474
  import watcher from "@parcel/watcher";
38118
38475
  function appServerTurnStalled(liveness, lastActivityAt, nowMs) {
38119
38476
  if (!liveness) return false;
@@ -38162,7 +38519,7 @@ function registeredLegacyFileIsTerminal(projectDir, slug) {
38162
38519
  return status === "done" || status === "dismissed";
38163
38520
  }
38164
38521
  function isBoardRelevantFrizzPath(roots, path3) {
38165
- return path3.endsWith(".md") && roots.has(dirname13(path3));
38522
+ return path3.endsWith(".md") && roots.has(dirname14(path3));
38166
38523
  }
38167
38524
  function futureSnooze(row, nowMs) {
38168
38525
  const parsed = SnoozeUntil.safeParse(row.snoozed_until);
@@ -38653,7 +39010,7 @@ function sessionThreadView(projectDir, storage, row, rawTele, registeredLegacyTe
38653
39010
  // from today's dispatch preference: unknown/migrated rows remain unmarked, while rows whose
38654
39011
  // database default was explicitly normalized to "claude" get the same per-thread identity as
38655
39012
  // Codex rows.
38656
- backend: row.backend === "claude" || row.backend === "codex" ? row.backend : void 0,
39013
+ backend: row.backend === "claude" || row.backend === "codex" || row.backend === "acp" ? row.backend : void 0,
38657
39014
  // Only a persisted, validated per-session value is exposed. A migrated row stays visibly unknown;
38658
39015
  // never label it with today's global defaults (which may not match its running process).
38659
39016
  permissionMode,
@@ -38960,7 +39317,7 @@ function createBoard(project, storage, bus, tailer, bootId, deps = {}) {
38960
39317
  if (parcelSub || stopped) return Promise.resolve();
38961
39318
  if (watchSetup) return watchSetup;
38962
39319
  const setup = (async () => {
38963
- const dir = join31(project.dir, ".frizz");
39320
+ const dir = join32(project.dir, ".frizz");
38964
39321
  const roots = /* @__PURE__ */ new Set([dir]);
38965
39322
  try {
38966
39323
  roots.add(realpathSync9(dir));
@@ -38975,7 +39332,7 @@ function createBoard(project, storage, bus, tailer, bootId, deps = {}) {
38975
39332
  }
38976
39333
  if (events.some((event) => isBoardRelevantFrizzPath(roots, event.path))) scheduleRebuild();
38977
39334
  },
38978
- { ignore: FRIZZ_WATCH_IGNORED_DIRS.map((name) => join31(dir, name)) }
39335
+ { ignore: FRIZZ_WATCH_IGNORED_DIRS.map((name) => join32(dir, name)) }
38979
39336
  );
38980
39337
  if (stopped) {
38981
39338
  await next.unsubscribe();
@@ -39293,7 +39650,7 @@ var init_wake_store = __esm({
39293
39650
  });
39294
39651
 
39295
39652
  // packages/server/src/github-review.ts
39296
- import { execFile as execFile6 } from "node:child_process";
39653
+ import { execFile as execFile7 } from "node:child_process";
39297
39654
  import { promisify as promisify6 } from "node:util";
39298
39655
  function refKey(ref) {
39299
39656
  return `${ref.kind === "issue" ? "issue:" : ""}${ref.owner}/${ref.repo}#${ref.number}`;
@@ -39712,7 +40069,7 @@ var execFileAsync4, GH_TIMEOUT_MS, GH_MAX_BUFFER, REQUEST_TIMEOUT_MS, MAX_REFS_P
39712
40069
  var init_github_review = __esm({
39713
40070
  "packages/server/src/github-review.ts"() {
39714
40071
  "use strict";
39715
- execFileAsync4 = promisify6(execFile6);
40072
+ execFileAsync4 = promisify6(execFile7);
39716
40073
  GH_TIMEOUT_MS = 3e4;
39717
40074
  GH_MAX_BUFFER = 1024 * 1024;
39718
40075
  REQUEST_TIMEOUT_MS = 3e4;
@@ -39849,7 +40206,7 @@ var init_pr_watch_noise = __esm({
39849
40206
  });
39850
40207
 
39851
40208
  // packages/server/src/scheduler.ts
39852
- import { execFile as execFile7 } from "node:child_process";
40209
+ import { execFile as execFile8 } from "node:child_process";
39853
40210
  import { promisify as promisify7 } from "node:util";
39854
40211
  import { createHash as createHash12, randomUUID as randomUUID14 } from "node:crypto";
39855
40212
  function failureSignature(rollup) {
@@ -41740,7 +42097,7 @@ var init_scheduler = __esm({
41740
42097
  init_pr_watch_noise();
41741
42098
  init_github_review();
41742
42099
  init_logging();
41743
- execFileAsync5 = promisify7(execFile7);
42100
+ execFileAsync5 = promisify7(execFile8);
41744
42101
  LIMIT_FENCE_PREFIX = "limit";
41745
42102
  LIMIT_HINT_PREFIX = "limit:";
41746
42103
  LIMIT_RESUME_GRACE_MS = 6e4;
@@ -41862,7 +42219,7 @@ var init_resume = __esm({
41862
42219
  });
41863
42220
 
41864
42221
  // packages/server/src/backend/claude.ts
41865
- import { join as join32 } from "node:path";
42222
+ import { join as join33 } from "node:path";
41866
42223
  function toolResultText2(content) {
41867
42224
  if (typeof content === "string") return content;
41868
42225
  if (!Array.isArray(content)) return "";
@@ -41953,7 +42310,7 @@ function createClaudeBackend(opts) {
41953
42310
  return { argv, env: claudeWorkerEnvironment(), prewrite: [] };
41954
42311
  },
41955
42312
  transcriptPath(sessionId) {
41956
- return join32(opts.logDir, `${sessionId}.jsonl`);
42313
+ return join33(opts.logDir, `${sessionId}.jsonl`);
41957
42314
  },
41958
42315
  parseLine(line) {
41959
42316
  return parseClaudeLine(line);
@@ -41977,6 +42334,1185 @@ var init_claude = __esm({
41977
42334
  }
41978
42335
  });
41979
42336
 
42337
+ // packages/server/src/backend/acp-transcript.ts
42338
+ import { appendFileSync as appendFileSync3, existsSync as existsSync12, mkdirSync as mkdirSync14, readFileSync as readFileSync21 } from "node:fs";
42339
+ import { dirname as dirname15, join as join34 } from "node:path";
42340
+ function acpTranscriptPath(stateDir, sessionId) {
42341
+ return join34(stateDir, ACP_TRANSCRIPT_DIR, `${sessionId}.jsonl`);
42342
+ }
42343
+ function parseAcpRecord(line) {
42344
+ if (!line.trim()) return void 0;
42345
+ let raw2;
42346
+ try {
42347
+ raw2 = JSON.parse(line);
42348
+ } catch {
42349
+ return void 0;
42350
+ }
42351
+ if (!raw2 || typeof raw2 !== "object" || typeof raw2.kind !== "string") return void 0;
42352
+ const kind = raw2.kind;
42353
+ if (kind === "acp-session" || kind === "acp-note" || EVENT_KINDS.has(kind)) return raw2;
42354
+ return void 0;
42355
+ }
42356
+ function parseAcpLine(line) {
42357
+ const rec = parseAcpRecord(line);
42358
+ if (!rec || rec.kind === "acp-session" || rec.kind === "acp-note") return [];
42359
+ const { messageId: _m, acp: _a, ...ev } = rec;
42360
+ return [ev];
42361
+ }
42362
+ function createAcpBackend(opts) {
42363
+ return {
42364
+ kind: "acp",
42365
+ buildSpawn(_o) {
42366
+ throw new Error("acp runs via the ACP bridge; this argv builder has no live caller");
42367
+ },
42368
+ buildResume(_o) {
42369
+ throw new Error("acp runs via the ACP bridge; this argv builder has no live caller");
42370
+ },
42371
+ // Deterministic: the bridge names the file after the frizz session id, so no discovery step.
42372
+ transcriptPath(sessionId) {
42373
+ return acpTranscriptPath(opts.stateDir, sessionId);
42374
+ },
42375
+ parseLine: parseAcpLine,
42376
+ foldLine(state, line) {
42377
+ const rec = parseAcpRecord(line);
42378
+ if (!rec) return;
42379
+ if (rec.kind === "acp-session") {
42380
+ if (rec.model) {
42381
+ state.model = rec.model;
42382
+ state.profileAt = rec.at;
42383
+ state.profileRevision = (state.profileRevision ?? 0) + 1;
42384
+ }
42385
+ return;
42386
+ }
42387
+ for (const ev of parseAcpLine(line)) applyEvent(state, ev);
42388
+ }
42389
+ };
42390
+ }
42391
+ function clip2(text2, max) {
42392
+ return text2.length > max ? `${text2.slice(0, max - 1)}\u2026` : text2;
42393
+ }
42394
+ function toolTarget(rawInput, acp) {
42395
+ const input = rawInput && typeof rawInput === "object" ? rawInput : void 0;
42396
+ const command = typeof input?.command === "string" ? input.command : void 0;
42397
+ const path3 = typeof input?.filePath === "string" ? input.filePath : typeof input?.file_path === "string" ? input.file_path : typeof input?.path === "string" ? input.path : acp?.locations?.[0];
42398
+ return { command, path: path3, input };
42399
+ }
42400
+ function toolCallFor(ev, acp) {
42401
+ const { command, path: path3, input } = toolTarget(ev.input, acp);
42402
+ const detail2 = command ?? path3 ?? acp?.title ?? "";
42403
+ const call = { name: ev.name, status: "pending" };
42404
+ if (detail2) call.detail = clip2(detail2, 500);
42405
+ if (command) call.command = command;
42406
+ else if (input && Object.keys(input).length) call.input = clip2(JSON.stringify(input, null, 2), TOOL_TEXT_MAX);
42407
+ return call;
42408
+ }
42409
+ function upgradeToolTarget(call, acp) {
42410
+ if (!acp || call.command || call.detail && call.detail !== call.name) return;
42411
+ const { command, path: path3, input } = toolTarget(acp.input, acp);
42412
+ const detail2 = command ?? path3;
42413
+ if (!detail2) return;
42414
+ call.detail = clip2(detail2, 500);
42415
+ if (command) call.command = command;
42416
+ else if (input && Object.keys(input).length && !call.input) call.input = clip2(JSON.stringify(input, null, 2), TOOL_TEXT_MAX);
42417
+ }
42418
+ function projectAcpTranscript(raw2, identityPrefix = "acp") {
42419
+ const out = [];
42420
+ let current;
42421
+ let currentMessageId;
42422
+ const openTools = /* @__PURE__ */ new Map();
42423
+ const lines = raw2.split("\n");
42424
+ const close2 = () => {
42425
+ current = void 0;
42426
+ currentMessageId = void 0;
42427
+ };
42428
+ const openAssistant = (i, at) => {
42429
+ if (!current) {
42430
+ current = { sourceId: `${identityPrefix}:${i}`, role: "assistant", text: "", tools: [], parts: [], ...at ? { at } : {} };
42431
+ out.push(current);
42432
+ }
42433
+ return current;
42434
+ };
42435
+ const appendText = (msg, text2) => {
42436
+ const last = msg.parts[msg.parts.length - 1];
42437
+ if (last?.kind === "text") last.text += text2;
42438
+ else msg.parts.push({ kind: "text", text: text2 });
42439
+ msg.text = msg.parts.filter((p) => p.kind === "text").map((p) => p.text).join("\n\n");
42440
+ };
42441
+ const appendTool = (msg, tool) => {
42442
+ const last = msg.parts[msg.parts.length - 1];
42443
+ if (last?.kind === "tools") last.tools.push(tool);
42444
+ else msg.parts.push({ kind: "tools", tools: [tool] });
42445
+ msg.tools.push(tool);
42446
+ };
42447
+ for (let i = 0; i < lines.length; i++) {
42448
+ const rec = parseAcpRecord(lines[i]);
42449
+ if (!rec) continue;
42450
+ switch (rec.kind) {
42451
+ case "acp-session":
42452
+ case "acp-note":
42453
+ case "title":
42454
+ case "context-usage":
42455
+ case "compaction":
42456
+ case "agent-report":
42457
+ case "agent-instruction":
42458
+ break;
42459
+ case "user-message": {
42460
+ close2();
42461
+ const text2 = rec.text ?? "";
42462
+ out.push({ sourceId: `${identityPrefix}:${i}`, role: "user", text: text2, tools: [], parts: [], ...rec.at ? { at: rec.at } : {}, ...rec.synthetic ? { wake: true } : {} });
42463
+ break;
42464
+ }
42465
+ case "turn-start":
42466
+ break;
42467
+ case "turn-end":
42468
+ close2();
42469
+ break;
42470
+ case "reasoning": {
42471
+ close2();
42472
+ out.push({ sourceId: `${identityPrefix}:${i}`, role: "assistant", kind: "reasoning", text: rec.text, tools: [], parts: [], ...rec.at ? { at: rec.at } : {} });
42473
+ break;
42474
+ }
42475
+ case "assistant-text": {
42476
+ const msg = openAssistant(i, rec.at);
42477
+ const mid = rec.messageId;
42478
+ if (mid && mid === currentMessageId) {
42479
+ const last = msg.parts[msg.parts.length - 1];
42480
+ if (last?.kind === "text") {
42481
+ last.text += rec.text;
42482
+ msg.text = msg.parts.filter((p) => p.kind === "text").map((p) => p.text).join("\n\n");
42483
+ break;
42484
+ }
42485
+ }
42486
+ currentMessageId = mid;
42487
+ appendText(msg, rec.text);
42488
+ break;
42489
+ }
42490
+ case "tool-call": {
42491
+ const msg = openAssistant(i, rec.at);
42492
+ currentMessageId = void 0;
42493
+ const call = toolCallFor(rec, rec.acp);
42494
+ openTools.set(rec.id, call);
42495
+ appendTool(msg, call);
42496
+ break;
42497
+ }
42498
+ case "tool-result": {
42499
+ const call = openTools.get(rec.id);
42500
+ if (!call) break;
42501
+ openTools.delete(rec.id);
42502
+ const failed = rec.acp?.status === "failed" || rec.acp?.kind === "failed";
42503
+ call.status = failed ? "failed" : "completed";
42504
+ upgradeToolTarget(call, rec.acp);
42505
+ if (rec.text) call.output = clip2(rec.text, TOOL_TEXT_MAX);
42506
+ break;
42507
+ }
42508
+ case "provider-error": {
42509
+ const msg = openAssistant(i, rec.at);
42510
+ msg.providerError = rec.error;
42511
+ close2();
42512
+ break;
42513
+ }
42514
+ }
42515
+ }
42516
+ return out;
42517
+ }
42518
+ var ACP_TRANSCRIPT_DIR, EVENT_KINDS, AcpTranscriptWriter, TOOL_TEXT_MAX;
42519
+ var init_acp_transcript = __esm({
42520
+ "packages/server/src/backend/acp-transcript.ts"() {
42521
+ "use strict";
42522
+ init_tailer();
42523
+ ACP_TRANSCRIPT_DIR = "acp";
42524
+ EVENT_KINDS = /* @__PURE__ */ new Set([
42525
+ "turn-start",
42526
+ "turn-end",
42527
+ "provider-error",
42528
+ "assistant-text",
42529
+ "user-message",
42530
+ "tool-call",
42531
+ "tool-result",
42532
+ "reasoning",
42533
+ "agent-report",
42534
+ "agent-instruction",
42535
+ "title",
42536
+ "compaction",
42537
+ "context-usage"
42538
+ ]);
42539
+ AcpTranscriptWriter = class {
42540
+ constructor(path3) {
42541
+ this.path = path3;
42542
+ mkdirSync14(dirname15(path3), { recursive: true });
42543
+ if (!existsSync12(path3)) appendFileSync3(path3, "");
42544
+ }
42545
+ append(record) {
42546
+ appendFileSync3(this.path, JSON.stringify(record) + "\n");
42547
+ }
42548
+ };
42549
+ TOOL_TEXT_MAX = 12e3;
42550
+ }
42551
+ });
42552
+
42553
+ // packages/server/src/backend/acp-types.ts
42554
+ function parseSessionUpdate(raw2) {
42555
+ const parsed = AcpSessionUpdateKnown.safeParse(raw2);
42556
+ return parsed.success ? parsed.data : { sessionUpdate: "unknown", raw: raw2 };
42557
+ }
42558
+ function contentText(block) {
42559
+ if (!block) return void 0;
42560
+ if (block.type === "text" && typeof block.text === "string") return block.text;
42561
+ if (block.type === "resource") {
42562
+ const r = block.resource;
42563
+ return typeof r?.text === "string" ? r.text : void 0;
42564
+ }
42565
+ return void 0;
42566
+ }
42567
+ var ACP_PROTOCOL_VERSION, JsonRpcId, JsonRpcError, JsonRpcInbound, ACP_ERROR_AUTH_REQUIRED, ACP_ERROR_METHOD_NOT_FOUND, ACP_ERROR_INTERNAL, AcpAuthMethod, AcpAgentCapabilities, AcpInitializeResult, AcpMcpServerStdio, AcpConfigOption, AcpNewSessionResult, AcpSetConfigOptionResult, AcpLoadSessionResult, AcpContentBlock, AcpToolKind, AcpToolCallStatus, AcpToolCallContent, AcpToolCallLocation, ToolCallFields, AcpToolCallUpdate, AcpPlanEntry, AcpSessionUpdateKnown, AcpSessionNotification, AcpStopReason, AcpPromptResult, AcpPermissionOptionKind, AcpPermissionOption, AcpRequestPermissionParams;
42568
+ var init_acp_types = __esm({
42569
+ "packages/server/src/backend/acp-types.ts"() {
42570
+ "use strict";
42571
+ init_zod();
42572
+ ACP_PROTOCOL_VERSION = 1;
42573
+ JsonRpcId = external_exports.union([external_exports.string(), external_exports.number()]);
42574
+ JsonRpcError = external_exports.object({
42575
+ code: external_exports.number(),
42576
+ message: external_exports.string(),
42577
+ data: external_exports.unknown().optional()
42578
+ }).passthrough();
42579
+ JsonRpcInbound = external_exports.union([
42580
+ external_exports.object({ jsonrpc: external_exports.literal("2.0").optional(), id: JsonRpcId, result: external_exports.unknown() }).passthrough(),
42581
+ external_exports.object({ jsonrpc: external_exports.literal("2.0").optional(), id: JsonRpcId, error: JsonRpcError }).passthrough(),
42582
+ external_exports.object({ jsonrpc: external_exports.literal("2.0").optional(), id: JsonRpcId, method: external_exports.string(), params: external_exports.unknown().optional() }).passthrough(),
42583
+ external_exports.object({ jsonrpc: external_exports.literal("2.0").optional(), method: external_exports.string(), params: external_exports.unknown().optional() }).passthrough()
42584
+ ]);
42585
+ ACP_ERROR_AUTH_REQUIRED = -32e3;
42586
+ ACP_ERROR_METHOD_NOT_FOUND = -32601;
42587
+ ACP_ERROR_INTERNAL = -32603;
42588
+ AcpAuthMethod = external_exports.object({
42589
+ id: external_exports.string(),
42590
+ name: external_exports.string().optional(),
42591
+ description: external_exports.string().optional(),
42592
+ // Absent = "agent" (call `authenticate {methodId}`); "terminal" = relaunch the same program
42593
+ // interactively with `args`/`env` — which Frizz does not do; it tells the operator instead.
42594
+ type: external_exports.string().optional()
42595
+ }).passthrough();
42596
+ AcpAgentCapabilities = external_exports.object({
42597
+ loadSession: external_exports.boolean().optional(),
42598
+ promptCapabilities: external_exports.object({ image: external_exports.boolean().optional(), audio: external_exports.boolean().optional(), embeddedContext: external_exports.boolean().optional() }).passthrough().optional(),
42599
+ mcpCapabilities: external_exports.object({ http: external_exports.boolean().optional(), sse: external_exports.boolean().optional() }).passthrough().optional(),
42600
+ // Object markers: `{}` present = supported, absent = not.
42601
+ sessionCapabilities: external_exports.object({
42602
+ list: external_exports.unknown().optional(),
42603
+ resume: external_exports.unknown().optional(),
42604
+ close: external_exports.unknown().optional(),
42605
+ fork: external_exports.unknown().optional(),
42606
+ delete: external_exports.unknown().optional()
42607
+ }).passthrough().optional()
42608
+ }).passthrough();
42609
+ AcpInitializeResult = external_exports.object({
42610
+ protocolVersion: external_exports.number(),
42611
+ agentCapabilities: AcpAgentCapabilities.optional(),
42612
+ authMethods: external_exports.array(AcpAuthMethod).optional(),
42613
+ agentInfo: external_exports.object({ name: external_exports.string().optional(), title: external_exports.string().optional(), version: external_exports.string().optional() }).passthrough().optional()
42614
+ }).passthrough();
42615
+ AcpMcpServerStdio = external_exports.object({
42616
+ name: external_exports.string(),
42617
+ command: external_exports.string(),
42618
+ args: external_exports.array(external_exports.string()),
42619
+ env: external_exports.array(external_exports.object({ name: external_exports.string(), value: external_exports.string() }))
42620
+ });
42621
+ AcpConfigOption = external_exports.object({
42622
+ id: external_exports.string(),
42623
+ name: external_exports.string().optional(),
42624
+ category: external_exports.string().optional(),
42625
+ type: external_exports.string().optional(),
42626
+ currentValue: external_exports.unknown().optional(),
42627
+ options: external_exports.array(external_exports.object({ value: external_exports.string(), name: external_exports.string().optional() }).passthrough()).optional()
42628
+ }).passthrough();
42629
+ AcpNewSessionResult = external_exports.object({
42630
+ sessionId: external_exports.string().min(1),
42631
+ configOptions: external_exports.array(AcpConfigOption).nullish(),
42632
+ modes: external_exports.unknown().optional()
42633
+ }).passthrough();
42634
+ AcpSetConfigOptionResult = external_exports.object({ configOptions: external_exports.array(AcpConfigOption).nullish() }).passthrough();
42635
+ AcpLoadSessionResult = external_exports.object({
42636
+ configOptions: external_exports.array(AcpConfigOption).nullish(),
42637
+ modes: external_exports.unknown().optional()
42638
+ }).passthrough();
42639
+ AcpContentBlock = external_exports.union([
42640
+ external_exports.object({ type: external_exports.literal("text"), text: external_exports.string() }).passthrough(),
42641
+ external_exports.object({ type: external_exports.literal("resource_link"), uri: external_exports.string(), name: external_exports.string().optional() }).passthrough(),
42642
+ external_exports.object({ type: external_exports.literal("resource"), resource: external_exports.object({ uri: external_exports.string(), text: external_exports.string().optional() }).passthrough() }).passthrough(),
42643
+ external_exports.object({ type: external_exports.literal("image"), mimeType: external_exports.string().optional(), data: external_exports.string().optional() }).passthrough(),
42644
+ external_exports.object({ type: external_exports.string() }).passthrough()
42645
+ ]);
42646
+ AcpToolKind = external_exports.enum(["read", "edit", "delete", "move", "search", "execute", "think", "fetch", "switch_mode", "other"]);
42647
+ AcpToolCallStatus = external_exports.enum(["pending", "in_progress", "completed", "failed"]);
42648
+ AcpToolCallContent = external_exports.union([
42649
+ external_exports.object({ type: external_exports.literal("content"), content: AcpContentBlock }).passthrough(),
42650
+ external_exports.object({ type: external_exports.literal("diff"), path: external_exports.string(), oldText: external_exports.string().nullish(), newText: external_exports.string() }).passthrough(),
42651
+ external_exports.object({ type: external_exports.literal("terminal"), terminalId: external_exports.string() }).passthrough(),
42652
+ external_exports.object({ type: external_exports.string() }).passthrough()
42653
+ ]);
42654
+ AcpToolCallLocation = external_exports.object({ path: external_exports.string(), line: external_exports.number().nullish() }).passthrough();
42655
+ ToolCallFields = {
42656
+ toolCallId: external_exports.string(),
42657
+ title: external_exports.string().optional(),
42658
+ kind: external_exports.string().optional(),
42659
+ status: external_exports.string().optional(),
42660
+ content: external_exports.array(AcpToolCallContent).nullish(),
42661
+ locations: external_exports.array(AcpToolCallLocation).nullish(),
42662
+ rawInput: external_exports.unknown().optional(),
42663
+ rawOutput: external_exports.unknown().optional()
42664
+ };
42665
+ AcpToolCallUpdate = external_exports.object(ToolCallFields).passthrough();
42666
+ AcpPlanEntry = external_exports.object({ content: external_exports.string(), priority: external_exports.string().optional(), status: external_exports.string().optional() }).passthrough();
42667
+ AcpSessionUpdateKnown = external_exports.discriminatedUnion("sessionUpdate", [
42668
+ external_exports.object({ sessionUpdate: external_exports.literal("user_message_chunk"), content: AcpContentBlock, messageId: external_exports.string().optional() }).passthrough(),
42669
+ external_exports.object({ sessionUpdate: external_exports.literal("agent_message_chunk"), content: AcpContentBlock, messageId: external_exports.string().optional() }).passthrough(),
42670
+ external_exports.object({ sessionUpdate: external_exports.literal("agent_thought_chunk"), content: AcpContentBlock, messageId: external_exports.string().optional() }).passthrough(),
42671
+ external_exports.object({ sessionUpdate: external_exports.literal("tool_call"), ...ToolCallFields }).passthrough(),
42672
+ external_exports.object({ sessionUpdate: external_exports.literal("tool_call_update"), ...ToolCallFields }).passthrough(),
42673
+ external_exports.object({ sessionUpdate: external_exports.literal("plan"), entries: external_exports.array(AcpPlanEntry) }).passthrough(),
42674
+ external_exports.object({ sessionUpdate: external_exports.literal("usage_update"), used: external_exports.number(), size: external_exports.number(), cost: external_exports.unknown().optional() }).passthrough(),
42675
+ external_exports.object({ sessionUpdate: external_exports.literal("session_info_update"), title: external_exports.string().nullish(), updatedAt: external_exports.string().nullish() }).passthrough(),
42676
+ external_exports.object({ sessionUpdate: external_exports.literal("config_option_update"), configOptions: external_exports.array(AcpConfigOption) }).passthrough(),
42677
+ external_exports.object({ sessionUpdate: external_exports.literal("compaction_update"), compactionId: external_exports.string().optional(), status: external_exports.string().optional() }).passthrough()
42678
+ ]);
42679
+ AcpSessionNotification = external_exports.object({ sessionId: external_exports.string(), update: external_exports.unknown() }).passthrough();
42680
+ AcpStopReason = external_exports.enum(["end_turn", "max_tokens", "max_turn_requests", "refusal", "cancelled"]);
42681
+ AcpPromptResult = external_exports.object({
42682
+ // A string, not the enum: a future reason must not turn a finished turn into a parse failure.
42683
+ stopReason: external_exports.string(),
42684
+ usage: external_exports.object({ inputTokens: external_exports.number().optional(), outputTokens: external_exports.number().optional(), totalTokens: external_exports.number().optional() }).passthrough().nullish()
42685
+ }).passthrough();
42686
+ AcpPermissionOptionKind = external_exports.enum(["allow_once", "allow_always", "reject_once", "reject_always"]);
42687
+ AcpPermissionOption = external_exports.object({
42688
+ optionId: external_exports.string(),
42689
+ name: external_exports.string().optional(),
42690
+ // A string: an unknown kind renders as an opaque choice rather than failing the whole request.
42691
+ kind: external_exports.string().optional()
42692
+ }).passthrough();
42693
+ AcpRequestPermissionParams = external_exports.object({
42694
+ sessionId: external_exports.string(),
42695
+ toolCall: AcpToolCallUpdate,
42696
+ options: external_exports.array(AcpPermissionOption)
42697
+ }).passthrough();
42698
+ }
42699
+ });
42700
+
42701
+ // packages/server/src/backend/acp-rpc.ts
42702
+ import { spawn as spawn6 } from "node:child_process";
42703
+ import { StringDecoder as StringDecoder4 } from "node:string_decoder";
42704
+ var spawnAcpChild, AcpRequestError, AcpRemoteError, AcpConnectionClosed, AcpRequestTimeout, DEFAULT_TIMEOUT_MS, DEFAULT_MAX_LINE_BYTES, AcpConnection;
42705
+ var init_acp_rpc = __esm({
42706
+ "packages/server/src/backend/acp-rpc.ts"() {
42707
+ "use strict";
42708
+ init_acp_types();
42709
+ spawnAcpChild = ({ command, args, cwd, env }) => spawn6(command, args, { cwd, env, stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
42710
+ AcpRequestError = class extends Error {
42711
+ constructor(code, message, data) {
42712
+ super(message);
42713
+ this.code = code;
42714
+ this.data = data;
42715
+ }
42716
+ };
42717
+ AcpRemoteError = class extends Error {
42718
+ constructor(method, error) {
42719
+ super(`${method}: ${error.message} (${error.code})`);
42720
+ this.method = method;
42721
+ this.error = error;
42722
+ }
42723
+ };
42724
+ AcpConnectionClosed = class extends Error {
42725
+ constructor(method, reason) {
42726
+ super(`${method}: agent connection closed (${reason})`);
42727
+ this.method = method;
42728
+ this.reason = reason;
42729
+ }
42730
+ };
42731
+ AcpRequestTimeout = class extends Error {
42732
+ constructor(method, timeoutMs) {
42733
+ super(`${method}: no response from the agent within ${Math.round(timeoutMs / 1e3)}s`);
42734
+ this.method = method;
42735
+ this.timeoutMs = timeoutMs;
42736
+ }
42737
+ };
42738
+ DEFAULT_TIMEOUT_MS = 6e4;
42739
+ DEFAULT_MAX_LINE_BYTES = 8 * 1024 * 1024;
42740
+ AcpConnection = class {
42741
+ constructor(process2, options) {
42742
+ this.process = process2;
42743
+ this.options = options;
42744
+ const maxLine = options.maxLineBytes ?? DEFAULT_MAX_LINE_BYTES;
42745
+ this.exitPromise = new Promise((resolve12) => {
42746
+ process2.on("exit", (code, signal) => {
42747
+ this.settleClosed(`exit ${signal ?? code ?? "?"}`);
42748
+ resolve12({ code, signal });
42749
+ });
42750
+ process2.on("error", (err) => {
42751
+ this.settleClosed(err.message);
42752
+ resolve12({ code: null, signal: null });
42753
+ });
42754
+ });
42755
+ process2.stdout?.on("data", (chunk) => {
42756
+ this.buffer += typeof chunk === "string" ? chunk : this.decoder.write(chunk);
42757
+ let nl;
42758
+ while ((nl = this.buffer.indexOf("\n")) >= 0) {
42759
+ const line = this.buffer.slice(0, nl);
42760
+ this.buffer = this.buffer.slice(nl + 1);
42761
+ if (this.overflowing) {
42762
+ this.overflowing = false;
42763
+ continue;
42764
+ }
42765
+ this.handleLine(line);
42766
+ }
42767
+ if (this.buffer.length > maxLine) {
42768
+ options.onDiagnostic?.({ kind: "oversized-line", message: `dropped a stdout line longer than ${maxLine} bytes` });
42769
+ this.buffer = "";
42770
+ this.overflowing = true;
42771
+ }
42772
+ });
42773
+ process2.stderr?.on("data", (chunk) => {
42774
+ const text2 = String(chunk);
42775
+ for (const line of text2.split("\n")) {
42776
+ if (!line.trim()) continue;
42777
+ this.stderrTail.push(line.slice(0, 2e3));
42778
+ if (this.stderrTail.length > 40) this.stderrTail.shift();
42779
+ options.onDiagnostic?.({ kind: "stderr", message: line.slice(0, 2e3) });
42780
+ }
42781
+ });
42782
+ }
42783
+ nextId = 1;
42784
+ pending = /* @__PURE__ */ new Map();
42785
+ decoder = new StringDecoder4("utf8");
42786
+ buffer = "";
42787
+ overflowing = false;
42788
+ closedReason;
42789
+ exitPromise;
42790
+ stderrTail = [];
42791
+ /** The last lines the agent wrote to stderr — what a failed dispatch shows the operator. */
42792
+ get recentStderr() {
42793
+ return this.stderrTail;
42794
+ }
42795
+ get closed() {
42796
+ return this.closedReason !== void 0;
42797
+ }
42798
+ /** Resolves when the process has exited (or failed to start). */
42799
+ get exited() {
42800
+ return this.exitPromise;
42801
+ }
42802
+ request(method, params, opts = {}) {
42803
+ if (this.closedReason !== void 0) return Promise.reject(new AcpConnectionClosed(method, this.closedReason));
42804
+ const id = this.nextId++;
42805
+ const timeoutMs = opts.timeoutMs ?? this.options.requestTimeoutMs ?? DEFAULT_TIMEOUT_MS;
42806
+ return new Promise((resolve12, reject) => {
42807
+ const timer = timeoutMs > 0 ? setTimeout(() => {
42808
+ this.pending.delete(id);
42809
+ reject(new AcpRequestTimeout(method, timeoutMs));
42810
+ }, timeoutMs) : void 0;
42811
+ timer?.unref?.();
42812
+ this.pending.set(id, { method, resolve: resolve12, reject, timer });
42813
+ if (!this.write({ jsonrpc: "2.0", id, method, params })) {
42814
+ this.pending.delete(id);
42815
+ if (timer) clearTimeout(timer);
42816
+ reject(new AcpConnectionClosed(method, this.closedReason ?? "stdin closed"));
42817
+ }
42818
+ });
42819
+ }
42820
+ /** A prompt turn is long-lived by design (its response IS the end of the turn), so it is the one
42821
+ * request that must not time out; callers pass `timeoutMs: 0` through this alias for readability. */
42822
+ requestOpenEnded(method, params) {
42823
+ return this.request(method, params, { timeoutMs: 0 });
42824
+ }
42825
+ notify(method, params) {
42826
+ this.write({ jsonrpc: "2.0", method, params });
42827
+ }
42828
+ /** Close stdin (the protocol's teardown) and, if the agent has not exited after `graceMs`, kill it. */
42829
+ async close(graceMs = 3e3) {
42830
+ try {
42831
+ this.process.stdin?.end();
42832
+ } catch {
42833
+ }
42834
+ if (this.closedReason !== void 0) return;
42835
+ const exited = await Promise.race([this.exitPromise.then(() => true), new Promise((r) => setTimeout(() => r(false), graceMs).unref?.())]);
42836
+ if (!exited) {
42837
+ try {
42838
+ this.process.kill("SIGKILL");
42839
+ } catch {
42840
+ }
42841
+ }
42842
+ }
42843
+ write(frame) {
42844
+ if (this.closedReason !== void 0) return false;
42845
+ const stdin = this.process.stdin;
42846
+ if (!stdin || stdin.writableEnded || stdin.destroyed) return false;
42847
+ try {
42848
+ stdin.write(JSON.stringify(frame) + "\n");
42849
+ return true;
42850
+ } catch (err) {
42851
+ this.settleClosed(err.message);
42852
+ return false;
42853
+ }
42854
+ }
42855
+ settleClosed(reason) {
42856
+ if (this.closedReason !== void 0) return;
42857
+ this.closedReason = reason;
42858
+ for (const [id, p] of this.pending) {
42859
+ this.pending.delete(id);
42860
+ if (p.timer) clearTimeout(p.timer);
42861
+ p.reject(new AcpConnectionClosed(p.method, reason));
42862
+ }
42863
+ }
42864
+ handleLine(line) {
42865
+ if (!line.trim()) return;
42866
+ let raw2;
42867
+ try {
42868
+ raw2 = JSON.parse(line);
42869
+ } catch {
42870
+ this.options.onDiagnostic?.({ kind: "junk-line", message: line.slice(0, 500) });
42871
+ return;
42872
+ }
42873
+ const parsed = JsonRpcInbound.safeParse(raw2);
42874
+ if (!parsed.success) {
42875
+ this.options.onDiagnostic?.({ kind: "protocol", message: `unrecognized frame: ${line.slice(0, 500)}` });
42876
+ return;
42877
+ }
42878
+ const frame = parsed.data;
42879
+ if (typeof frame.method === "string") {
42880
+ if (frame.id !== void 0) this.handleRequest(frame.id, frame.method, frame.params);
42881
+ else this.options.onNotification(frame.method, frame.params);
42882
+ return;
42883
+ }
42884
+ const id = frame.id;
42885
+ const p = this.pending.get(id);
42886
+ if (!p) {
42887
+ this.options.onDiagnostic?.({ kind: "protocol", message: `response to unknown request id ${String(id)}` });
42888
+ return;
42889
+ }
42890
+ this.pending.delete(id);
42891
+ if (p.timer) clearTimeout(p.timer);
42892
+ if (frame.error !== void 0) p.reject(new AcpRemoteError(p.method, frame.error));
42893
+ else p.resolve(frame.result);
42894
+ }
42895
+ handleRequest(id, method, params) {
42896
+ void this.options.onRequest(method, params).then(
42897
+ (result) => {
42898
+ this.write({ jsonrpc: "2.0", id, result: result ?? {} });
42899
+ },
42900
+ (err) => {
42901
+ const e = err instanceof AcpRequestError ? { code: err.code, message: err.message, ...err.data !== void 0 ? { data: err.data } : {} } : { code: -32603, message: err instanceof Error ? err.message : String(err) };
42902
+ if (!(err instanceof AcpRequestError)) this.options.onDiagnostic?.({ kind: "unhandled-request", message: `${method}: ${e.message}` });
42903
+ this.write({ jsonrpc: "2.0", id, error: e });
42904
+ }
42905
+ );
42906
+ }
42907
+ };
42908
+ }
42909
+ });
42910
+
42911
+ // packages/server/src/backend/acp-bridge.ts
42912
+ function clip3(text2, max) {
42913
+ return text2.length > max ? `${text2.slice(0, max - 1)}\u2026` : text2;
42914
+ }
42915
+ function frizzMcpServer(mcp, slug) {
42916
+ if (!mcp) return [];
42917
+ return [{
42918
+ name: FRIZZ_MCP.name,
42919
+ command: process.execPath,
42920
+ args: [mcp.scriptPath],
42921
+ env: Object.entries(frizzMcpEnv({ ...mcp, slug })).map(([name, value]) => ({ name, value }))
42922
+ }];
42923
+ }
42924
+ function toolResultText3(tc) {
42925
+ const parts = [];
42926
+ for (const c of tc.content ?? []) {
42927
+ if (c.type === "content") {
42928
+ const t = contentText(c.content);
42929
+ if (t) parts.push(t);
42930
+ } else if (c.type === "diff") parts.push(`--- ${c.path}
42931
+ ${c.newText}`);
42932
+ }
42933
+ if (parts.length) return parts.join("\n");
42934
+ const raw2 = tc.rawOutput;
42935
+ if (raw2 && typeof raw2 === "object" && typeof raw2.output === "string") return raw2.output;
42936
+ if (typeof tc.rawOutput === "string") return tc.rawOutput;
42937
+ return "";
42938
+ }
42939
+ function buildAcpPermissionInteraction(params, owner, agent) {
42940
+ const tc = params.toolCall;
42941
+ const input = tc.rawInput && typeof tc.rawInput === "object" ? tc.rawInput : void 0;
42942
+ const command = typeof input?.command === "string" ? input.command : "";
42943
+ const isCommand = tc.kind === "execute" && command !== "";
42944
+ const isFile = (tc.kind === "edit" || tc.kind === "delete" || tc.kind === "move") && (tc.locations?.[0]?.path || typeof input?.filePath === "string" || typeof input?.path === "string");
42945
+ const payloadKind = isCommand ? "command-approval" : isFile ? "file-approval" : "permission-approval";
42946
+ const optionByDecision = /* @__PURE__ */ new Map();
42947
+ const allowedDecisions = [];
42948
+ const add = (id, semantic, label, optionId, description) => {
42949
+ if (optionByDecision.has(id)) return;
42950
+ optionByDecision.set(id, optionId);
42951
+ allowedDecisions.push({ id, semantic, label: clip3(label, 150), ...description ? { description: clip3(description, 500) } : {} });
42952
+ };
42953
+ for (const opt of params.options) {
42954
+ const label = opt.name ?? opt.kind ?? opt.optionId;
42955
+ const once = payloadKind === "permission-approval" ? "grant-turn" : "accept";
42956
+ const always = payloadKind === "permission-approval" ? "grant-session" : "acceptForSession";
42957
+ const deny = payloadKind === "permission-approval" ? "deny" : "decline";
42958
+ if (opt.kind === "allow_once") add(once, "approve", label, opt.optionId, "Allow this once.");
42959
+ else if (opt.kind === "allow_always") add(always, "approve", label, opt.optionId, "Allow for the rest of this session.");
42960
+ else if (opt.kind === "reject_once" || opt.kind === "reject_always") add(deny, "deny", label, opt.optionId, "Block this tool call.");
42961
+ }
42962
+ if (!allowedDecisions.some((d) => d.semantic === "deny")) return null;
42963
+ const title = clip3(tc.title ?? (isCommand ? "Run a command?" : "Approve a tool call?"), 150);
42964
+ const previewLines = [command || "", ...Object.entries(input ?? {}).filter(([k, v]) => k !== "command" && v !== "" && v !== null).map(([k, v]) => `${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`)].filter(Boolean);
42965
+ const preview = clip3(redactCredentialSyntax(previewLines.join("\n\n")), 6e3);
42966
+ const pathLabel = clip3(tc.locations?.[0]?.path ?? (typeof input?.filePath === "string" ? input.filePath : typeof input?.path === "string" ? input.path : ""), 1e3);
42967
+ const base = {
42968
+ protocolVersion: INTERACTION_PROTOCOL_VERSION,
42969
+ contentFormat: "plain-text",
42970
+ provider: { kind: "acp", name: agent.label },
42971
+ source: { kind: "tool", id: clip3(tc.toolCallId, 500), label: clip3(tc.title ?? tc.kind ?? "tool", 150) },
42972
+ owner: { projectId: owner.projectId, threadSlug: owner.threadSlug, sessionId: owner.sessionId, turnId: clip3(tc.toolCallId, 500), itemId: clip3(tc.toolCallId, 500), sessionEpoch: 0, capabilityRevision: 0 },
42973
+ providerRequestId: clip3(tc.toolCallId, 500),
42974
+ allowedDecisions,
42975
+ expiresAt: null
42976
+ };
42977
+ const payload = payloadKind === "command-approval" ? { kind: "command-approval", title, command: { summary: clip3(command.replace(/\s+/g, " "), 150), preview, redacted: true, workingDirectoryLabel: clip3(owner.cwd, 1e3) } } : payloadKind === "file-approval" ? { kind: "file-approval", title, operation: tc.kind === "delete" ? "delete" : tc.kind === "move" ? "move" : "write", pathLabel: pathLabel || "(unknown path)", ...preview ? { diffPreview: preview } : {} } : { kind: "permission-approval", title, permission: clip3(tc.kind ?? "tool", 250), ...preview ? { preview } : {}, workingDirectoryLabel: clip3(owner.cwd, 1e3) };
42978
+ const parsed = InteractionRequest.safeParse({ ...base, payload });
42979
+ return parsed.success ? { request: parsed.data, optionByDecision } : null;
42980
+ }
42981
+ function createAcpBridge(options) {
42982
+ return new AcpBridge(options);
42983
+ }
42984
+ var AUTH_HINT, ACP_MODEL_CACHE_MS, AcpBridge;
42985
+ var init_acp_bridge = __esm({
42986
+ "packages/server/src/backend/acp-bridge.ts"() {
42987
+ "use strict";
42988
+ init_src();
42989
+ init_credential_redaction();
42990
+ init_logging();
42991
+ init_acp_agents();
42992
+ init_acp_rpc();
42993
+ init_acp_transcript();
42994
+ init_acp_types();
42995
+ init_types2();
42996
+ init_worker_env();
42997
+ AUTH_HINT = "The agent needs a login first. ";
42998
+ ACP_MODEL_CACHE_MS = 10 * 6e4;
42999
+ AcpBridge = class {
43000
+ constructor(options) {
43001
+ this.options = options;
43002
+ this.unsubscribe = options.interactions?.subscribe((change) => {
43003
+ for (const live of this.sessions.values()) {
43004
+ const pending = live.pendingPerms.get(change.interactionId);
43005
+ if (!pending || change.lifecycle === "pending") continue;
43006
+ live.pendingPerms.delete(change.interactionId);
43007
+ const record = options.interactions.get(pending.scope, change.interactionId);
43008
+ const decisionId = change.lifecycle === "resolved" ? record?.resolution?.decisionId : void 0;
43009
+ const optionId = decisionId ? pending.optionByDecision.get(decisionId) : void 0;
43010
+ pending.resolve(optionId ? { outcome: { outcome: "selected", optionId } } : { outcome: { outcome: "cancelled" } });
43011
+ }
43012
+ });
43013
+ }
43014
+ sessions = /* @__PURE__ */ new Map();
43015
+ unsubscribe;
43016
+ closed = false;
43017
+ now() {
43018
+ return (this.options.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
43019
+ }
43020
+ diagnostic(slug, kind, message) {
43021
+ this.options.onDiagnostic?.({ threadSlug: slug, kind, message });
43022
+ if (kind !== "stderr") log.debug("acp", `[${slug}] ${kind}: ${message}`);
43023
+ }
43024
+ /** The agents this bridge can launch, with the operator's own entries merged in. */
43025
+ agents() {
43026
+ return acpAgentSpecs(this.options.customAgents?.());
43027
+ }
43028
+ resolveAgent(agentId) {
43029
+ const agent = resolveAcpAgent(agentId, this.options.customAgents?.(), this.options.env ?? process.env);
43030
+ if (!agent) throw new Error(`Unknown ACP agent "${agentId}". Add it under settings.acpAgents or pick one of: ${this.agents().map((a) => a.id).join(", ")}.`);
43031
+ if (!agent.bin) throw new Error(`The ACP agent "${agent.label}" is not installed: \`${agent.command}\` is not on PATH. Install it, or point settings.acpAgents at its executable.`);
43032
+ return agent;
43033
+ }
43034
+ // ---- session lifecycle ----------------------------------------------------------------------
43035
+ /** Spawn the agent, initialize, and open (or load) a session. Throws an actionable error on any
43036
+ * failure and leaves nothing behind. */
43037
+ async open(input) {
43038
+ const { threadSlug: slug, sessionId, cwd } = input;
43039
+ const agent = this.resolveAgent(input.agentId);
43040
+ const env = inheritWorkerEnvironment(this.options.env ?? process.env);
43041
+ const spawn8 = this.options.spawn ?? spawnAcpChild;
43042
+ const proc = spawn8({ command: agent.bin, args: agent.args, cwd, env });
43043
+ const writer = new AcpTranscriptWriter(acpTranscriptPath(this.options.stateDir, sessionId));
43044
+ let live;
43045
+ const conn = new AcpConnection(proc, {
43046
+ onRequest: (method, params) => live ? this.handleRequest(live, method, params) : Promise.reject(new AcpRequestError(ACP_ERROR_INTERNAL, "session not ready")),
43047
+ onNotification: (method, params) => {
43048
+ if (live && method === "session/update") this.handleNotification(live, params);
43049
+ },
43050
+ onDiagnostic: (d) => this.diagnostic(slug, d.kind, d.message),
43051
+ requestTimeoutMs: this.options.initializeTimeoutMs ?? 6e4
43052
+ });
43053
+ const fail = async (err) => {
43054
+ await conn.close(500);
43055
+ const stderr = conn.recentStderr.slice(-3).join(" | ");
43056
+ const e = err instanceof Error ? err : new Error(String(err));
43057
+ throw new Error(`${e.message}${stderr ? ` (agent stderr: ${clip3(stderr, 400)})` : ""}`);
43058
+ };
43059
+ let init;
43060
+ try {
43061
+ init = AcpInitializeResult.parse(await conn.request("initialize", {
43062
+ protocolVersion: ACP_PROTOCOL_VERSION,
43063
+ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
43064
+ clientInfo: { name: "frizz", version: "0" }
43065
+ }));
43066
+ } catch (err) {
43067
+ return fail(err instanceof Error ? new Error(`${agent.label} did not complete the ACP handshake: ${err.message}`) : err);
43068
+ }
43069
+ const mcpServers = frizzMcpServer(this.options.frizzMcp, slug);
43070
+ live = { slug, sessionId, cwd, agent, conn, init, acpSessionId: "", writer, loading: false, queue: [], pendingPerms: /* @__PURE__ */ new Map(), tools: /* @__PURE__ */ new Map(), exited: false };
43071
+ void conn.exited.then(() => {
43072
+ if (live) this.onExit(live);
43073
+ });
43074
+ const authError = (err) => {
43075
+ const hint = (init.authMethods ?? []).map((m) => m.description ?? m.name ?? m.id).filter(Boolean).join("; ");
43076
+ return new Error(`${AUTH_HINT}${agent.label} refused to open a session: ${err.error.message}.${hint ? ` ${hint}.` : ""}`);
43077
+ };
43078
+ let resumed = "fresh";
43079
+ if (input.acpSessionId && init.agentCapabilities?.loadSession) {
43080
+ live.loading = true;
43081
+ try {
43082
+ const loaded = AcpLoadSessionResult.parse(await conn.request("session/load", { sessionId: input.acpSessionId, cwd, mcpServers }));
43083
+ live.acpSessionId = input.acpSessionId;
43084
+ this.adoptModel(live, loaded.configOptions);
43085
+ resumed = "loaded";
43086
+ } catch (err) {
43087
+ if (err instanceof AcpRemoteError && err.error.code === ACP_ERROR_AUTH_REQUIRED) return fail(authError(err));
43088
+ this.diagnostic(slug, "protocol", `session/load ${input.acpSessionId} failed (${err.message}); starting a fresh session`);
43089
+ } finally {
43090
+ live.loading = false;
43091
+ }
43092
+ }
43093
+ if (!live.acpSessionId) {
43094
+ try {
43095
+ const created = AcpNewSessionResult.parse(await conn.request("session/new", { cwd, mcpServers }));
43096
+ live.acpSessionId = created.sessionId;
43097
+ this.adoptModel(live, created.configOptions);
43098
+ } catch (err) {
43099
+ if (err instanceof AcpRemoteError && err.error.code === ACP_ERROR_AUTH_REQUIRED) return fail(authError(err));
43100
+ return fail(err instanceof Error ? new Error(`${agent.label} could not open a session: ${err.message}`) : err);
43101
+ }
43102
+ }
43103
+ const modelProblem = await this.applyModel(live, input.modelId);
43104
+ if (resumed === "fresh") {
43105
+ writer.append({ kind: "acp-session", at: this.now(), agent: { id: agent.id, ...init.agentInfo?.name ? { name: init.agentInfo.name } : {}, ...init.agentInfo?.version ? { version: init.agentInfo.version } : {} }, acpSessionId: live.acpSessionId, cwd, ...live.model ? { model: live.model } : {} });
43106
+ if (input.acpSessionId) writer.append({ kind: "acp-note", at: this.now(), text: `Resumed with a fresh ${agent.label} session: the previous one (${input.acpSessionId}) could not be loaded, so the agent no longer has the earlier conversation.` });
43107
+ }
43108
+ if (modelProblem) writer.append({ kind: "acp-note", at: this.now(), text: modelProblem });
43109
+ this.sessions.set(sessionId, live);
43110
+ return { live, resumed };
43111
+ }
43112
+ /** The agent's model option (`category: "model"`, else the one literally named `model`). */
43113
+ modelOption(configOptions) {
43114
+ return configOptions?.find((o) => o.category === "model") ?? configOptions?.find((o) => o.id === "model");
43115
+ }
43116
+ adoptModel(live, configOptions) {
43117
+ const option = this.modelOption(configOptions);
43118
+ if (!option) return;
43119
+ live.modelOptionId = option.id;
43120
+ if (typeof option.currentValue === "string") live.model = option.currentValue;
43121
+ }
43122
+ /** Ask the agent for the thread's model when it is not already the one in effect. Returns the note
43123
+ * to write when the agent refused — the thread runs on, on the agent's own model, and says so. */
43124
+ async applyModel(live, modelId) {
43125
+ if (!modelId || modelId === live.model) return void 0;
43126
+ try {
43127
+ const res = AcpSetConfigOptionResult.parse(await live.conn.request("session/set_config_option", { sessionId: live.acpSessionId, configId: live.modelOptionId ?? "model", value: modelId }));
43128
+ this.adoptModel(live, res.configOptions);
43129
+ if (live.model !== modelId && res.configOptions?.length) return `${live.agent.label} did not switch to ${modelId}; the session runs on ${live.model ?? "its own default model"}.`;
43130
+ if (!res.configOptions?.length) live.model = modelId;
43131
+ return void 0;
43132
+ } catch (err) {
43133
+ const message = err instanceof AcpRemoteError ? err.error.message : err.message;
43134
+ this.diagnostic(live.slug, "protocol", `session/set_config_option ${modelId} failed: ${message}`);
43135
+ return `${live.agent.label} could not switch to ${modelId} (${clip3(message, 200)}); the session runs on ${live.model ?? "its own default model"}.`;
43136
+ }
43137
+ }
43138
+ // ---- the model catalogue an agent advertises -----------------------------------------------------
43139
+ modelProbes = /* @__PURE__ */ new Map();
43140
+ modelCache = /* @__PURE__ */ new Map();
43141
+ /** The models an agent offers, read by opening a throwaway session (there is no other way to ask:
43142
+ * ACP advertises models only in a session's config options). Cached per agent for ten minutes; a
43143
+ * probe that cannot open a session (not installed, not logged in) yields an empty list with the
43144
+ * reason, never a throw, so the composer degrades to the agent's own default. */
43145
+ agentModels(agentId, cwd, opts = {}) {
43146
+ const cached2 = this.modelCache.get(agentId);
43147
+ if (cached2 && !opts.refresh && Date.now() - Date.parse(cached2.probedAt) < ACP_MODEL_CACHE_MS) return Promise.resolve(cached2);
43148
+ const inflight = this.modelProbes.get(agentId);
43149
+ if (inflight) return inflight;
43150
+ const probe = this.probeModels(agentId, cwd).then((result) => {
43151
+ if (!result.error) this.modelCache.set(agentId, result);
43152
+ return result;
43153
+ }).finally(() => this.modelProbes.delete(agentId));
43154
+ this.modelProbes.set(agentId, probe);
43155
+ return probe;
43156
+ }
43157
+ async probeModels(agentId, cwd) {
43158
+ const probedAt = this.now();
43159
+ const empty = (error) => ({ agentId, models: [], error, probedAt });
43160
+ let agent;
43161
+ try {
43162
+ agent = this.resolveAgent(agentId);
43163
+ } catch (err) {
43164
+ return empty(err.message);
43165
+ }
43166
+ const env = inheritWorkerEnvironment(this.options.env ?? process.env);
43167
+ const spawn8 = this.options.spawn ?? spawnAcpChild;
43168
+ let proc;
43169
+ try {
43170
+ proc = spawn8({ command: agent.bin, args: agent.args, cwd, env });
43171
+ } catch (err) {
43172
+ return empty(`${agent.label} could not start: ${err.message}`);
43173
+ }
43174
+ const conn = new AcpConnection(proc, {
43175
+ onRequest: () => Promise.reject(new AcpRequestError(ACP_ERROR_METHOD_NOT_FOUND, "probe session takes no requests")),
43176
+ onNotification: () => {
43177
+ },
43178
+ onDiagnostic: (d) => this.diagnostic(`probe:${agentId}`, d.kind, d.message),
43179
+ requestTimeoutMs: this.options.initializeTimeoutMs ?? 6e4
43180
+ });
43181
+ try {
43182
+ const init = AcpInitializeResult.parse(await conn.request("initialize", {
43183
+ protocolVersion: ACP_PROTOCOL_VERSION,
43184
+ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
43185
+ clientInfo: { name: "frizz", version: "0" }
43186
+ }));
43187
+ const created = AcpNewSessionResult.parse(await conn.request("session/new", { cwd, mcpServers: [] }));
43188
+ const option = this.modelOption(created.configOptions);
43189
+ const current = typeof option?.currentValue === "string" ? option.currentValue : void 0;
43190
+ void init;
43191
+ return {
43192
+ agentId,
43193
+ models: (option?.options ?? []).map((o) => ({ id: o.value, name: o.name ?? o.value })),
43194
+ ...current ? { current } : {},
43195
+ probedAt
43196
+ };
43197
+ } catch (err) {
43198
+ const message = err instanceof AcpRemoteError && err.error.code === ACP_ERROR_AUTH_REQUIRED ? `${AUTH_HINT}${agent.label} refused to open a session: ${err.error.message}` : `${agent.label}: ${err.message}`;
43199
+ return empty(clip3(message, 400));
43200
+ } finally {
43201
+ await conn.close(500);
43202
+ }
43203
+ }
43204
+ onExit(live) {
43205
+ if (live.exited) return;
43206
+ live.exited = true;
43207
+ this.retirePermissions(live, "provider-cancelled");
43208
+ if (live.turn) {
43209
+ }
43210
+ if (this.sessions.get(live.sessionId) === live) this.sessions.delete(live.sessionId);
43211
+ this.options.onStatusChange?.();
43212
+ }
43213
+ // ---- public API --------------------------------------------------------------------------------
43214
+ async spawnDispatch(input) {
43215
+ if (this.closed) throw new Error("ACP bridge is closed");
43216
+ const { live } = await this.open(input);
43217
+ void this.runTurn(live, input.prompt, input.userText, void 0);
43218
+ return this.info(live);
43219
+ }
43220
+ /** Deliver a follow-up: to the live session, to a re-opened one (load or fresh), or into the queue
43221
+ * when a turn is running. Returns the (possibly new) ACP session id so the caller can re-pin it. */
43222
+ async followUp(input) {
43223
+ if (this.closed) throw new Error("ACP bridge is closed");
43224
+ let live = this.sessions.get(input.sessionId);
43225
+ let resumed = "live";
43226
+ if (!live || live.exited || live.conn.closed) {
43227
+ const opened = await this.open(input);
43228
+ live = opened.live;
43229
+ resumed = opened.resumed;
43230
+ }
43231
+ if (live.turn) {
43232
+ live.queue.push({ text: input.text, ...input.deliveryId ? { deliveryId: input.deliveryId } : {} });
43233
+ return { ...this.info(live), state: "queued", resumed };
43234
+ }
43235
+ void this.runTurn(live, input.text, input.text, input.deliveryId);
43236
+ return { ...this.info(live), state: "delivered", resumed };
43237
+ }
43238
+ /** `session/cancel`, then wait (bounded) for the prompt to return. `interrupted: false` when no turn
43239
+ * was running. The queue is NOT discarded — a follow-up typed during the turn still goes out. */
43240
+ async interruptTurn(threadSlug, sessionId) {
43241
+ const live = this.sessions.get(sessionId);
43242
+ if (!live || live.slug !== threadSlug || !live.turn) return { interrupted: false };
43243
+ live.turn.cancelRequested = true;
43244
+ live.conn.notify("session/cancel", { sessionId: live.acpSessionId });
43245
+ const settled = await Promise.race([live.turn.done.then(() => true), new Promise((r) => setTimeout(() => r(false), 15e3).unref?.())]);
43246
+ if (!settled) {
43247
+ await live.conn.close(1e3);
43248
+ return { interrupted: true };
43249
+ }
43250
+ return { interrupted: true };
43251
+ }
43252
+ releaseSession(threadSlug, sessionId, reason) {
43253
+ const live = this.sessions.get(sessionId);
43254
+ if (!live || live.slug !== threadSlug) return;
43255
+ this.sessions.delete(sessionId);
43256
+ live.queue.length = 0;
43257
+ this.retirePermissions(live, "provider-cancelled");
43258
+ if (live.turn) live.conn.notify("session/cancel", { sessionId: live.acpSessionId });
43259
+ void live.conn.close(reason === "shutdown" ? 1e3 : 3e3);
43260
+ }
43261
+ /** Switch a LIVE session's model (`session/set_config_option`); `applied: false` when the session is
43262
+ * not open, in which case the caller's stored slug reaches the agent on the next open. A refusal is
43263
+ * written to the transcript as a note, exactly as on open, and still counts as applied: the agent
43264
+ * answered, and the note says what it runs on. */
43265
+ async setModel(threadSlug, sessionId, modelId) {
43266
+ const live = this.sessions.get(sessionId);
43267
+ if (!live || live.slug !== threadSlug || live.exited || live.conn.closed) return { applied: false };
43268
+ const problem = await this.applyModel(live, modelId);
43269
+ if (problem) live.writer.append({ kind: "acp-note", at: this.now(), text: problem });
43270
+ return { applied: true, ...live.model ? { model: live.model } : {} };
43271
+ }
43272
+ turnLiveness(threadSlug, sessionId) {
43273
+ const live = this.sessions.get(sessionId);
43274
+ if (!live || live.slug !== threadSlug) return void 0;
43275
+ return { sessionLive: !live.exited && !live.conn.closed, turnActive: live.turn !== void 0, queued: live.queue.length };
43276
+ }
43277
+ session(threadSlug, sessionId) {
43278
+ const live = this.sessions.get(sessionId);
43279
+ return live && live.slug === threadSlug ? this.info(live) : void 0;
43280
+ }
43281
+ ownsInteraction(scope, interactionId) {
43282
+ const live = this.sessions.get(scope.sessionId);
43283
+ return live !== void 0 && live.slug === scope.threadSlug && live.pendingPerms.has(interactionId);
43284
+ }
43285
+ async shutdown() {
43286
+ this.closed = true;
43287
+ this.unsubscribe?.();
43288
+ const all = [...this.sessions.values()];
43289
+ this.sessions.clear();
43290
+ await Promise.all(all.map(async (live) => {
43291
+ this.retirePermissions(live, "provider-cancelled");
43292
+ await live.conn.close(1e3);
43293
+ }));
43294
+ }
43295
+ info(live) {
43296
+ return {
43297
+ acpSessionId: live.acpSessionId,
43298
+ agent: { id: live.agent.id, ...live.init.agentInfo?.name ? { name: live.init.agentInfo.name } : {}, ...live.init.agentInfo?.version ? { version: live.init.agentInfo.version } : {} },
43299
+ ...live.model ? { model: live.model } : {}
43300
+ };
43301
+ }
43302
+ // ---- the turn ---------------------------------------------------------------------------------
43303
+ async runTurn(live, sendText, recordText, deliveryId) {
43304
+ let finish;
43305
+ const done = new Promise((r) => {
43306
+ finish = r;
43307
+ });
43308
+ const turn = { startedAt: this.now(), ...deliveryId ? { deliveryId } : {}, text: "", messageText: "", thought: "", cancelRequested: false, done, finish };
43309
+ live.turn = turn;
43310
+ live.writer.append({ kind: "user-message", at: turn.startedAt, text: recordText, synthetic: false });
43311
+ live.writer.append({ kind: "turn-start", at: turn.startedAt });
43312
+ this.options.onStatusChange?.();
43313
+ try {
43314
+ const raw2 = await live.conn.requestOpenEnded("session/prompt", { sessionId: live.acpSessionId, prompt: [{ type: "text", text: sendText }] });
43315
+ const result = AcpPromptResult.parse(raw2);
43316
+ this.flushThought(live);
43317
+ const finalText = this.flushText(live, true);
43318
+ live.writer.append({ kind: "turn-end", at: this.now(), ...finalText !== void 0 ? { finalText } : {}, successful: result.stopReason !== "refusal" && result.stopReason !== "cancelled" });
43319
+ if (result.stopReason === "cancelled") live.writer.append({ kind: "acp-note", at: this.now(), text: "Turn cancelled." });
43320
+ } catch (err) {
43321
+ this.flushThought(live);
43322
+ const finalText = this.flushText(live, true);
43323
+ const at = this.now();
43324
+ const abortedByUs = turn.cancelRequested && err instanceof AcpRemoteError && err.error.code === ACP_ERROR_INTERNAL;
43325
+ if (abortedByUs) {
43326
+ live.writer.append({ kind: "turn-end", at, ...finalText !== void 0 ? { finalText } : {}, successful: false });
43327
+ live.writer.append({ kind: "acp-note", at, text: "Turn cancelled." });
43328
+ } else {
43329
+ const message = err instanceof AcpRemoteError ? err.error.code === ACP_ERROR_AUTH_REQUIRED ? `${AUTH_HINT}${err.error.message}` : err.error.message : err.message;
43330
+ live.writer.append({ kind: "provider-error", at, error: { message: clip3(message, 2e3), ...err instanceof AcpRemoteError ? { code: String(err.error.code) } : {} } });
43331
+ live.writer.append({ kind: "turn-end", at, ...finalText !== void 0 ? { finalText } : {}, successful: false });
43332
+ }
43333
+ } finally {
43334
+ this.retirePermissions(live, "turn-ended");
43335
+ if (live.turn === turn) live.turn = void 0;
43336
+ turn.finish();
43337
+ this.options.onStatusChange?.();
43338
+ const next = live.queue.shift();
43339
+ if (next && !live.exited && !live.conn.closed) void this.runTurn(live, next.text, next.text, next.deliveryId);
43340
+ }
43341
+ }
43342
+ /** Write the buffered text as one record. Returns, for a FINAL flush, the whole text of the agent's
43343
+ * last message (every partial flush included) — what the turn-end carries as `finalText`, so the
43344
+ * fold's preview and fence parsing see the complete message, never the last chunk. */
43345
+ flushText(live, final) {
43346
+ const turn = live.turn;
43347
+ if (!turn) return void 0;
43348
+ if (turn.flushTimer) {
43349
+ clearTimeout(turn.flushTimer);
43350
+ turn.flushTimer = void 0;
43351
+ }
43352
+ if (turn.text) {
43353
+ const text2 = turn.text;
43354
+ turn.text = "";
43355
+ live.writer.append({ kind: "assistant-text", at: this.now(), text: text2, final, ...turn.messageId ? { messageId: turn.messageId } : {} });
43356
+ }
43357
+ return final && turn.messageText ? turn.messageText : void 0;
43358
+ }
43359
+ flushThought(live) {
43360
+ const turn = live.turn;
43361
+ if (!turn?.thought.trim()) {
43362
+ if (turn) turn.thought = "";
43363
+ return;
43364
+ }
43365
+ live.writer.append({ kind: "reasoning", at: this.now(), text: turn.thought });
43366
+ turn.thought = "";
43367
+ }
43368
+ scheduleFlush(live) {
43369
+ const turn = live.turn;
43370
+ if (!turn || turn.flushTimer) return;
43371
+ turn.flushTimer = setTimeout(() => {
43372
+ turn.flushTimer = void 0;
43373
+ this.flushText(live, false);
43374
+ }, this.options.flushMs ?? 1e3);
43375
+ turn.flushTimer.unref?.();
43376
+ }
43377
+ handleNotification(live, params) {
43378
+ if (live.loading) return;
43379
+ const parsed = AcpSessionNotification.safeParse(params);
43380
+ if (!parsed.success) {
43381
+ this.diagnostic(live.slug, "protocol", "malformed session/update");
43382
+ return;
43383
+ }
43384
+ const update = parseSessionUpdate(parsed.data.update);
43385
+ const turn = live.turn;
43386
+ switch (update.sessionUpdate) {
43387
+ case "agent_thought_chunk": {
43388
+ if (!turn) return;
43389
+ const t = contentText(update.content);
43390
+ if (t) turn.thought += t;
43391
+ return;
43392
+ }
43393
+ case "agent_message_chunk": {
43394
+ if (!turn) return;
43395
+ this.flushThought(live);
43396
+ const t = contentText(update.content);
43397
+ if (!t) return;
43398
+ if (update.messageId && turn.messageId && update.messageId !== turn.messageId) {
43399
+ this.flushText(live, false);
43400
+ turn.messageText = "";
43401
+ }
43402
+ if (update.messageId) turn.messageId = update.messageId;
43403
+ turn.text += t;
43404
+ turn.messageText += t;
43405
+ this.scheduleFlush(live);
43406
+ return;
43407
+ }
43408
+ case "tool_call": {
43409
+ this.flushThought(live);
43410
+ this.flushText(live, false);
43411
+ if (turn) turn.messageText = "";
43412
+ const meta = { ...update.kind ? { kind: update.kind } : {}, ...update.title ? { title: update.title } : {}, locations: (update.locations ?? []).map((l) => l.path), written: true };
43413
+ live.tools.set(update.toolCallId, meta);
43414
+ live.writer.append({ kind: "tool-call", at: this.now(), id: update.toolCallId, name: update.title ?? update.kind ?? "tool", input: update.rawInput ?? {}, acp: { ...meta.kind ? { kind: meta.kind } : {}, ...meta.title ? { title: meta.title } : {}, locations: meta.locations ?? [] } });
43415
+ if (update.status === "completed" || update.status === "failed") this.recordToolResult(live, update);
43416
+ return;
43417
+ }
43418
+ case "tool_call_update": {
43419
+ let meta = live.tools.get(update.toolCallId);
43420
+ if (!meta) {
43421
+ meta = { written: false };
43422
+ live.tools.set(update.toolCallId, meta);
43423
+ }
43424
+ if (update.kind) meta.kind = update.kind;
43425
+ if (update.title) meta.title = update.title;
43426
+ if (update.locations?.length) meta.locations = update.locations.map((l) => l.path);
43427
+ if (!meta.written) {
43428
+ this.flushThought(live);
43429
+ this.flushText(live, false);
43430
+ meta.written = true;
43431
+ live.writer.append({ kind: "tool-call", at: this.now(), id: update.toolCallId, name: meta.title ?? meta.kind ?? "tool", input: update.rawInput ?? {}, acp: { ...meta.kind ? { kind: meta.kind } : {}, ...meta.title ? { title: meta.title } : {}, locations: meta.locations ?? [] } });
43432
+ }
43433
+ if (update.status === "completed" || update.status === "failed") this.recordToolResult(live, update);
43434
+ return;
43435
+ }
43436
+ case "usage_update":
43437
+ this.flushText(live, false);
43438
+ if (update.used > 0) live.writer.append({ kind: "context-usage", at: this.now(), tokens: update.used, window: update.size });
43439
+ return;
43440
+ case "session_info_update":
43441
+ if (update.title) live.writer.append({ kind: "title", title: clip3(update.title, 200) });
43442
+ return;
43443
+ case "config_option_update":
43444
+ this.adoptModel(live, update.configOptions);
43445
+ return;
43446
+ case "plan":
43447
+ case "user_message_chunk":
43448
+ case "compaction_update":
43449
+ return;
43450
+ case "unknown":
43451
+ this.diagnostic(live.slug, "update", `unhandled session/update ${String(update.raw?.sessionUpdate)}`);
43452
+ return;
43453
+ }
43454
+ }
43455
+ recordToolResult(live, tc) {
43456
+ const meta = live.tools.get(tc.toolCallId);
43457
+ if (meta?.status === "completed" || meta?.status === "failed") return;
43458
+ if (meta) meta.status = tc.status;
43459
+ const input = tc.rawInput && typeof tc.rawInput === "object" ? tc.rawInput : void 0;
43460
+ live.writer.append({ kind: "tool-result", at: this.now(), id: tc.toolCallId, text: clip3(toolResultText3(tc), 2e4), acp: { ...tc.status ? { kind: tc.status } : {}, ...meta?.locations?.length ? { locations: meta.locations } : {}, ...input && Object.keys(input).length ? { input } : {} } });
43461
+ }
43462
+ // ---- the agent asking us ------------------------------------------------------------------------
43463
+ async handleRequest(live, method, params) {
43464
+ if (method === "session/request_permission") return this.handlePermission(live, params);
43465
+ throw new AcpRequestError(ACP_ERROR_METHOD_NOT_FOUND, `frizz does not implement ${method}`);
43466
+ }
43467
+ handlePermission(live, params) {
43468
+ const parsed = AcpRequestPermissionParams.safeParse(params);
43469
+ const cancelled = { outcome: { outcome: "cancelled" } };
43470
+ if (!parsed.success) return Promise.resolve(cancelled);
43471
+ const store = this.options.interactions;
43472
+ const reject = parsed.data.options.find((o) => o.kind === "reject_once") ?? parsed.data.options.find((o) => o.kind === "reject_always");
43473
+ const failClosed = reject ? { outcome: { outcome: "selected", optionId: reject.optionId } } : cancelled;
43474
+ if (!store) {
43475
+ this.diagnostic(live.slug, "permission", "no interaction store; denying the request");
43476
+ return Promise.resolve(failClosed);
43477
+ }
43478
+ const built = buildAcpPermissionInteraction(parsed.data, { projectId: this.options.projectId, threadSlug: live.slug, sessionId: live.sessionId, cwd: live.cwd }, { id: live.agent.id, label: live.agent.label });
43479
+ if (!built) {
43480
+ this.diagnostic(live.slug, "permission", "request could not be represented as a card; denying");
43481
+ return Promise.resolve(failClosed);
43482
+ }
43483
+ const scope = { projectId: this.options.projectId, threadSlug: live.slug, sessionId: live.sessionId };
43484
+ let id;
43485
+ try {
43486
+ id = store.create(built.request).interaction.id;
43487
+ } catch (err) {
43488
+ this.diagnostic(live.slug, "permission", `could not record the approval: ${err.message}`);
43489
+ return Promise.resolve(failClosed);
43490
+ }
43491
+ return new Promise((resolve12) => {
43492
+ live.pendingPerms.set(id, { resolve: resolve12, optionByDecision: built.optionByDecision, scope });
43493
+ this.options.onStatusChange?.();
43494
+ });
43495
+ }
43496
+ /** Cancel every open card for a session that can no longer answer one, answering the agent
43497
+ * `cancelled` for each. Hygiene, so it never throws. */
43498
+ retirePermissions(live, reason) {
43499
+ if (!live.pendingPerms.size) return;
43500
+ const store = this.options.interactions;
43501
+ let swept = [];
43502
+ try {
43503
+ swept = store?.cancelForSession(live.slug, live.sessionId, reason) ?? [];
43504
+ } catch {
43505
+ }
43506
+ for (const record of swept) live.pendingPerms.get(record.id)?.resolve({ outcome: { outcome: "cancelled" } });
43507
+ for (const [id, pending] of live.pendingPerms) {
43508
+ pending.resolve({ outcome: { outcome: "cancelled" } });
43509
+ live.pendingPerms.delete(id);
43510
+ }
43511
+ }
43512
+ };
43513
+ }
43514
+ });
43515
+
41980
43516
  // packages/server/src/login-utility.ts
41981
43517
  import { randomBytes } from "node:crypto";
41982
43518
  import pty from "node-pty";
@@ -42154,27 +43690,27 @@ var init_login_utility = __esm({
42154
43690
  });
42155
43691
 
42156
43692
  // packages/server/src/backend/codex-app-server-diagnostics.ts
42157
- import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync14, renameSync as renameSync8, statSync as statSync15 } from "node:fs";
42158
- import { join as join33 } from "node:path";
43693
+ import { appendFileSync as appendFileSync4, mkdirSync as mkdirSync15, renameSync as renameSync8, statSync as statSync16 } from "node:fs";
43694
+ import { join as join35 } from "node:path";
42159
43695
  function codexDiagnosticLogPath(stateDir, projectId) {
42160
- return join33(stateDir, "codex-app-server", `${projectId}.diagnostics.log`);
43696
+ return join35(stateDir, "codex-app-server", `${projectId}.diagnostics.log`);
42161
43697
  }
42162
43698
  function createCodexDiagnosticSink(stateDir, projectId, now = () => /* @__PURE__ */ new Date()) {
42163
- const dir = join33(stateDir, "codex-app-server");
43699
+ const dir = join35(stateDir, "codex-app-server");
42164
43700
  const path3 = codexDiagnosticLogPath(stateDir, projectId);
42165
43701
  let ensured = false;
42166
43702
  return (event) => {
42167
43703
  if (event.event === "stderr") return;
42168
43704
  try {
42169
43705
  if (!ensured) {
42170
- mkdirSync14(dir, { recursive: true });
43706
+ mkdirSync15(dir, { recursive: true });
42171
43707
  ensured = true;
42172
43708
  }
42173
43709
  try {
42174
- if (statSync15(path3).size > MAX_BYTES2) renameSync8(path3, `${path3}.1`);
43710
+ if (statSync16(path3).size > MAX_BYTES2) renameSync8(path3, `${path3}.1`);
42175
43711
  } catch {
42176
43712
  }
42177
- appendFileSync3(path3, `${JSON.stringify({ at: now().toISOString(), ...event })}
43713
+ appendFileSync4(path3, `${JSON.stringify({ at: now().toISOString(), ...event })}
42178
43714
  `);
42179
43715
  } catch {
42180
43716
  }
@@ -42189,10 +43725,10 @@ var init_codex_app_server_diagnostics = __esm({
42189
43725
  });
42190
43726
 
42191
43727
  // packages/server/src/backend/stale-socket-sweep.ts
42192
- import { execFile as execFile8 } from "node:child_process";
43728
+ import { execFile as execFile9 } from "node:child_process";
42193
43729
  import { readdirSync as readdirSync8, unlinkSync as unlinkSync4 } from "node:fs";
42194
43730
  import { connect as connectSocket } from "node:net";
42195
- import { join as join34 } from "node:path";
43731
+ import { join as join36 } from "node:path";
42196
43732
  function parseLsofSocketNames(stdout) {
42197
43733
  const referenced = /* @__PURE__ */ new Set();
42198
43734
  for (const line of stdout.split("\n")) {
@@ -42209,7 +43745,7 @@ function sweepStaleSockets(options, deps = {}) {
42209
43745
  const keep = new Set(options.keep ?? []);
42210
43746
  let candidates;
42211
43747
  try {
42212
- candidates = (deps.readdir ?? readdirSync8)(options.dir).filter((name) => name.startsWith(options.prefix) && name.endsWith(".sock")).map((name) => join34(options.dir, name)).filter((path3) => !keep.has(path3));
43748
+ candidates = (deps.readdir ?? readdirSync8)(options.dir).filter((name) => name.startsWith(options.prefix) && name.endsWith(".sock")).map((name) => join36(options.dir, name)).filter((path3) => !keep.has(path3));
42213
43749
  } catch {
42214
43750
  return;
42215
43751
  }
@@ -42236,7 +43772,7 @@ var init_stale_socket_sweep = __esm({
42236
43772
  "packages/server/src/backend/stale-socket-sweep.ts"() {
42237
43773
  "use strict";
42238
43774
  defaultListReferenced = (prefix, done) => {
42239
- execFile8("lsof", ["-U", "-F", "n"], { timeout: 3e4, maxBuffer: 16 * 1024 * 1024 }, (error, stdout) => {
43775
+ execFile9("lsof", ["-U", "-F", "n"], { timeout: 3e4, maxBuffer: 16 * 1024 * 1024 }, (error, stdout) => {
42240
43776
  if (!stdout) return done(null);
42241
43777
  if (error && !stdout.includes(prefix)) return done(null);
42242
43778
  done(parseLsofSocketNames(stdout));
@@ -42259,8 +43795,8 @@ var init_stale_socket_sweep = __esm({
42259
43795
 
42260
43796
  // packages/server/src/orphan-reaper.ts
42261
43797
  import { execFileSync as execFileSync4 } from "node:child_process";
42262
- import { readFileSync as readFileSync21 } from "node:fs";
42263
- import { dirname as dirname14 } from "node:path";
43798
+ import { readFileSync as readFileSync22 } from "node:fs";
43799
+ import { dirname as dirname16 } from "node:path";
42264
43800
  function firstTokenBasename(command) {
42265
43801
  const first = command.trimStart().split(/\s+/, 1)[0] ?? "";
42266
43802
  const slash = first.lastIndexOf("/");
@@ -42488,7 +44024,7 @@ function sweepOrphansOnce(deps = {}) {
42488
44024
  }
42489
44025
  function sweepStaleBrokerSockets() {
42490
44026
  try {
42491
- sweepStaleSockets({ dir: dirname14(claudeBrokerSocketPath("", "")), prefix: CLAUDE_BROKER_SOCKET_PREFIX });
44027
+ sweepStaleSockets({ dir: dirname16(claudeBrokerSocketPath("", "")), prefix: CLAUDE_BROKER_SOCKET_PREFIX });
42492
44028
  } catch {
42493
44029
  }
42494
44030
  }
@@ -42538,7 +44074,7 @@ var init_orphan_reaper = __esm({
42538
44074
  defaultExec = (file, args) => execFileSync4(file, args, { encoding: "utf8", maxBuffer: 16 * 1024 * 1024, timeout: 1e4 });
42539
44075
  defaultReadEnv = (pid) => {
42540
44076
  try {
42541
- return readFileSync21(`/proc/${pid}/environ`, "utf8");
44077
+ return readFileSync22(`/proc/${pid}/environ`, "utf8");
42542
44078
  } catch {
42543
44079
  return null;
42544
44080
  }
@@ -42686,7 +44222,7 @@ var init_thread_hibernation = __esm({
42686
44222
  });
42687
44223
 
42688
44224
  // packages/server/src/context.ts
42689
- import { join as join35 } from "node:path";
44225
+ import { join as join37 } from "node:path";
42690
44226
  import { randomUUID as randomUUID15 } from "node:crypto";
42691
44227
  import { homedir as homedir15 } from "node:os";
42692
44228
  function reconcileSessions(storage) {
@@ -42713,6 +44249,9 @@ function partialContextCleanup(resources) {
42713
44249
  codexAppServer: createRetryableCleanup(async () => {
42714
44250
  await resources.codexAppServer?.shutdown();
42715
44251
  }),
44252
+ acpBridge: createRetryableCleanup(async () => {
44253
+ await resources.acpBridge?.shutdown();
44254
+ }),
42716
44255
  claudeBroker: createRetryableCleanup(async () => {
42717
44256
  resources.claudeBroker?.close();
42718
44257
  resources.claudeRuntimeIngest?.close();
@@ -42836,7 +44375,7 @@ async function createContext(opts = {}) {
42836
44375
  function createContextUnchecked(opts, resources) {
42837
44376
  const home = opts.home ?? homedir15();
42838
44377
  const project = opts.project ?? resolveProject();
42839
- const storage = createStorage(opts.database ?? join35(project.stateDir, "ui.db"), project.id);
44378
+ const storage = createStorage(opts.database ?? join37(project.stateDir, "ui.db"), project.id);
42840
44379
  resources.storage = storage;
42841
44380
  const bus = new Bus();
42842
44381
  const transcriptChange = new Emitter();
@@ -42911,7 +44450,8 @@ function createContextUnchecked(opts, resources) {
42911
44450
  };
42912
44451
  const claudeBackend = createClaudeBackend({ logDir: defaultLogDir(project), claudeBin: opts.claudeBin });
42913
44452
  const codexBackend = createCodexBackend({});
42914
- const backendFor = (kind) => kind === "codex" ? codexBackend : claudeBackend;
44453
+ const acpBackend = createAcpBackend({ stateDir: project.stateDir });
44454
+ const backendFor = (kind) => kind === "codex" ? codexBackend : kind === "acp" ? acpBackend : claudeBackend;
42915
44455
  const codexAppServer = codexAppServerBridgeEnabled() ? createCodexAppServerBridge({
42916
44456
  projectId: project.id,
42917
44457
  projectDir: project.dir,
@@ -42963,6 +44503,19 @@ function createContextUnchecked(opts, resources) {
42963
44503
  }
42964
44504
  void codexAppServer?.warmUp();
42965
44505
  opts.startup?.afterPhase?.("Codex app-server bridge");
44506
+ const acpBridge = createAcpBridge({
44507
+ projectId: project.id,
44508
+ stateDir: project.stateDir,
44509
+ frizzMcp: resolveFrizzMcp(frizzMcpTarget),
44510
+ interactions: storage.interactions,
44511
+ env: process.env,
44512
+ customAgents: () => getSettings(storage, home).acpAgents,
44513
+ onStatusChange: () => board?.refresh()
44514
+ });
44515
+ resources.acpBridge = acpBridge;
44516
+ contextUnsubscribers.push(storage.subscribeSessionLifecycle((event) => {
44517
+ acpBridge.releaseSession(event.previous.slug, event.previous.session_id, event.type === "replaced" ? "session-replaced" : "session-deleted");
44518
+ }));
42966
44519
  const claudeRuntimeIngest = claudeBrokerBridgeEnabled() ? createClaudeRuntimeIngest({ nudge: () => tailer.nudge?.() }) : void 0;
42967
44520
  resources.claudeRuntimeIngest = claudeRuntimeIngest;
42968
44521
  const claudeBroker = claudeBrokerBridgeEnabled() ? createClaudeAgentBrokerBridge({
@@ -43074,6 +44627,7 @@ function createContextUnchecked(opts, resources) {
43074
44627
  claudeBin: opts.claudeBin,
43075
44628
  backendFor,
43076
44629
  codexAppServer,
44630
+ acpBridge,
43077
44631
  claudeBroker,
43078
44632
  // Auth preflight (claude-auth plan, Slice A): Claude reads its local credential and confirms only
43079
44633
  // a positive signed-out against its CLI (readClaudePreflightAuth — the comment there records why
@@ -43113,6 +44667,21 @@ ${wakeDeliveryToken(deliveryId)}`;
43113
44667
  }
43114
44668
  return deliverCodexWake({ bridge, storage, cwd: project.dir, row, slug, deliveryMessage, deliveryId });
43115
44669
  }
44670
+ if (row?.backend === "acp") {
44671
+ return acpBridge.followUp({
44672
+ threadSlug: slug,
44673
+ sessionId: row.session_id,
44674
+ cwd: project.dir,
44675
+ agentId: row.acp_agent ?? "",
44676
+ modelId: acpModelIdFromModel(row.model),
44677
+ acpSessionId: row.agent_session_id,
44678
+ text: deliveryMessage,
44679
+ deliveryId
44680
+ }).then((r) => {
44681
+ if (r.acpSessionId !== row.agent_session_id) storage.setAgentSession(slug, r.acpSessionId);
44682
+ if (row.exited === 1) storage.setExitedIfCurrent(slug, row.session_id, row.runtime_generation ?? 0, false);
44683
+ });
44684
+ }
43116
44685
  if (row?.backend === "claude" && row.claude_runtime === "broker") {
43117
44686
  if (!claudeBroker) {
43118
44687
  process.stderr.write(`[frizz] claude-broker wake for ${slug} dropped: the session broker is unavailable
@@ -43178,6 +44747,7 @@ ${wakeDeliveryToken(deliveryId)}`;
43178
44747
  storage,
43179
44748
  interactions: storage.interactions,
43180
44749
  codexAppServer,
44750
+ acpBridge,
43181
44751
  claudeBroker,
43182
44752
  board,
43183
44753
  tailer,
@@ -43219,6 +44789,8 @@ var init_context = __esm({
43219
44789
  init_resume();
43220
44790
  init_claude();
43221
44791
  init_codex();
44792
+ init_acp_transcript();
44793
+ init_acp_bridge();
43222
44794
  init_auth_status();
43223
44795
  init_login_utility();
43224
44796
  init_usage_limit();
@@ -45895,8 +47467,8 @@ var init_server = __esm({
45895
47467
  });
45896
47468
 
45897
47469
  // packages/server/src/repair.ts
45898
- import { existsSync as existsSync12, readFileSync as readFileSync22, writeFileSync as writeFileSync11 } from "node:fs";
45899
- import { basename as basename7, dirname as dirname15, resolve as resolve8 } from "node:path";
47470
+ import { existsSync as existsSync13, readFileSync as readFileSync23, writeFileSync as writeFileSync11 } from "node:fs";
47471
+ import { basename as basename7, dirname as dirname17, resolve as resolve8 } from "node:path";
45900
47472
  function quoteValue(v) {
45901
47473
  if (/^[\w./#:+-]+$/.test(v)) return v;
45902
47474
  return `"${v.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
@@ -45908,10 +47480,10 @@ function deriveTitle(body, slug) {
45908
47480
  function repairThreadFile(frizzDir, file) {
45909
47481
  const root = resolve8(frizzDir);
45910
47482
  const abs = resolve8(root, file);
45911
- if (dirname15(abs) !== root) throw new RepairError(`refusing to repair "${file}": not directly under .frizz/`);
47483
+ if (dirname17(abs) !== root) throw new RepairError(`refusing to repair "${file}": not directly under .frizz/`);
45912
47484
  if (!abs.endsWith(".md")) throw new RepairError(`refusing to repair "${file}": not a .md thread file`);
45913
- if (!existsSync12(abs)) throw new RepairError(`no thread file to repair: ${basename7(abs)}`);
45914
- const content = readFileSync22(abs, "utf8");
47485
+ if (!existsSync13(abs)) throw new RepairError(`no thread file to repair: ${basename7(abs)}`);
47486
+ const content = readFileSync23(abs, "utf8");
45915
47487
  const firstLine2 = content.replace(/^/, "").split(/\r?\n/).find((l) => l.trim() !== "");
45916
47488
  if (firstLine2?.trim() === "---") {
45917
47489
  throw new RepairError(`${basename7(abs)} already opens with a "---" block \u2014 repair only heals a MISSING frontmatter block`);
@@ -46104,7 +47676,7 @@ var init_repo_files = __esm({
46104
47676
  });
46105
47677
 
46106
47678
  // cc-worker/hooks/bash-background.mjs
46107
- import { readFileSync as readFileSync23, realpathSync as realpathSync10 } from "node:fs";
47679
+ import { readFileSync as readFileSync24, realpathSync as realpathSync10 } from "node:fs";
46108
47680
  import { basename as basename8 } from "node:path";
46109
47681
  import { fileURLToPath as fileURLToPath4, pathToFileURL } from "node:url";
46110
47682
  function emit(obj) {
@@ -46124,8 +47696,8 @@ function withoutHeredocBodies(command) {
46124
47696
  return " ".repeat(line.length);
46125
47697
  }
46126
47698
  for (const match2 of line.matchAll(/<<-?\s*(?:'([^']+)'|"([^"]+)"|([A-Za-z_][A-Za-z0-9_-]*))/g)) {
46127
- const delimiter2 = match2[1] ?? match2[2] ?? match2[3];
46128
- if (delimiter2) pending.push(delimiter2);
47699
+ const delimiter3 = match2[1] ?? match2[2] ?? match2[3];
47700
+ if (delimiter3) pending.push(delimiter3);
46129
47701
  }
46130
47702
  active = pending.shift();
46131
47703
  return line;
@@ -46259,7 +47831,7 @@ var init_bash_background = __esm({
46259
47831
  if (isDirectHookExecution(process.argv[1], import.meta.url)) {
46260
47832
  try {
46261
47833
  const env = process.argv.includes("--frizz-thread") ? { ...process.env, FRIZZ_THREAD: process.env.FRIZZ_THREAD || "codex-worker" } : process.env;
46262
- emit(evaluateBashBackgroundHook(JSON.parse(readFileSync23(0, "utf8")), env));
47834
+ emit(evaluateBashBackgroundHook(JSON.parse(readFileSync24(0, "utf8")), env));
46263
47835
  } catch {
46264
47836
  emit({});
46265
47837
  }
@@ -46268,10 +47840,10 @@ var init_bash_background = __esm({
46268
47840
  });
46269
47841
 
46270
47842
  // packages/server/src/transcript.ts
46271
- import { closeSync as closeSync13, existsSync as existsSync13, fstatSync as fstatSync3, mkdirSync as mkdirSync15, openSync as openSync14, readdirSync as readdirSync9, readFileSync as readFileSync24, readSync as readSync7, renameSync as renameSync9, statSync as statSync16, unlinkSync as unlinkSync5, writeFileSync as writeFileSync12 } from "node:fs";
47843
+ import { closeSync as closeSync13, existsSync as existsSync14, fstatSync as fstatSync3, mkdirSync as mkdirSync16, openSync as openSync14, readdirSync as readdirSync9, readFileSync as readFileSync25, readSync as readSync7, renameSync as renameSync9, statSync as statSync17, unlinkSync as unlinkSync5, writeFileSync as writeFileSync12 } from "node:fs";
46272
47844
  import { createHash as createHash13 } from "node:crypto";
46273
- import { StringDecoder as StringDecoder4 } from "node:string_decoder";
46274
- import { join as join36 } from "node:path";
47845
+ import { StringDecoder as StringDecoder5 } from "node:string_decoder";
47846
+ import { join as join38 } from "node:path";
46275
47847
  import { homedir as homedir16 } from "node:os";
46276
47848
  function formatTokens(n) {
46277
47849
  if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
@@ -46851,14 +48423,14 @@ function pruneScreenshotCache() {
46851
48423
  if (entries2.length <= SCREENSHOT_CACHE_MAX) return;
46852
48424
  const byMtime = entries2.map((n) => {
46853
48425
  try {
46854
- return { n, m: statSync16(join36(SCREENSHOT_CACHE_DIR, n)).mtimeMs };
48426
+ return { n, m: statSync17(join38(SCREENSHOT_CACHE_DIR, n)).mtimeMs };
46855
48427
  } catch {
46856
48428
  return { n, m: 0 };
46857
48429
  }
46858
48430
  }).sort((a, b) => b.m - a.m);
46859
48431
  for (const { n } of byMtime.slice(SCREENSHOT_CACHE_MAX)) {
46860
48432
  try {
46861
- unlinkSync5(join36(SCREENSHOT_CACHE_DIR, n));
48433
+ unlinkSync5(join38(SCREENSHOT_CACHE_DIR, n));
46862
48434
  } catch {
46863
48435
  }
46864
48436
  }
@@ -46888,14 +48460,14 @@ function persistBase64Image(mediaType, data, idKey) {
46888
48460
  const ext = IMAGE_MEDIA_EXT[typeof mediaType === "string" ? mediaType.toLowerCase() : ""];
46889
48461
  if (!ext || !data) return void 0;
46890
48462
  const name = createHash13("sha256").update(idKey).digest("hex").slice(0, 32);
46891
- const path3 = join36(SCREENSHOT_CACHE_DIR, `${name}.${ext}`);
48463
+ const path3 = join38(SCREENSHOT_CACHE_DIR, `${name}.${ext}`);
46892
48464
  try {
46893
- if (existsSync13(path3)) return path3;
48465
+ if (existsSync14(path3)) return path3;
46894
48466
  if (data.length > SCREENSHOT_MAX_BASE64) return void 0;
46895
48467
  const buf = Buffer.from(data, "base64");
46896
48468
  if (buf.length === 0 || !looksLikeImage(buf, ext)) return void 0;
46897
- mkdirSync15(SCREENSHOT_CACHE_DIR, { recursive: true });
46898
- const tmp = join36(SCREENSHOT_CACHE_DIR, `.${name}.${process.pid}.${screenshotTmpSeq++}.tmp`);
48469
+ mkdirSync16(SCREENSHOT_CACHE_DIR, { recursive: true });
48470
+ const tmp = join38(SCREENSHOT_CACHE_DIR, `.${name}.${process.pid}.${screenshotTmpSeq++}.tmp`);
46899
48471
  writeFileSync12(tmp, buf);
46900
48472
  renameSync9(tmp, path3);
46901
48473
  pruneScreenshotCache();
@@ -46910,14 +48482,14 @@ function persistSentFile(srcPath, idKey) {
46910
48482
  const outExt = ext === "jpeg" ? "jpg" : ext;
46911
48483
  try {
46912
48484
  const name = createHash13("sha256").update(idKey).digest("hex").slice(0, 32);
46913
- const dest = join36(SCREENSHOT_CACHE_DIR, `${name}.${outExt}`);
46914
- if (existsSync13(dest)) return dest;
46915
- const size = statSync16(srcPath).size;
48485
+ const dest = join38(SCREENSHOT_CACHE_DIR, `${name}.${outExt}`);
48486
+ if (existsSync14(dest)) return dest;
48487
+ const size = statSync17(srcPath).size;
46916
48488
  if (size === 0 || size > SENT_IMAGE_MAX_BYTES) return void 0;
46917
- const buf = readFileSync24(srcPath);
48489
+ const buf = readFileSync25(srcPath);
46918
48490
  if (!looksLikeImage(buf, outExt)) return void 0;
46919
- mkdirSync15(SCREENSHOT_CACHE_DIR, { recursive: true });
46920
- const tmp = join36(SCREENSHOT_CACHE_DIR, `.${name}.${process.pid}.${screenshotTmpSeq++}.tmp`);
48491
+ mkdirSync16(SCREENSHOT_CACHE_DIR, { recursive: true });
48492
+ const tmp = join38(SCREENSHOT_CACHE_DIR, `.${name}.${process.pid}.${screenshotTmpSeq++}.tmp`);
46921
48493
  writeFileSync12(tmp, buf);
46922
48494
  renameSync9(tmp, dest);
46923
48495
  pruneScreenshotCache();
@@ -46935,7 +48507,7 @@ function viewImageCall(path3, idKey) {
46935
48507
  ...image ? { outputImage: image } : {}
46936
48508
  };
46937
48509
  }
46938
- function toolResultText3(content) {
48510
+ function toolResultText4(content) {
46939
48511
  if (typeof content === "string") return content.trim() || null;
46940
48512
  if (Array.isArray(content)) {
46941
48513
  const joined = content.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n");
@@ -46966,7 +48538,7 @@ function attachToolResults(rec, pending, backgroundShells, backgroundTaskIds, ch
46966
48538
  const ackAgentId = typeof rec.toolUseResult?.agentId === "string" ? rec.toolUseResult.agentId.trim() : "";
46967
48539
  const dispatchId = entry.calls.find((c) => typeof c.agentId === "string" && c.agentId)?.agentId;
46968
48540
  if (ackAgentId && dispatchId) childDispatchIds.set(ackAgentId, dispatchId);
46969
- const text2 = toolResultText3(b.content);
48541
+ const text2 = toolResultText4(b.content);
46970
48542
  if (text2 && entry.name === "TaskList") {
46971
48543
  const todos = parseTodoListResult(text2);
46972
48544
  if (todos) for (const call of entry.calls) call.todos = todos;
@@ -47319,7 +48891,7 @@ function retainedFoldEntry(path3, identityPrefix, fileId, size) {
47319
48891
  }
47320
48892
  const hit = entry !== void 0;
47321
48893
  if (!entry) {
47322
- entry = { identityPrefix, fold: createTranscriptFold(identityPrefix), bytesRead: 0, decoder: new StringDecoder4("utf8"), fileId };
48894
+ entry = { identityPrefix, fold: createTranscriptFold(identityPrefix), bytesRead: 0, decoder: new StringDecoder5("utf8"), fileId };
47323
48895
  } else {
47324
48896
  transcriptCache.delete(path3);
47325
48897
  }
@@ -47355,7 +48927,7 @@ function readTranscript(project, sessionId) {
47355
48927
  }
47356
48928
  function verifyIncrementalParse(path3, identityPrefix, incremental) {
47357
48929
  try {
47358
- const fresh = parseTranscript(readFileSync24(path3, "utf8"), identityPrefix);
48930
+ const fresh = parseTranscript(readFileSync25(path3, "utf8"), identityPrefix);
47359
48931
  const a = JSON.stringify(incremental);
47360
48932
  const b = JSON.stringify(fresh);
47361
48933
  if (a !== b) {
@@ -47375,16 +48947,16 @@ function verifyIncrementalParse(path3, identityPrefix, incremental) {
47375
48947
  }
47376
48948
  }
47377
48949
  function logDirOf(project) {
47378
- return join36(homedir16(), ".claude", "projects", project.cwdSlug);
48950
+ return join38(homedir16(), ".claude", "projects", project.cwdSlug);
47379
48951
  }
47380
48952
  function resolveTranscriptPath(project, sessionId) {
47381
- const path3 = join36(logDirOf(project), `${sessionId}.jsonl`);
48953
+ const path3 = join38(logDirOf(project), `${sessionId}.jsonl`);
47382
48954
  try {
47383
- if (statSync16(path3).size > 0) return path3;
48955
+ if (statSync17(path3).size > 0) return path3;
47384
48956
  } catch {
47385
48957
  }
47386
48958
  const stranded = discoverTranscriptDir(logDirOf(project), sessionId);
47387
- return stranded ? join36(stranded, `${sessionId}.jsonl`) : path3;
48959
+ return stranded ? join38(stranded, `${sessionId}.jsonl`) : path3;
47388
48960
  }
47389
48961
  function projectCodexTranscript(raw2, identityPrefix = "codex") {
47390
48962
  const out = [];
@@ -47678,6 +49250,17 @@ function parseCodexTranscript(raw2, identityPrefix = "codex") {
47678
49250
  return latestTranscriptWindow(out);
47679
49251
  }
47680
49252
  function latestWindowStart(messages) {
49253
+ return byteBoundedStart(messages, countBoundedStart(messages));
49254
+ }
49255
+ function byteBoundedStart(messages, start) {
49256
+ let bytes = 0;
49257
+ for (let i = messages.length - 1; i >= start; i--) {
49258
+ bytes += messageBytes(messages[i]);
49259
+ if (bytes > LATEST_WINDOW_MAX_BYTES) return Math.min(i + 1, messages.length - 1);
49260
+ }
49261
+ return start;
49262
+ }
49263
+ function countBoundedStart(messages) {
47681
49264
  const tail = Math.max(0, messages.length - MAX_MESSAGES);
47682
49265
  if (tail === 0) return 0;
47683
49266
  let boundary = -1;
@@ -47759,7 +49342,7 @@ function codexDirectToolCall(name, obj, callId) {
47759
49342
  detail: strField(obj.task_name) ?? "sub-agent",
47760
49343
  subagentType: codexAgentCell(obj),
47761
49344
  agentId: callId,
47762
- input: compactFields(obj, ["agent_type", "fork_context", "fork_turns", "service_tier"])
49345
+ prompt: typeof obj.message === "string" && obj.message.trim() && strField(obj.message) !== ENCRYPTED_PAYLOAD ? capAgentPrompt(obj.message) : void 0
47763
49346
  };
47764
49347
  case "send_message":
47765
49348
  return codexPeerMessageCall("Send message", target, obj);
@@ -47871,14 +49454,6 @@ function codexAgentCell(obj) {
47871
49454
  const effort = strField(obj.reasoning_effort);
47872
49455
  return model && effort ? `${model}/${effort}` : model ?? effort;
47873
49456
  }
47874
- function compactFields(obj, keys) {
47875
- const projected = {};
47876
- for (const key of keys) {
47877
- const value = obj[key];
47878
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") projected[key] = value;
47879
- }
47880
- return Object.keys(projected).length ? renderToolInput(projected) : void 0;
47881
- }
47882
49457
  function formatCompactDuration(ms) {
47883
49458
  if (ms < 1e3) return `${Math.round(ms)}ms`;
47884
49459
  if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
@@ -48434,9 +50009,9 @@ function defaultCodexBackend() {
48434
50009
  function sourceForThread(project, storage, slug, backendFor) {
48435
50010
  const row = storage.getSession(slug);
48436
50011
  if (row) {
48437
- const backend = row.backend === "codex" ? "codex" : "claude";
48438
- const nativeId = backend === "codex" ? row.agent_session_id ?? row.session_id : row.transcript_id ?? row.session_id;
48439
- const path3 = backend === "codex" ? (backendFor?.("codex") ?? defaultCodexBackend()).transcriptPath(nativeId) : resolveTranscriptPath(project, nativeId);
50012
+ const backend = row.backend === "codex" ? "codex" : row.backend === "acp" ? "acp" : "claude";
50013
+ const nativeId = backend === "codex" ? row.agent_session_id ?? row.session_id : backend === "acp" ? row.session_id : row.transcript_id ?? row.session_id;
50014
+ const path3 = backend === "codex" ? (backendFor?.("codex") ?? defaultCodexBackend()).transcriptPath(nativeId) : backend === "acp" ? backendFor?.("acp")?.transcriptPath(nativeId) : resolveTranscriptPath(project, nativeId);
48440
50015
  if (!path3) return void 0;
48441
50016
  return {
48442
50017
  slug,
@@ -48449,7 +50024,7 @@ function sourceForThread(project, storage, slug, backendFor) {
48449
50024
  }
48450
50025
  if (!FOREIGN_SESSION_ID_RE.test(slug)) return void 0;
48451
50026
  const claudePath = resolveTranscriptPath(project, slug);
48452
- if (existsSync13(claudePath)) {
50027
+ if (existsSync14(claudePath)) {
48453
50028
  return { slug, sessionId: slug, nativeId: slug, backend: "claude", runtimeGeneration: 0, path: claudePath };
48454
50029
  }
48455
50030
  const rollout = (backendFor?.("codex") ?? defaultCodexBackend()).transcriptPath(slug);
@@ -48543,6 +50118,7 @@ function fixedSnapshot(source) {
48543
50118
  function projectSnapshot(snapshot) {
48544
50119
  const prefix = `${snapshot.backend}:${snapshot.nativeId}`;
48545
50120
  if (snapshot.backend === "codex") return retireStaleQueuedBubbles(projectCodexTranscript(snapshot.raw, prefix));
50121
+ if (snapshot.backend === "acp") return retireStaleQueuedBubbles(projectAcpTranscript(snapshot.raw, prefix));
48546
50122
  const { entry } = retainedFoldEntry(snapshot.path, prefix, snapshot.fileKey, snapshot.size);
48547
50123
  ingestBounded(entry, snapshot.size, (from, length) => snapshot.readRange(from, length));
48548
50124
  return retireStaleQueuedBubbles([...entry.fold.allMessages()]);
@@ -48599,7 +50175,7 @@ function decodeTranscriptCursor(cursor) {
48599
50175
  }
48600
50176
  const p = value;
48601
50177
  const validText2 = (s, max) => typeof s === "string" && s.length > 0 && s.length <= max && !/[\0\r\n]/.test(s);
48602
- if (!p || p.v !== 1 || !validText2(p.slug, 256) || !validText2(p.sessionId, 256) || !validText2(p.nativeId, 256) || p.backend !== "claude" && p.backend !== "codex" || !Number.isSafeInteger(p.runtimeGeneration) || (p.runtimeGeneration ?? -1) < 0 || !validText2(p.fileKey, 256) || !Number.isSafeInteger(p.snapshotBytes) || (p.snapshotBytes ?? -1) < 0 || !validText2(p.prefixDigest, 128) || !validText2(p.anchorSourceId, 768)) throw new Error("invalid transcript cursor");
50178
+ if (!p || p.v !== 1 || !validText2(p.slug, 256) || !validText2(p.sessionId, 256) || !validText2(p.nativeId, 256) || p.backend !== "claude" && p.backend !== "codex" && p.backend !== "acp" || !Number.isSafeInteger(p.runtimeGeneration) || (p.runtimeGeneration ?? -1) < 0 || !validText2(p.fileKey, 256) || !Number.isSafeInteger(p.snapshotBytes) || (p.snapshotBytes ?? -1) < 0 || !validText2(p.prefixDigest, 128) || !validText2(p.anchorSourceId, 768)) throw new Error("invalid transcript cursor");
48603
50179
  return p;
48604
50180
  }
48605
50181
  function messageBytes(message) {
@@ -48645,7 +50221,7 @@ function emptyTranscriptPage(source) {
48645
50221
  editedFiles: []
48646
50222
  };
48647
50223
  }
48648
- function readLatestThreadTranscriptPage(project, storage, slug, backendFor) {
50224
+ function readLatestThreadTranscriptPage(project, storage, slug, backendFor, opts = {}) {
48649
50225
  let source = sourceForThread(project, storage, slug, backendFor);
48650
50226
  if (!source) return emptyTranscriptPage();
48651
50227
  let snapshot = fixedSnapshot(source);
@@ -48681,7 +50257,7 @@ function readLatestThreadTranscriptPage(project, storage, slug, backendFor) {
48681
50257
  // ones inside this checkout belong on the rail (edited-files.ts). Then git drops whatever the
48682
50258
  // repository would not carry — scratch, build output — leaving the rail an account of repo work
48683
50259
  // (repo-files.ts).
48684
- editedFiles: repoCarriedEditedFiles(project.dir, editedFilesOf(projected, project.dir))
50260
+ ...opts.editedFiles === false ? {} : { editedFiles: repoCarriedEditedFiles(project.dir, editedFilesOf(projected, project.dir)) }
48685
50261
  };
48686
50262
  }
48687
50263
  function readEarlierThreadTranscriptPage(project, storage, slug, cursor, backendFor) {
@@ -48715,41 +50291,11 @@ function readEarlierThreadTranscriptPage(project, storage, slug, cursor, backend
48715
50291
  }
48716
50292
  function readCodexTranscriptFile(absPath, nativeId = absPath) {
48717
50293
  try {
48718
- return parseCodexTranscript(readFileSync24(absPath, "utf8"), `codex:${nativeId}`);
50294
+ return parseCodexTranscript(readFileSync25(absPath, "utf8"), `codex:${nativeId}`);
48719
50295
  } catch {
48720
50296
  return [];
48721
50297
  }
48722
50298
  }
48723
- function readThreadTranscript(project, storage, slug, backendFor) {
48724
- const row = storage.getSession(slug);
48725
- if (row) {
48726
- if (row.backend === "codex") {
48727
- const backend = backendFor?.("codex") ?? defaultCodexBackend();
48728
- const nativeId = row.agent_session_id ?? row.session_id;
48729
- const path3 = backend.transcriptPath(nativeId);
48730
- const codexLedger = parseDeliveryLedger(row.delivery_ledger);
48731
- return projectDeliveryLedger(path3 ? readCodexTranscriptFile(path3, nativeId) : [], codexLedger);
48732
- }
48733
- const ledger = parseDeliveryLedger(row.delivery_ledger);
48734
- const msgs = readTranscript(project, row.transcript_id ?? row.session_id);
48735
- if (msgs.length || row.transcript_id) return projectDeliveryLedger(msgs, ledger);
48736
- if (Date.now() - Date.parse(row.spawned_at) < DISCOVERY_GRACE_MS) return projectDeliveryLedger(msgs, ledger);
48737
- const exclude = /* @__PURE__ */ new Set();
48738
- for (const r of storage.allSessions()) {
48739
- if (r.slug === row.slug) continue;
48740
- exclude.add(r.session_id);
48741
- if (r.transcript_id) exclude.add(r.transcript_id);
48742
- }
48743
- const found = discoverTranscriptId(logDirOf(project), row.session_id, { exclude });
48744
- return projectDeliveryLedger(found ? readTranscript(project, found) : msgs, ledger);
48745
- }
48746
- if (FOREIGN_SESSION_ID_RE.test(slug)) {
48747
- const source = sourceForThread(project, storage, slug, backendFor);
48748
- if (!source) return [];
48749
- return source.backend === "codex" ? readCodexTranscriptFile(source.path, slug) : readTranscript(project, slug);
48750
- }
48751
- return [];
48752
- }
48753
50299
  function projectTranscriptAgentLifecycles(messages, lookup) {
48754
50300
  const cache2 = /* @__PURE__ */ new Map();
48755
50301
  const projectTool = (tool) => {
@@ -48813,12 +50359,12 @@ function projectTranscriptPageAgentLifecycles(page, lookup, peerLookup) {
48813
50359
  function readTranscriptFile(absPath) {
48814
50360
  try {
48815
50361
  const pathKey = createHash13("sha256").update(absPath).digest("base64url").slice(0, 16);
48816
- return parseTranscript(readFileSync24(absPath, "utf8"), `claude-file:${pathKey}`);
50362
+ return parseTranscript(readFileSync25(absPath, "utf8"), `claude-file:${pathKey}`);
48817
50363
  } catch {
48818
50364
  return [];
48819
50365
  }
48820
50366
  }
48821
- var MAX_MESSAGES, LATEST_WINDOW_ASK_REACH_ITEMS, LATEST_WINDOW_ASK_REACH_BYTES, REST_LABEL, LEGACY_TASK_MARKER, LEGACY_BANNER_PREAMBLE, COMMAND_NAME_RE, COMMAND_ARGS_RE, PEER_SESSION_PREAMBLE, CLAUDE_COORDINATOR_INSTRUCTION_PREFIX, CLAUDE_COORDINATOR_INSTRUCTION_SUFFIX, CLAUDE_PEER_INSTRUCTION_PREFIX, QUEUED_STALE_MS, EDIT_CAP, TRUNC_MARKER, ENCRYPTED_PAYLOAD, AGENT_PROMPT_CAP, SEND_BODY_CAP, COMMAND_CAP, TOOL_INPUT_CAP, READ_LINE_CAP, READ_BYTE_CAP, IMAGE_MEDIA_EXT, SCREENSHOT_CACHE_DIR, SCREENSHOT_CACHE_MAX, SCREENSHOT_MAX_BASE64, screenshotTmpSeq, SENT_IMAGE_MAX_BYTES, SENT_FILES_MAX, TODO_ROWS_MAX, TODO_TEXT_CAP, TRANSCRIPT_CACHE_CAP, transcriptCache, PARSE_VERIFY_SAMPLE, TRANSCRIPT_DECODE_WINDOW, FOREIGN_SESSION_ID_RE, MAX_PINNED_BACKGROUND_OPERATIONS, CAPTION_MAX, _defaultCodexBackend, TRANSCRIPT_EARLIER_MAX_ITEMS, TRANSCRIPT_EARLIER_MAX_BYTES, fullDigestCache, FULL_DIGEST_CACHE_CAP, EMPTY_RETIRED_OPS;
50367
+ var MAX_MESSAGES, LATEST_WINDOW_ASK_REACH_ITEMS, LATEST_WINDOW_ASK_REACH_BYTES, LATEST_WINDOW_MAX_BYTES, REST_LABEL, LEGACY_TASK_MARKER, LEGACY_BANNER_PREAMBLE, COMMAND_NAME_RE, COMMAND_ARGS_RE, PEER_SESSION_PREAMBLE, CLAUDE_COORDINATOR_INSTRUCTION_PREFIX, CLAUDE_COORDINATOR_INSTRUCTION_SUFFIX, CLAUDE_PEER_INSTRUCTION_PREFIX, QUEUED_STALE_MS, EDIT_CAP, TRUNC_MARKER, ENCRYPTED_PAYLOAD, AGENT_PROMPT_CAP, SEND_BODY_CAP, COMMAND_CAP, TOOL_INPUT_CAP, READ_LINE_CAP, READ_BYTE_CAP, IMAGE_MEDIA_EXT, SCREENSHOT_CACHE_DIR, SCREENSHOT_CACHE_MAX, SCREENSHOT_MAX_BASE64, screenshotTmpSeq, SENT_IMAGE_MAX_BYTES, SENT_FILES_MAX, TODO_ROWS_MAX, TODO_TEXT_CAP, TRANSCRIPT_CACHE_CAP, transcriptCache, PARSE_VERIFY_SAMPLE, TRANSCRIPT_DECODE_WINDOW, FOREIGN_SESSION_ID_RE, MAX_PINNED_BACKGROUND_OPERATIONS, CAPTION_MAX, _defaultCodexBackend, TRANSCRIPT_EARLIER_MAX_ITEMS, TRANSCRIPT_EARLIER_MAX_BYTES, fullDigestCache, FULL_DIGEST_CACHE_CAP, EMPTY_RETIRED_OPS;
48822
50368
  var init_transcript = __esm({
48823
50369
  "packages/server/src/transcript.ts"() {
48824
50370
  "use strict";
@@ -48829,6 +50375,7 @@ var init_transcript = __esm({
48829
50375
  init_delivery_marker();
48830
50376
  init_completion_relay();
48831
50377
  init_codex();
50378
+ init_acp_transcript();
48832
50379
  init_discover();
48833
50380
  init_tailer();
48834
50381
  init_credential_redaction();
@@ -48838,6 +50385,7 @@ var init_transcript = __esm({
48838
50385
  MAX_MESSAGES = 300;
48839
50386
  LATEST_WINDOW_ASK_REACH_ITEMS = 100;
48840
50387
  LATEST_WINDOW_ASK_REACH_BYTES = 512 * 1024;
50388
+ LATEST_WINDOW_MAX_BYTES = 3 * 1024 * 1024;
48841
50389
  REST_LABEL = "Agent rested";
48842
50390
  LEGACY_TASK_MARKER = "\nTASK:\n";
48843
50391
  LEGACY_BANNER_PREAMBLE = "Everything ABOVE this line is frizz system orientation. Everything BELOW the `TASK:` marker is the human operator's own prompt, verbatim.";
@@ -48889,7 +50437,7 @@ var init_transcript = __esm({
48889
50437
  });
48890
50438
 
48891
50439
  // packages/server/src/open-external.ts
48892
- import { spawn as spawn6 } from "node:child_process";
50440
+ import { spawn as spawn7 } from "node:child_process";
48893
50441
  function validateExternalUrl(raw2) {
48894
50442
  let parsed;
48895
50443
  try {
@@ -48911,7 +50459,7 @@ async function openExternalUrl(raw2, options = {}) {
48911
50459
  const v = validateExternalUrl(raw2);
48912
50460
  if (!v.ok) throw new Error(v.reason);
48913
50461
  const spec = externalUrlOpenCommand(v.url, options.platform);
48914
- const child = (options.spawn ?? spawn6)(spec.command, [...spec.args], { detached: true, stdio: "ignore", windowsHide: true });
50462
+ const child = (options.spawn ?? spawn7)(spec.command, [...spec.args], { detached: true, stdio: "ignore", windowsHide: true });
48915
50463
  try {
48916
50464
  await awaitOpenerStart(child, spec.command);
48917
50465
  return { opened: true };
@@ -49210,7 +50758,7 @@ var init_github_hovercard = __esm({
49210
50758
  });
49211
50759
 
49212
50760
  // packages/server/src/backend/account-actions.ts
49213
- import { execFile as execFile9 } from "node:child_process";
50761
+ import { execFile as execFile10 } from "node:child_process";
49214
50762
  import { promisify as promisify8 } from "node:util";
49215
50763
  function liveThreadsForBackend(threads, backend) {
49216
50764
  let count = 0;
@@ -49223,6 +50771,7 @@ function liveThreadsForBackend(threads, backend) {
49223
50771
  return count;
49224
50772
  }
49225
50773
  async function runProviderLogout(opts) {
50774
+ if (opts.backend === "acp") return { status: "failed", detail: "Log out with the agent's own CLI; Frizz holds no credential for an ACP agent.", auth: "unknown" };
49226
50775
  if (opts.liveThreads > 0) {
49227
50776
  return { status: "blocked", activeThreads: opts.liveThreads, auth: (await readAuthSnapshot({ claudeBin: opts.claudeBin }))[opts.backend] };
49228
50777
  }
@@ -49245,7 +50794,7 @@ var init_account_actions = __esm({
49245
50794
  "packages/server/src/backend/account-actions.ts"() {
49246
50795
  "use strict";
49247
50796
  init_auth_status();
49248
- execFileAsync6 = promisify8(execFile9);
50797
+ execFileAsync6 = promisify8(execFile10);
49249
50798
  LIVE_RUNTIMES = /* @__PURE__ */ new Set(["spawning", "running", "perm-prompt"]);
49250
50799
  }
49251
50800
  });
@@ -49378,12 +50927,12 @@ var init_background_shell_output = __esm({
49378
50927
  });
49379
50928
 
49380
50929
  // packages/server/src/image-header.ts
49381
- import { closeSync as closeSync15, openSync as openSync16, readSync as readSync9, statSync as statSync17 } from "node:fs";
50930
+ import { closeSync as closeSync15, openSync as openSync16, readSync as readSync9, statSync as statSync18 } from "node:fs";
49382
50931
  import { extname } from "node:path";
49383
50932
  function readHead3(path3, budget) {
49384
50933
  let fd;
49385
50934
  try {
49386
- const size = statSync17(path3).size;
50935
+ const size = statSync18(path3).size;
49387
50936
  if (size === 0) return void 0;
49388
50937
  const buffer = Buffer.alloc(Math.min(size, budget));
49389
50938
  fd = openSync16(path3, "r");
@@ -49520,7 +51069,7 @@ var init_image_header = __esm({
49520
51069
  });
49521
51070
 
49522
51071
  // packages/server/src/directory-picker.ts
49523
- import { execFile as execFile10 } from "node:child_process";
51072
+ import { execFile as execFile11 } from "node:child_process";
49524
51073
  import { promisify as promisify9 } from "node:util";
49525
51074
  async function pickImageFile(startIn, prompt = "Choose an image for this project", platform3 = process.platform) {
49526
51075
  if (platform3 === "darwin") {
@@ -49602,20 +51151,22 @@ var run2, PICKER_TIMEOUT_MS, OSASCRIPT;
49602
51151
  var init_directory_picker = __esm({
49603
51152
  "packages/server/src/directory-picker.ts"() {
49604
51153
  "use strict";
49605
- run2 = promisify9(execFile10);
51154
+ run2 = promisify9(execFile11);
49606
51155
  PICKER_TIMEOUT_MS = 5 * 6e4;
49607
51156
  OSASCRIPT = (prompt) => `POSIX path of (choose folder with prompt ${JSON.stringify(prompt)})`;
49608
51157
  }
49609
51158
  });
49610
51159
 
49611
51160
  // packages/server/src/router.ts
49612
- import { readFileSync as readFileSync25, statSync as statSync18 } from "node:fs";
49613
- import { join as join37, resolve as resolve9 } from "node:path";
51161
+ import { readFileSync as readFileSync26, statSync as statSync19 } from "node:fs";
51162
+ import { join as join39, resolve as resolve9 } from "node:path";
49614
51163
  import { randomUUID as randomUUID16 } from "node:crypto";
49615
- import { basename as basename9, dirname as dirname16 } from "node:path";
49616
- import { existsSync as existsSync14, mkdirSync as mkdirSync16, rmSync as rmSync10, writeFileSync as writeFileSync13 } from "node:fs";
51164
+ import { basename as basename9, dirname as dirname18 } from "node:path";
51165
+ import { existsSync as existsSync15, mkdirSync as mkdirSync17, rmSync as rmSync10, writeFileSync as writeFileSync13 } from "node:fs";
49617
51166
  import { homedir as homedir17 } from "node:os";
49618
51167
  function validateGithubDispatchProfile(input) {
51168
+ if (input.backend === "acp") return;
51169
+ if (input.effort === void 0) throw new Error(`Unsupported ${input.backend} model/effort pair: ${input.model} / (no effort)`);
49619
51170
  validateThreadProfile(input.backend, input.model, input.effort);
49620
51171
  }
49621
51172
  function githubDispatcherRequest(input, item) {
@@ -49638,7 +51189,11 @@ function isAppServerCodexRow(row) {
49638
51189
  function appServerCodexTurnLive(codex, row) {
49639
51190
  return codex?.turnLiveness(row.slug, row.session_id)?.bridgeTurn === true;
49640
51191
  }
49641
- async function stopThreadRuntime(storage, row, runtime = cachedLivenessTerminator, codex, claudeBroker) {
51192
+ async function stopThreadRuntime(storage, row, runtime = cachedLivenessTerminator, codex, claudeBroker, acp) {
51193
+ if (row.backend === "acp") {
51194
+ if (!acp?.turnLiveness(row.slug, row.session_id)?.turnActive) return "absent";
51195
+ return (await acp.interruptTurn(row.slug, row.session_id)).interrupted ? "stopped" : "absent";
51196
+ }
49642
51197
  if (isAppServerCodexRow(row)) {
49643
51198
  if (!appServerCodexTurnLive(codex, row)) return "absent";
49644
51199
  if (!codex) throw new Error("The Codex app-server is unavailable; nothing was stopped");
@@ -49679,9 +51234,9 @@ function stopRegisteredRuntime(storage, row, runtime = cachedLivenessTerminator)
49679
51234
  }
49680
51235
  return "stopped";
49681
51236
  }
49682
- async function stopRuntimeBySlug(storage, slug, runtime = cachedLivenessTerminator, codex, claudeBroker) {
51237
+ async function stopRuntimeBySlug(storage, slug, runtime = cachedLivenessTerminator, codex, claudeBroker, acp) {
49683
51238
  const row = storage.getSession(slug);
49684
- if (row) return { outcome: await stopThreadRuntime(storage, row, runtime, codex, claudeBroker), row };
51239
+ if (row) return { outcome: await stopThreadRuntime(storage, row, runtime, codex, claudeBroker, acp), row };
49685
51240
  if (storage.getAdoptionClaim(slug)) throw new Error("An adoption attempt is in progress; nothing was stopped");
49686
51241
  throw new Error("No registered runtime identity is available; nothing was stopped");
49687
51242
  }
@@ -49713,18 +51268,18 @@ function cutOffHold(telemetry) {
49713
51268
  if (telemetry?.turn !== "in-flight") return void 0;
49714
51269
  return { turnInFlight: true, cutOff: true, unobservable: false, subAgents: [], subAgentCount: 0, bgShells: [], bgShellCount: 0 };
49715
51270
  }
49716
- async function completeRegisteredThread(storage, row, terminateLive, runtime = cachedLivenessTerminator, telemetry, codex, claudeBroker) {
51271
+ async function completeRegisteredThread(storage, row, terminateLive, runtime = cachedLivenessTerminator, telemetry, codex, claudeBroker, acp) {
49717
51272
  const binding = adoptionRuntimeBinding(storage, row);
49718
51273
  if (binding.kind === "conflict") {
49719
51274
  throw new Error("This thread has a competing adoption attempt; nothing was changed");
49720
51275
  }
49721
51276
  const appServerCodex = isAppServerCodexRow(row);
49722
51277
  const brokerClaude = isBrokerClaudeRow(row);
49723
- const live = brokerClaude ? claudeBroker?.isDaemonAlive(row.session_id) ?? false : appServerCodex ? appServerCodexTurnLive(codex, row) : false;
51278
+ const live = brokerClaude ? claudeBroker?.isDaemonAlive(row.session_id) ?? false : appServerCodex ? appServerCodexTurnLive(codex, row) : row.backend === "acp" ? acp?.turnLiveness(row.slug, row.session_id)?.turnActive ?? false : false;
49724
51279
  const hold = terminateLive ? void 0 : live ? completionConfirmationHold(telemetry) : cutOffHold(telemetry);
49725
51280
  if (hold) return { needsConfirmation: true, hold };
49726
51281
  if (live) {
49727
- await stopThreadRuntime(storage, row, runtime, codex, claudeBroker);
51282
+ await stopThreadRuntime(storage, row, runtime, codex, claudeBroker, acp);
49728
51283
  if (!appServerCodex && !brokerClaude && binding.kind === "unbound" && runtime.isLive(row.slug)) {
49729
51284
  throw new Error("The session could not be confirmed stopped; it was not marked done");
49730
51285
  }
@@ -49735,7 +51290,7 @@ async function completeRegisteredThread(storage, row, terminateLive, runtime = c
49735
51290
  }
49736
51291
  return { needsConfirmation: false };
49737
51292
  }
49738
- async function stopAndForgetRegisteredRuntime(storage, row, runtime = cachedLivenessTerminator, codex, claudeBroker) {
51293
+ async function stopAndForgetRegisteredRuntime(storage, row, runtime = cachedLivenessTerminator, codex, claudeBroker, acp) {
49739
51294
  const binding = adoptionRuntimeBinding(storage, row);
49740
51295
  if (binding.kind === "conflict") {
49741
51296
  throw new Error("This thread changed while it was being dismissed; nothing was removed");
@@ -49745,7 +51300,7 @@ async function stopAndForgetRegisteredRuntime(storage, row, runtime = cachedLive
49745
51300
  runtimeGeneration: row.runtime_generation ?? 0,
49746
51301
  adoptionAttemptToken: binding.kind === "bound" ? binding.claim.attempt_token : null
49747
51302
  };
49748
- await stopThreadRuntime(storage, row, runtime, codex, claudeBroker);
51303
+ await stopThreadRuntime(storage, row, runtime, codex, claudeBroker, acp);
49749
51304
  const forgotten = storage.forgetSessionIfCurrent(row.slug, expected);
49750
51305
  if (!forgotten) {
49751
51306
  throw new Error("This thread resumed or was replaced while it was being dismissed; the new worker was preserved");
@@ -49777,11 +51332,11 @@ function projectCard(entry, stale) {
49777
51332
  function addProjectAtPath(input, home = homedir17()) {
49778
51333
  const typed = input.trim();
49779
51334
  if (!typed) throw new Error("Enter a folder path.");
49780
- const expanded = typed === "~" || typed.startsWith("~/") ? join37(home, typed.slice(1)) : typed;
51335
+ const expanded = typed === "~" || typed.startsWith("~/") ? join39(home, typed.slice(1)) : typed;
49781
51336
  const absolute = resolve9(expanded);
49782
51337
  let stats;
49783
51338
  try {
49784
- stats = statSync18(absolute);
51339
+ stats = statSync19(absolute);
49785
51340
  } catch {
49786
51341
  throw new Error(`No folder at ${absolute}`);
49787
51342
  }
@@ -49806,7 +51361,7 @@ function storeProjectIcon(id, name, bytes) {
49806
51361
  if (!entry) throw new Error("No such project.");
49807
51362
  if (bytes.length === 0) throw new Error("That file is empty.");
49808
51363
  const path3 = customIconPath(id, `.${extension2}`);
49809
- mkdirSync16(dirname16(path3), { recursive: true });
51364
+ mkdirSync17(dirname18(path3), { recursive: true });
49810
51365
  writeFileSync13(path3, bytes);
49811
51366
  if (!imageDimensions(path3)) {
49812
51367
  rmSync10(path3, { force: true });
@@ -49823,7 +51378,7 @@ function storeProjectIcon(id, name, bytes) {
49823
51378
  function setProjectIconFromFile(id, file) {
49824
51379
  let bytes;
49825
51380
  try {
49826
- bytes = readFileSync25(file);
51381
+ bytes = readFileSync26(file);
49827
51382
  } catch {
49828
51383
  throw new Error("That file could not be read.");
49829
51384
  }
@@ -49856,7 +51411,7 @@ function mergeSubAgentSteers(messages, steers) {
49856
51411
  return merged;
49857
51412
  }
49858
51413
  function createRouter(ctx) {
49859
- const frizzDir = join37(ctx.project.dir, ".frizz");
51414
+ const frizzDir = join39(ctx.project.dir, ".frizz");
49860
51415
  const openRoots = openableFileRoots(ctx.project);
49861
51416
  function isAutoTitledSession(slug) {
49862
51417
  return ctx.storage.getSession(slug)?.title_auto === 1;
@@ -50483,6 +52038,28 @@ function createRouter(ctx) {
50483
52038
  ${gapNote}` : input.message;
50484
52039
  if (row) reopenArchivedThreadForFollowUp(ctx, row);
50485
52040
  if (row) wakeParkedThreadForFollowUp(ctx, row);
52041
+ if (row?.backend === "acp") {
52042
+ const bridge = ctx.acpBridge;
52043
+ if (!bridge) throw new Error("The ACP bridge is unavailable; cannot deliver this follow-up");
52044
+ const result = await bridge.followUp({
52045
+ threadSlug: input.slug,
52046
+ sessionId: row.session_id,
52047
+ cwd: ctx.project.dir,
52048
+ agentId: row.acp_agent ?? "",
52049
+ modelId: acpModelIdFromModel(row.model),
52050
+ acpSessionId: row.agent_session_id,
52051
+ text: messageForWorker,
52052
+ ...input.deliveryId ? { deliveryId: input.deliveryId } : {}
52053
+ });
52054
+ if (result.acpSessionId !== row.agent_session_id) ctx.storage.setAgentSession(input.slug, result.acpSessionId);
52055
+ if (row.exited === 1) ctx.storage.setExitedIfCurrent(input.slug, row.session_id, row.runtime_generation ?? 0, false);
52056
+ if (input.deliveryId) {
52057
+ appendDelivery(ctx.storage, input.slug, { id: input.deliveryId, text: input.message, state: result.state === "queued" ? "enqueued" : "delivered" });
52058
+ ctx.transcriptChange.emit([input.slug]);
52059
+ }
52060
+ ctx.board.refresh();
52061
+ return;
52062
+ }
50486
52063
  if (row?.backend === "codex") {
50487
52064
  const bridge = ctx.codexAppServer;
50488
52065
  if (!bridge) throw new Error("Codex app-server is unavailable; cannot deliver this follow-up");
@@ -50668,6 +52245,7 @@ ${gapNote}` : input.message;
50668
52245
  const thread = (await ctx.board.snapshot()).threads.find((t) => t.id === input.slug);
50669
52246
  if (!thread || thread.foreign || thread.kind !== "session") throw new Error(`thread ${input.slug} is not editable`);
50670
52247
  const permRow = ctx.storage.getSession(input.slug);
52248
+ if (permRow?.backend === "acp") throw new Error("An ACP agent enforces its own permissions; Frizz has no mode to set on it");
50671
52249
  if (permRow?.backend === "codex") {
50672
52250
  ctx.storage.setPermissionMode(input.slug, input.permissionMode);
50673
52251
  ctx.board.refresh();
@@ -50713,7 +52291,9 @@ ${gapNote}` : input.message;
50713
52291
  const row = ctx.storage.getSession(input.slug);
50714
52292
  if (!row) throw new Error(`thread ${input.slug} has no session to ask for skills`);
50715
52293
  let skills;
50716
- if (row.backend === "codex") {
52294
+ if (row.backend === "acp") {
52295
+ skills = [];
52296
+ } else if (row.backend === "codex") {
50717
52297
  if (!isAppServerCodexRow(row) || !ctx.codexAppServer) {
50718
52298
  throw new Error("This Codex thread has no app-server session to ask for skills");
50719
52299
  }
@@ -50734,6 +52314,16 @@ ${gapNote}` : input.message;
50734
52314
  const thread = (await ctx.board.snapshot()).threads.find((candidate) => candidate.id === input.slug);
50735
52315
  if (!thread || thread.foreign || thread.kind !== "session") throw new Error(`thread ${input.slug} is not editable`);
50736
52316
  const profRow = ctx.storage.getSession(input.slug);
52317
+ if (profRow?.backend === "acp") {
52318
+ if (acpAgentIdFromModel(input.model) !== acpAgentIdFromModel(profRow.model ?? "")) {
52319
+ throw new Error("An ACP thread's agent cannot change; pick a model of the same agent");
52320
+ }
52321
+ ctx.storage.setProfile(input.slug, input.model, "");
52322
+ const live = ctx.acpBridge ? await ctx.acpBridge.setModel(input.slug, profRow.session_id, acpModelIdFromModel(input.model)) : { applied: false };
52323
+ ctx.board.refresh();
52324
+ return { effect: live.applied ? "applied" : "next-resume" };
52325
+ }
52326
+ if (!input.effort) throw new Error(`effort is required for a ${profRow?.backend ?? "claude"} thread`);
50737
52327
  if (profRow?.backend === "codex") {
50738
52328
  ctx.storage.setProfile(input.slug, input.model, input.effort);
50739
52329
  ctx.board.refresh();
@@ -50806,7 +52396,8 @@ ${gapNote}` : input.message;
50806
52396
  cachedLivenessTerminator,
50807
52397
  ctx.tailer.get(input.slug),
50808
52398
  ctx.codexAppServer,
50809
- ctx.claudeBroker
52399
+ ctx.claudeBroker,
52400
+ ctx.acpBridge
50810
52401
  );
50811
52402
  if (!result.needsConfirmation) ctx.board.refresh();
50812
52403
  return result;
@@ -51400,7 +52991,7 @@ Say which way you went and why in your write-up, so the human can course-correct
51400
52991
  if (t && t.runtime !== "exited") {
51401
52992
  throw new Error("only a stalled or exited session can be dismissed \u2014 archive a live one instead");
51402
52993
  }
51403
- await stopAndForgetRegisteredRuntime(ctx.storage, row, cachedLivenessTerminator, ctx.codexAppServer, ctx.claudeBroker);
52994
+ await stopAndForgetRegisteredRuntime(ctx.storage, row, cachedLivenessTerminator, ctx.codexAppServer, ctx.claudeBroker, ctx.acpBridge);
51404
52995
  ctx.tailer.forget(input.slug);
51405
52996
  ctx.board.refresh();
51406
52997
  }
@@ -51518,7 +53109,7 @@ Say which way you went and why in your write-up, so the human can course-correct
51518
53109
  handler: async ({ input }) => {
51519
53110
  assertLegacyMutationAllowed(input.slug);
51520
53111
  if (input.status === "dismissed") {
51521
- const stopped = await stopRuntimeBySlug(ctx.storage, input.slug, cachedLivenessTerminator, ctx.codexAppServer, ctx.claudeBroker);
53112
+ const stopped = await stopRuntimeBySlug(ctx.storage, input.slug, cachedLivenessTerminator, ctx.codexAppServer, ctx.claudeBroker, ctx.acpBridge);
51522
53113
  if (stopped.row && !ctx.storage.setExitedIfCurrent(
51523
53114
  stopped.row.slug,
51524
53115
  stopped.row.session_id,
@@ -51622,7 +53213,7 @@ Say which way you went and why in your write-up, so the human can course-correct
51622
53213
  killAgent: mutation({
51623
53214
  input: SlugInput,
51624
53215
  handler: async ({ input }) => {
51625
- const stopped = await stopRuntimeBySlug(ctx.storage, input.slug, cachedLivenessTerminator, ctx.codexAppServer, ctx.claudeBroker);
53216
+ const stopped = await stopRuntimeBySlug(ctx.storage, input.slug, cachedLivenessTerminator, ctx.codexAppServer, ctx.claudeBroker, ctx.acpBridge);
51626
53217
  if (stopped.row && !ctx.storage.setExitedIfCurrent(
51627
53218
  stopped.row.slug,
51628
53219
  stopped.row.session_id,
@@ -51641,6 +53232,20 @@ Say which way you went and why in your write-up, so the human can course-correct
51641
53232
  output: external_exports.array(CodexModel),
51642
53233
  handler: async () => readCodexModels()
51643
53234
  }),
53235
+ // The ACP agents Frizz knows how to launch, with `available` for the ones on this machine's PATH.
53236
+ // The composer lists the available ones as `acp:<id>` models (plans/acp-backend.md, decision 9).
53237
+ acpAgents: query({
53238
+ output: external_exports.array(AcpAgent),
53239
+ handler: async () => listAcpAgents(ctx.getSettings().acpAgents).map((a) => ({ id: a.id, label: a.label, command: a.command, available: a.bin !== void 0 }))
53240
+ }),
53241
+ // The models one ACP agent advertises — read by opening a throwaway session in the project dir
53242
+ // (cached in the bridge), so it is a separate query the composer asks only once a model picker
53243
+ // needs it, never on every board load.
53244
+ acpAgentModels: query({
53245
+ input: AcpAgentModelsInput,
53246
+ output: AcpAgentModels,
53247
+ handler: async ({ input }) => ctx.acpBridge ? ctx.acpBridge.agentModels(input.agentId, ctx.project.dir, { refresh: input.refresh === true }) : { agentId: input.agentId, models: [], error: "The ACP bridge is unavailable", probedAt: (/* @__PURE__ */ new Date()).toISOString() }
53248
+ }),
51644
53249
  // Provider subscription quota (5h + weekly rate-limit windows) for the sidebar status bar. Codex
51645
53250
  // reads live from the app-server's `account/rateLimits/read`, falling back to the rollout JSONL
51646
53251
  // frizz already tails; Claude delegates to Claude Code's own non-interactive `/usage` command.
@@ -51690,7 +53295,7 @@ Say which way you went and why in your write-up, so the human can course-correct
51690
53295
  const { state, backend } = ctx.loginUtility.status(input.attemptId);
51691
53296
  const auth = await readAuthSnapshot({ claudeBin: ctx.claudeBin });
51692
53297
  if (state === "exited") ctx.loginUtility.cancel(input.attemptId);
51693
- return { state, auth: auth[backend ?? "claude"] };
53298
+ return { state, auth: backend === "acp" ? "unknown" : auth[backend ?? "claude"] };
51694
53299
  }
51695
53300
  }),
51696
53301
  accountLoginCancel: mutation({
@@ -51859,7 +53464,7 @@ Say which way you went and why in your write-up, so the human can course-correct
51859
53464
  }
51860
53465
  const updated = clearProjectIcon(input.id);
51861
53466
  if (!updated) throw new Error("No such project.");
51862
- return projectCard(updated, !existsSync14(updated.path));
53467
+ return projectCard(updated, !existsSync15(updated.path));
51863
53468
  }
51864
53469
  }),
51865
53470
  /**
@@ -51928,6 +53533,16 @@ Say which way you went and why in your write-up, so the human can course-correct
51928
53533
  output: Settings,
51929
53534
  handler: async ({ input }) => ctx.setSettings(input)
51930
53535
  }),
53536
+ // Context controls live in the new-thread picker. Patch only the chosen field against the
53537
+ // latest settings, synchronously, rather than replacing unrelated settings from a stale drawer.
53538
+ contextWindowSet: mutation({
53539
+ input: external_exports.object({ backend: external_exports.enum(["claude", "codex"]), tokens: external_exports.number().int().positive().nullable() }),
53540
+ output: Settings,
53541
+ handler: async ({ input }) => ctx.setSettings({
53542
+ ...ctx.getSettings(),
53543
+ [input.backend === "claude" ? "autoCompactWindow" : "codexContextWindow"]: input.tokens ?? void 0
53544
+ })
53545
+ }),
51931
53546
  // Clear the stored settings blob so defaults (incl. the shipped default preamble) apply again.
51932
53547
  settingsReset: mutation({
51933
53548
  input: external_exports.object({}),
@@ -52032,6 +53647,7 @@ var init_router5 = __esm({
52032
53647
  init_server();
52033
53648
  init_src();
52034
53649
  init_context();
53650
+ init_acp_agents();
52035
53651
  init_storage();
52036
53652
  init_usage_limit();
52037
53653
  init_board();
@@ -52219,7 +53835,7 @@ var init_local_origin = __esm({
52219
53835
  });
52220
53836
 
52221
53837
  // packages/server/src/compression.ts
52222
- import { brotliCompressSync, gzipSync, constants as constants2 } from "node:zlib";
53838
+ import { brotliCompressSync, gzipSync, constants as constants3 } from "node:zlib";
52223
53839
  function negotiateEncoding(acceptEncoding) {
52224
53840
  if (!acceptEncoding) return null;
52225
53841
  const offers = /* @__PURE__ */ new Map();
@@ -52242,9 +53858,9 @@ function negotiateEncoding(acceptEncoding) {
52242
53858
  function compress(body, encoding) {
52243
53859
  const out = encoding === "br" ? brotliCompressSync(body, {
52244
53860
  params: {
52245
- [constants2.BROTLI_PARAM_QUALITY]: 4,
53861
+ [constants3.BROTLI_PARAM_QUALITY]: 4,
52246
53862
  // Telling brotli the input size lets it size its window instead of guessing.
52247
- [constants2.BROTLI_PARAM_SIZE_HINT]: body.byteLength
53863
+ [constants3.BROTLI_PARAM_SIZE_HINT]: body.byteLength
52248
53864
  }
52249
53865
  }) : gzipSync(body, { level: 6 });
52250
53866
  return Uint8Array.from(out);
@@ -52265,10 +53881,11 @@ var init_compression = __esm({
52265
53881
  });
52266
53882
 
52267
53883
  // packages/server/src/local-image.ts
52268
- import { readFileSync as readFileSync26, realpathSync as realpathSync11, statSync as statSync19 } from "node:fs";
52269
- import { extname as extname2, isAbsolute as isAbsolute6 } from "node:path";
53884
+ import { readFileSync as readFileSync27, realpathSync as realpathSync11, statSync as statSync20 } from "node:fs";
53885
+ import { extname as extname2, isAbsolute as isAbsolute7 } from "node:path";
52270
53886
  function resolveLocalImage(rawPath) {
52271
- if (!rawPath || !isAbsolute6(rawPath)) return { status: 400 };
53887
+ if (rawPath) rawPath = normalizeLocalPath(rawPath);
53888
+ if (!rawPath || !isAbsolute7(rawPath)) return { status: 400 };
52272
53889
  const contentType = IMAGE_CONTENT_TYPE[extname2(rawPath).toLowerCase()];
52273
53890
  if (!contentType) return { status: 400 };
52274
53891
  let real;
@@ -52278,8 +53895,8 @@ function resolveLocalImage(rawPath) {
52278
53895
  return { status: 404 };
52279
53896
  }
52280
53897
  try {
52281
- if (!statSync19(real).isFile()) return { status: 404 };
52282
- return { status: 200, contentType, body: readFileSync26(real) };
53898
+ if (!statSync20(real).isFile()) return { status: 404 };
53899
+ return { status: 200, contentType, body: readFileSync27(real) };
52283
53900
  } catch {
52284
53901
  return { status: 404 };
52285
53902
  }
@@ -52288,6 +53905,7 @@ var IMAGE_CONTENT_TYPE;
52288
53905
  var init_local_image = __esm({
52289
53906
  "packages/server/src/local-image.ts"() {
52290
53907
  "use strict";
53908
+ init_local_path();
52291
53909
  IMAGE_CONTENT_TYPE = {
52292
53910
  ".png": "image/png",
52293
53911
  ".jpg": "image/jpeg",
@@ -52299,9 +53917,9 @@ var init_local_image = __esm({
52299
53917
  });
52300
53918
 
52301
53919
  // packages/server/src/project-icon.ts
52302
- import { readFileSync as readFileSync27, readdirSync as readdirSync10, realpathSync as realpathSync12, statSync as statSync20 } from "node:fs";
53920
+ import { readFileSync as readFileSync28, readdirSync as readdirSync10, realpathSync as realpathSync12, statSync as statSync21 } from "node:fs";
52303
53921
  import { homedir as homedir18 } from "node:os";
52304
- import { basename as basename10, dirname as dirname17, extname as extname3, join as join38, relative as relative2, sep as sep3 } from "node:path";
53922
+ import { basename as basename10, dirname as dirname19, extname as extname3, join as join40, relative as relative2, sep as sep3 } from "node:path";
52305
53923
  function normalizeStem(stem) {
52306
53924
  const sized = stem.toLowerCase().replace(/[@-]\d+(\.\d+)?x(\d+)?$/u, "").replace(/[-_]\d{2,4}$/u, "");
52307
53925
  const normalized = sized.replace(VARIANT_SUFFIX, "");
@@ -52319,7 +53937,7 @@ function sizeScore(dimensions) {
52319
53937
  return Math.max(0, Math.min(40, Math.round(Math.log2(edge / 24) * 8)));
52320
53938
  }
52321
53939
  function under(root, relativeDirectory) {
52322
- return relativeDirectory ? join38(root, relativeDirectory) : root;
53940
+ return relativeDirectory ? join40(root, relativeDirectory) : root;
52323
53941
  }
52324
53942
  function listFiles(directory) {
52325
53943
  try {
@@ -52341,19 +53959,19 @@ function iconDirectories(root) {
52341
53959
  for (const asset of ASSET_DIRECTORIES) directories.push(under(root, asset));
52342
53960
  for (const host of HOST_DIRECTORIES) {
52343
53961
  if (!children2.has(host)) continue;
52344
- for (const asset of ASSET_DIRECTORIES) directories.push(under(join38(root, host), asset));
53962
+ for (const asset of ASSET_DIRECTORIES) directories.push(under(join40(root, host), asset));
52345
53963
  }
52346
53964
  for (const extra of EXTRA_DIRECTORIES) directories.push(under(root, extra));
52347
53965
  for (const container of WORKSPACE_CONTAINERS) {
52348
53966
  if (!children2.has(container)) continue;
52349
53967
  let members;
52350
53968
  try {
52351
- members = readdirSync10(join38(root, container), { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name);
53969
+ members = readdirSync10(join40(root, container), { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name);
52352
53970
  } catch {
52353
53971
  continue;
52354
53972
  }
52355
53973
  for (const member of members) {
52356
- for (const asset of ASSET_DIRECTORIES) directories.push(under(join38(root, container, member), asset));
53974
+ for (const asset of ASSET_DIRECTORIES) directories.push(under(join40(root, container, member), asset));
52357
53975
  }
52358
53976
  }
52359
53977
  return [...new Set(directories)];
@@ -52361,13 +53979,13 @@ function iconDirectories(root) {
52361
53979
  function manifestIcons(manifestPath) {
52362
53980
  let parsed;
52363
53981
  try {
52364
- parsed = JSON.parse(readFileSync27(manifestPath, "utf8"));
53982
+ parsed = JSON.parse(readFileSync28(manifestPath, "utf8"));
52365
53983
  } catch {
52366
53984
  return [];
52367
53985
  }
52368
53986
  if (!Array.isArray(parsed?.icons)) return [];
52369
- const directory = dirname17(manifestPath);
52370
- 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) => join38(directory, src.replace(/[?#].*$/u, "").replace(/^\//u, "")));
53987
+ const directory = dirname19(manifestPath);
53988
+ 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) => join40(directory, src.replace(/[?#].*$/u, "").replace(/^\//u, "")));
52371
53989
  }
52372
53990
  function projectIconCandidates(root) {
52373
53991
  const projectStem = normalizeStem(basename10(root)).normalized;
@@ -52386,10 +54004,10 @@ function projectIconCandidates(root) {
52386
54004
  for (const name of listFiles(directory)) {
52387
54005
  const extension2 = extname3(name).toLowerCase();
52388
54006
  if (MANIFEST_NAMES.includes(name.toLowerCase())) {
52389
- for (const icon of manifestIcons(join38(directory, name))) {
54007
+ for (const icon of manifestIcons(join40(directory, name))) {
52390
54008
  if (MEASURABLE_IMAGE_EXTENSIONS.has(extname3(icon).toLowerCase())) {
52391
54009
  fromManifest.add(icon);
52392
- files.push({ path: icon, directory: dirname17(icon) });
54010
+ files.push({ path: icon, directory: dirname19(icon) });
52393
54011
  }
52394
54012
  }
52395
54013
  continue;
@@ -52397,7 +54015,7 @@ function projectIconCandidates(root) {
52397
54015
  if (!MEASURABLE_IMAGE_EXTENSIONS.has(extension2)) continue;
52398
54016
  const stem = name.slice(0, name.length - extension2.length);
52399
54017
  if (normalizeStem(stem).normalized === "favicon") iconHomes.add(directory);
52400
- files.push({ path: join38(directory, name), directory });
54018
+ files.push({ path: join40(directory, name), directory });
52401
54019
  if (files.length >= MAX_CANDIDATES) break;
52402
54020
  }
52403
54021
  }
@@ -52449,7 +54067,7 @@ function resolveProjectIconResponse(id, options = {}) {
52449
54067
  let real;
52450
54068
  try {
52451
54069
  real = realpathSync12(path3);
52452
- if (!statSync20(real).isFile()) return { status: 404 };
54070
+ if (!statSync21(real).isFile()) return { status: 404 };
52453
54071
  } catch {
52454
54072
  return { status: 404 };
52455
54073
  }
@@ -52462,7 +54080,7 @@ function resolveProjectIconResponse(id, options = {}) {
52462
54080
  });
52463
54081
  if (!roots.some((root) => real === root || real.startsWith(root + sep3))) return { status: 404 };
52464
54082
  try {
52465
- return { status: 200, contentType, body: readFileSync27(real) };
54083
+ return { status: 200, contentType, body: readFileSync28(real) };
52466
54084
  } catch {
52467
54085
  return { status: 404 };
52468
54086
  }
@@ -52574,8 +54192,8 @@ var init_project_icon = __esm({
52574
54192
  });
52575
54193
 
52576
54194
  // packages/server/src/local-visualization.ts
52577
- import { readdirSync as readdirSync11, readFileSync as readFileSync28, realpathSync as realpathSync13, statSync as statSync21 } from "node:fs";
52578
- import { join as join39, sep as sep4 } from "node:path";
54195
+ import { readdirSync as readdirSync11, readFileSync as readFileSync29, realpathSync as realpathSync13, statSync as statSync22 } from "node:fs";
54196
+ import { join as join41, sep as sep4 } from "node:path";
52579
54197
  function children(path3, pattern) {
52580
54198
  try {
52581
54199
  return readdirSync11(path3, { withFileTypes: true }).filter((entry) => entry.isDirectory() && pattern.test(entry.name)).map((entry) => entry.name).sort().reverse();
@@ -52587,7 +54205,7 @@ function isUnder2(path3, root) {
52587
54205
  return path3 === root || path3.startsWith(root.endsWith(sep4) ? root : root + sep4);
52588
54206
  }
52589
54207
  function resolveFragment(projectDir, sessionId, file) {
52590
- const base = join39(projectDir, ".codex", "visualizations");
54208
+ const base = join41(projectDir, ".codex", "visualizations");
52591
54209
  let projectReal;
52592
54210
  let baseReal;
52593
54211
  try {
@@ -52598,15 +54216,15 @@ function resolveFragment(projectDir, sessionId, file) {
52598
54216
  }
52599
54217
  if (!isUnder2(baseReal, projectReal)) return null;
52600
54218
  for (const year of children(base, YEAR_PART)) {
52601
- const yearDir = join39(base, year);
54219
+ const yearDir = join41(base, year);
52602
54220
  for (const month of children(yearDir, DATE_PART)) {
52603
- const monthDir = join39(yearDir, month);
54221
+ const monthDir = join41(yearDir, month);
52604
54222
  for (const day of children(monthDir, DATE_PART)) {
52605
- const sessionRoot = join39(baseReal, year, month, day, sessionId);
52606
- const candidate = join39(monthDir, day, sessionId, file);
54223
+ const sessionRoot = join41(baseReal, year, month, day, sessionId);
54224
+ const candidate = join41(monthDir, day, sessionId, file);
52607
54225
  try {
52608
54226
  const real = realpathSync13(candidate);
52609
- if (isUnder2(real, sessionRoot) && statSync21(real).isFile()) return real;
54227
+ if (isUnder2(real, sessionRoot) && statSync22(real).isFile()) return real;
52610
54228
  } catch {
52611
54229
  }
52612
54230
  }
@@ -52687,9 +54305,9 @@ function resolveLocalVisualization(projectDir, sessionId, file) {
52687
54305
  if (!path3) return { status: 404 };
52688
54306
  let fragment;
52689
54307
  try {
52690
- const size = statSync21(path3).size;
54308
+ const size = statSync22(path3).size;
52691
54309
  if (size > MAX_FRAGMENT_BYTES) return { status: 413 };
52692
- fragment = readFileSync28(path3, "utf8");
54310
+ fragment = readFileSync29(path3, "utf8");
52693
54311
  } catch {
52694
54312
  return { status: 404 };
52695
54313
  }
@@ -52731,8 +54349,8 @@ var init_local_visualization = __esm({
52731
54349
  });
52732
54350
 
52733
54351
  // packages/server/src/app.ts
52734
- import { mkdirSync as mkdirSync17, writeFileSync as writeFileSync14 } from "node:fs";
52735
- import { join as join40 } from "node:path";
54352
+ import { mkdirSync as mkdirSync18, writeFileSync as writeFileSync14 } from "node:fs";
54353
+ import { join as join42 } from "node:path";
52736
54354
  import { randomUUID as randomUUID17 } from "node:crypto";
52737
54355
  function createApp(ctx, options = {}) {
52738
54356
  const app = new Hono2();
@@ -52836,10 +54454,10 @@ function createApp(ctx, options = {}) {
52836
54454
  if (typeof body.data !== "string" || body.data.length > ATTACHMENT_MAX_BASE64_CHARS) return c.json({ error: "bad payload" }, 400);
52837
54455
  const ext = `.${attachmentExtension(name)}`;
52838
54456
  const buf = Buffer.from(body.data, "base64");
52839
- const dir = join40(ctx.project.stateDir, "attachments");
52840
- mkdirSync17(dir, { recursive: true });
54457
+ const dir = join42(ctx.project.stateDir, "attachments");
54458
+ mkdirSync18(dir, { recursive: true });
52841
54459
  const base = name.replace(/\.[^.]+$/, "").replace(/[^a-zA-Z0-9_-]+/g, "-").slice(0, 40) || "file";
52842
- const path3 = join40(dir, `${Date.now()}-${randomUUID17().slice(0, 8)}-${base}${ext}`);
54460
+ const path3 = join42(dir, `${Date.now()}-${randomUUID17().slice(0, 8)}-${base}${ext}`);
52843
54461
  writeFileSync14(path3, buf);
52844
54462
  return c.json({ path: path3 });
52845
54463
  });
@@ -53305,10 +54923,19 @@ function isWsPath(url) {
53305
54923
  }
53306
54924
  function makeTranscriptReader(project, storage, backendFor, lifecycleFor, peerNameFor, ownerGone) {
53307
54925
  return (slug) => {
53308
- const messages = readThreadTranscript(project, storage, slug, backendFor);
53309
- const named = peerNameFor ? projectTranscriptPeerNames(messages, (taskId) => peerNameFor(slug, taskId)) : messages;
54926
+ const page = readLatestThreadTranscriptPage(project, storage, slug, backendFor, { editedFiles: false });
54927
+ const named = peerNameFor ? projectTranscriptPeerNames(page.messages, (taskId) => peerNameFor(slug, taskId)) : page.messages;
53310
54928
  const projected = lifecycleFor ? projectTranscriptAgentLifecycles(named, (id) => lifecycleFor(slug, id)) : named;
53311
- return projectRetiredBackgroundOps(projected, retiredOpsFor(storage, slug), ownerGone?.(slug) ?? false);
54929
+ const messages = projectRetiredBackgroundOps(projected, retiredOpsFor(storage, slug), ownerGone?.(slug) ?? false);
54930
+ return {
54931
+ messages,
54932
+ page: {
54933
+ beforeCursor: page.beforeCursor,
54934
+ hasEarlier: page.hasEarlier,
54935
+ reachedTurnBoundary: page.reachedTurnBoundary,
54936
+ transcriptKey: page.transcriptKey
54937
+ }
54938
+ };
53312
54939
  };
53313
54940
  }
53314
54941
  function createAppSocketServer(deps) {
@@ -53589,9 +55216,9 @@ function createAppSocketServer(deps) {
53589
55216
  if (cached2) return { kind: "ready", snapshot: cached2 };
53590
55217
  const reservation = reserveTranscriptRead(origin);
53591
55218
  if (reservation.kind === "limited") return reservation;
53592
- const messages = readTranscript2(slug);
53593
- if (!messages) return { kind: "unavailable" };
53594
- const frame = encodeMsg({ t: "transcript", slug, messages });
55219
+ const read = readTranscript2(slug);
55220
+ if (!read) return { kind: "unavailable" };
55221
+ const frame = encodeMsg(read.page ? { t: "transcript", slug, messages: read.messages, page: read.page } : { t: "transcript", slug, messages: read.messages });
53595
55222
  if (!frame) return { kind: "unavailable" };
53596
55223
  if (frame.bytes > maxLogicalFrameBytes) return { kind: "oversized", actualBytes: frame.bytes };
53597
55224
  const snapshot = { frame, sig: frameSignature(frame), weight: snapshotWeight(frame) };
@@ -54151,11 +55778,11 @@ var init_app_socket = __esm({
54151
55778
  });
54152
55779
 
54153
55780
  // packages/server/src/local-file-watch.ts
54154
- import { statSync as statSync22, watch } from "node:fs";
54155
- import { basename as basename11, dirname as dirname18 } from "node:path";
55781
+ import { statSync as statSync23, watch } from "node:fs";
55782
+ import { basename as basename11, dirname as dirname20 } from "node:path";
54156
55783
  function stampOf(path3) {
54157
55784
  try {
54158
- const s = statSync22(path3, { bigint: true });
55785
+ const s = statSync23(path3, { bigint: true });
54159
55786
  return `${s.ino}:${s.size}:${s.mtimeNs}`;
54160
55787
  } catch {
54161
55788
  return "gone";
@@ -54175,7 +55802,7 @@ function watchLocalFile(path3, onChange) {
54175
55802
  let entry = entries.get(path3);
54176
55803
  if (!entry) {
54177
55804
  const name = basename11(path3);
54178
- const watcher2 = watch(dirname18(path3), { persistent: false }, (_event, changed) => {
55805
+ const watcher2 = watch(dirname20(path3), { persistent: false }, (_event, changed) => {
54179
55806
  if (changed !== null && changed !== void 0 && changed.toString() !== name) return;
54180
55807
  const current = entries.get(path3);
54181
55808
  if (!current) return;
@@ -54211,10 +55838,10 @@ var init_local_file_watch = __esm({
54211
55838
  });
54212
55839
 
54213
55840
  // packages/server/src/boot-progress.ts
54214
- import { mkdirSync as mkdirSync18, readFileSync as readFileSync29, renameSync as renameSync10, rmSync as rmSync11, writeFileSync as writeFileSync15 } from "node:fs";
54215
- import { basename as basename12, dirname as dirname19, join as join41 } from "node:path";
55841
+ import { mkdirSync as mkdirSync19, readFileSync as readFileSync30, renameSync as renameSync10, rmSync as rmSync11, writeFileSync as writeFileSync15 } from "node:fs";
55842
+ import { basename as basename12, dirname as dirname21, join as join43 } from "node:path";
54216
55843
  function bootProgressPath(stateDir) {
54217
- return join41(stateDir, BOOT_PROGRESS_NAME);
55844
+ return join43(stateDir, BOOT_PROGRESS_NAME);
54218
55845
  }
54219
55846
  function createBootProgressPublisher(stateDir, minIntervalMs = 200) {
54220
55847
  if (!stateDir) {
@@ -54234,8 +55861,8 @@ function createBootProgressPublisher(stateDir, minIntervalMs = 200) {
54234
55861
  lastAt = now;
54235
55862
  step++;
54236
55863
  try {
54237
- mkdirSync18(dirname19(path3), { recursive: true, mode: 448 });
54238
- const temp = join41(dirname19(path3), `.${basename12(path3)}.${process.pid}.tmp`);
55864
+ mkdirSync19(dirname21(path3), { recursive: true, mode: 448 });
55865
+ const temp = join43(dirname21(path3), `.${basename12(path3)}.${process.pid}.tmp`);
54239
55866
  writeFileSync15(temp, `${JSON.stringify({ pid: process.pid, step, phase, at: new Date(now).toISOString() })}
54240
55867
  `, "utf8");
54241
55868
  renameSync10(temp, path3);
@@ -54251,22 +55878,23 @@ function createBootProgressPublisher(stateDir, minIntervalMs = 200) {
54251
55878
  }
54252
55879
  });
54253
55880
  }
54254
- var BOOT_PROGRESS_NAME;
55881
+ var BOOT_PROGRESS_NAME, BOOT_HARD_TIMEOUT_MS;
54255
55882
  var init_boot_progress = __esm({
54256
55883
  "packages/server/src/boot-progress.ts"() {
54257
55884
  "use strict";
54258
55885
  BOOT_PROGRESS_NAME = "boot.progress";
55886
+ BOOT_HARD_TIMEOUT_MS = 10 * 6e4;
54259
55887
  }
54260
55888
  });
54261
55889
 
54262
55890
  // packages/server/src/runtime-lease.ts
54263
- import { existsSync as existsSync15, mkdirSync as mkdirSync19, readdirSync as readdirSync12, readFileSync as readFileSync30, unlinkSync as unlinkSync6, writeFileSync as writeFileSync16 } from "node:fs";
54264
- import { dirname as dirname20, join as join42 } from "node:path";
55891
+ import { existsSync as existsSync16, mkdirSync as mkdirSync20, readdirSync as readdirSync12, readFileSync as readFileSync31, unlinkSync as unlinkSync6, writeFileSync as writeFileSync16 } from "node:fs";
55892
+ import { dirname as dirname22, join as join44 } from "node:path";
54265
55893
  function runtimeVersionDir(bin) {
54266
- let dir = dirname20(bin);
55894
+ let dir = dirname22(bin);
54267
55895
  for (let step = 0; step < MAX_ASCENT; step++) {
54268
- if (existsSync15(join42(dir, PROVISIONED_MARKER))) return dir;
54269
- const parent = dirname20(dir);
55896
+ if (existsSync16(join44(dir, PROVISIONED_MARKER))) return dir;
55897
+ const parent = dirname22(dir);
54270
55898
  if (parent === dir) return void 0;
54271
55899
  dir = parent;
54272
55900
  }
@@ -54284,9 +55912,9 @@ function leaseRuntime(bin, role) {
54284
55912
  const versionDir = runtimeVersionDir(bin);
54285
55913
  if (!versionDir) return () => {
54286
55914
  };
54287
- const path3 = join42(versionDir, LEASES_DIR, `${process.pid}.json`);
55915
+ const path3 = join44(versionDir, LEASES_DIR, `${process.pid}.json`);
54288
55916
  try {
54289
- mkdirSync19(dirname20(path3), { recursive: true });
55917
+ mkdirSync20(dirname22(path3), { recursive: true });
54290
55918
  const lease = { pid: process.pid, role, bin, at: (/* @__PURE__ */ new Date()).toISOString() };
54291
55919
  writeFileSync16(path3, JSON.stringify(lease));
54292
55920
  } catch {
@@ -54301,7 +55929,7 @@ function leaseRuntime(bin, role) {
54301
55929
  };
54302
55930
  }
54303
55931
  function liveRuntimeLeases(versionDir) {
54304
- const dir = join42(versionDir, LEASES_DIR);
55932
+ const dir = join44(versionDir, LEASES_DIR);
54305
55933
  let names;
54306
55934
  try {
54307
55935
  names = readdirSync12(dir);
@@ -54311,10 +55939,10 @@ function liveRuntimeLeases(versionDir) {
54311
55939
  const live = [];
54312
55940
  for (const name of names) {
54313
55941
  if (!name.endsWith(".json")) continue;
54314
- const path3 = join42(dir, name);
55942
+ const path3 = join44(dir, name);
54315
55943
  let lease;
54316
55944
  try {
54317
- lease = JSON.parse(readFileSync30(path3, "utf8"));
55945
+ lease = JSON.parse(readFileSync31(path3, "utf8"));
54318
55946
  } catch {
54319
55947
  continue;
54320
55948
  }
@@ -54345,25 +55973,25 @@ import { createHash as createHash15 } from "node:crypto";
54345
55973
  import {
54346
55974
  chmodSync as chmodSync2,
54347
55975
  closeSync as closeSync16,
54348
- existsSync as existsSync16,
54349
- mkdirSync as mkdirSync20,
55976
+ existsSync as existsSync17,
55977
+ mkdirSync as mkdirSync21,
54350
55978
  openSync as openSync17,
54351
- readFileSync as readFileSync31,
55979
+ readFileSync as readFileSync32,
54352
55980
  readdirSync as readdirSync13,
54353
55981
  renameSync as renameSync11,
54354
55982
  rmSync as rmSync12,
54355
- statSync as statSync23,
55983
+ statSync as statSync24,
54356
55984
  writeFileSync as writeFileSync17,
54357
55985
  writeSync as writeSync2
54358
55986
  } from "node:fs";
54359
- import { dirname as dirname21, isAbsolute as isAbsolute7, join as join43, normalize, relative as relative3, sep as sep5 } from "node:path";
55987
+ import { dirname as dirname23, isAbsolute as isAbsolute8, join as join45, normalize, relative as relative3, sep as sep5 } from "node:path";
54360
55988
  import { Readable, Transform, Writable as Writable3 } from "node:stream";
54361
55989
  import { pipeline } from "node:stream/promises";
54362
55990
  import { createGunzip } from "node:zlib";
54363
55991
  function runtimesRoot(env = process.env) {
54364
55992
  const override = env[RUNTIMES_DIR_ENV];
54365
- if (override && isAbsolute7(override)) return override;
54366
- return join43(frizzRoots().cache, "runtimes");
55993
+ if (override && isAbsolute8(override)) return override;
55994
+ return join45(frizzRoots().cache, "runtimes");
54367
55995
  }
54368
55996
  function isMusl() {
54369
55997
  if (process.platform !== "linux") return false;
@@ -54380,16 +56008,16 @@ function runtimeCoordinates(backend, platform3 = process.platform, arch = proces
54380
56008
  return { pkg: "@openai/codex", packageVersion: `${CODEX_VERSION}-${platform3}-${arch}`, label: CODEX_VERSION, binary: `codex${exe}` };
54381
56009
  }
54382
56010
  function provisionedBinary(backend, root, label) {
54383
- const dir = join43(root, backend, label);
56011
+ const dir = join45(root, backend, label);
54384
56012
  let marker;
54385
56013
  try {
54386
- marker = JSON.parse(readFileSync31(join43(dir, MARKER), "utf8"));
56014
+ marker = JSON.parse(readFileSync32(join45(dir, MARKER), "utf8"));
54387
56015
  } catch {
54388
56016
  return void 0;
54389
56017
  }
54390
56018
  if (typeof marker.binary !== "string" || marker.label !== label) return void 0;
54391
- const bin = join43(dir, marker.binary);
54392
- return existsSync16(bin) ? bin : void 0;
56019
+ const bin = join45(dir, marker.binary);
56020
+ return existsSync17(bin) ? bin : void 0;
54393
56021
  }
54394
56022
  function octal(buffer, offset, length) {
54395
56023
  const text2 = buffer.toString("latin1", offset, offset + length).replace(/\0.*$/su, "").trim();
@@ -54402,18 +56030,18 @@ function safeEntryPath(dest, name) {
54402
56030
  const parts = name.split("/").filter((part) => part.length > 0);
54403
56031
  if (parts.length < 2) return void 0;
54404
56032
  const rel = normalize(parts.slice(1).join(sep5));
54405
- if (isAbsolute7(rel) || rel === ".." || rel.startsWith(`..${sep5}`)) return void 0;
54406
- const full = join43(dest, rel);
56033
+ if (isAbsolute8(rel) || rel === ".." || rel.startsWith(`..${sep5}`)) return void 0;
56034
+ const full = join45(dest, rel);
54407
56035
  const back = relative3(dest, full);
54408
- if (back.startsWith("..") || isAbsolute7(back)) return void 0;
56036
+ if (back.startsWith("..") || isAbsolute8(back)) return void 0;
54409
56037
  return full;
54410
56038
  }
54411
56039
  function locateBinary(dir, backend, binary) {
54412
56040
  if (backend === "claude") {
54413
- const direct = join43(dir, binary);
54414
- return existsSync16(direct) ? direct : void 0;
56041
+ const direct = join45(dir, binary);
56042
+ return existsSync17(direct) ? direct : void 0;
54415
56043
  }
54416
- const vendor = join43(dir, "vendor");
56044
+ const vendor = join45(dir, "vendor");
54417
56045
  let triples;
54418
56046
  try {
54419
56047
  triples = readdirSync13(vendor);
@@ -54421,8 +56049,8 @@ function locateBinary(dir, backend, binary) {
54421
56049
  return void 0;
54422
56050
  }
54423
56051
  for (const triple of triples) {
54424
- const candidate = join43(vendor, triple, "bin", binary);
54425
- if (existsSync16(candidate)) return candidate;
56052
+ const candidate = join45(vendor, triple, "bin", binary);
56053
+ if (existsSync17(candidate)) return candidate;
54426
56054
  }
54427
56055
  return void 0;
54428
56056
  }
@@ -54437,12 +56065,12 @@ async function provisionRuntime(backend, options) {
54437
56065
  const registry = options.registry ?? REGISTRY;
54438
56066
  const progress = options.onProgress ?? (() => {
54439
56067
  });
54440
- const backendDir = join43(root, backend);
54441
- const finalDir = join43(backendDir, coordinates.label);
54442
- const partial = join43(backendDir, `.partial-${coordinates.label}-${process.pid}`);
54443
- mkdirSync20(backendDir, { recursive: true });
56068
+ const backendDir = join45(root, backend);
56069
+ const finalDir = join45(backendDir, coordinates.label);
56070
+ const partial = join45(backendDir, `.partial-${coordinates.label}-${process.pid}`);
56071
+ mkdirSync21(backendDir, { recursive: true });
54444
56072
  rmSync12(partial, { recursive: true, force: true });
54445
- mkdirSync20(partial);
56073
+ mkdirSync21(partial);
54446
56074
  try {
54447
56075
  const manifestUrl = `${registry}/${coordinates.pkg.replace("/", "%2F")}/${coordinates.packageVersion}`;
54448
56076
  progress(`resolving ${coordinates.pkg}@${coordinates.packageVersion}`);
@@ -54486,7 +56114,7 @@ async function provisionRuntime(backend, options) {
54486
56114
  binary: relative3(partial, bin),
54487
56115
  at: (/* @__PURE__ */ new Date()).toISOString()
54488
56116
  };
54489
- writeFileSync17(join43(partial, MARKER), `${JSON.stringify(marker, null, 2)}
56117
+ writeFileSync17(join45(partial, MARKER), `${JSON.stringify(marker, null, 2)}
54490
56118
  `, "utf8");
54491
56119
  const rename2 = options.rename ?? renameSync11;
54492
56120
  try {
@@ -54495,7 +56123,7 @@ async function provisionRuntime(backend, options) {
54495
56123
  rename2(partial, finalDir);
54496
56124
  break;
54497
56125
  } catch (err) {
54498
- if (attempt >= RENAME_ATTEMPTS || !TRANSIENT_HANDLE_CODES.has(errorCode4(err) ?? "") || existsSync16(finalDir)) throw err;
56126
+ if (attempt >= RENAME_ATTEMPTS || !TRANSIENT_HANDLE_CODES.has(errorCode4(err) ?? "") || existsSync17(finalDir)) throw err;
54499
56127
  await new Promise((resolve12) => setTimeout(resolve12, options.retryDelayMs ?? RENAME_RETRY_DELAY_MS));
54500
56128
  }
54501
56129
  }
@@ -54506,7 +56134,7 @@ async function provisionRuntime(backend, options) {
54506
56134
  return { bin: theirs, label: coordinates.label, fetched: false };
54507
56135
  }
54508
56136
  progress("ready");
54509
- return { bin: join43(finalDir, marker.binary), label: coordinates.label, fetched: true };
56137
+ return { bin: join45(finalDir, marker.binary), label: coordinates.label, fetched: true };
54510
56138
  } catch (err) {
54511
56139
  rmSync12(partial, { recursive: true, force: true });
54512
56140
  throw err;
@@ -54514,7 +56142,7 @@ async function provisionRuntime(backend, options) {
54514
56142
  }
54515
56143
  function sweepRuntimes(backend, root, keepLabel, now = Date.now(), deps = {}) {
54516
56144
  const rm2 = deps.rm ?? defaultSweepRm;
54517
- const backendDir = join43(root, backend);
56145
+ const backendDir = join45(root, backend);
54518
56146
  let entries2;
54519
56147
  try {
54520
56148
  entries2 = readdirSync13(backendDir);
@@ -54524,10 +56152,10 @@ function sweepRuntimes(backend, root, keepLabel, now = Date.now(), deps = {}) {
54524
56152
  const sweep = { removed: [], kept: [] };
54525
56153
  for (const entry of entries2) {
54526
56154
  if (entry === keepLabel) continue;
54527
- const full = join43(backendDir, entry);
56155
+ const full = join45(backendDir, entry);
54528
56156
  if (entry.startsWith(".partial-")) {
54529
56157
  try {
54530
- if (now - statSync23(full).mtimeMs < 24 * 60 * 60 * 1e3) continue;
56158
+ if (now - statSync24(full).mtimeMs < 24 * 60 * 60 * 1e3) continue;
54531
56159
  } catch {
54532
56160
  continue;
54533
56161
  }
@@ -54617,8 +56245,8 @@ var init_runtimes = __esm({
54617
56245
  init_codex_app_server();
54618
56246
  init_frizz_paths();
54619
56247
  init_runtime_lease();
54620
- CLAUDE_AGENT_SDK_VERSION = "0.3.273";
54621
- CLAUDE_CODE_VERSION = "2.1.273";
56248
+ CLAUDE_AGENT_SDK_VERSION = "0.3.275";
56249
+ CLAUDE_CODE_VERSION = "2.1.275";
54622
56250
  CODEX_VERSION = CODEX_APP_SERVER_SUPPORTED_VERSION;
54623
56251
  RUNTIMES_MODE_ENV = "FRIZZ_RUNTIMES";
54624
56252
  RUNTIMES_DIR_ENV = "FRIZZ_RUNTIMES_DIR";
@@ -54707,7 +56335,7 @@ var init_runtimes = __esm({
54707
56335
  return;
54708
56336
  case "5": {
54709
56337
  const dir = safeEntryPath(this.dest, name);
54710
- if (dir) mkdirSync20(dir, { recursive: true });
56338
+ if (dir) mkdirSync21(dir, { recursive: true });
54711
56339
  return;
54712
56340
  }
54713
56341
  case "0":
@@ -54715,7 +56343,7 @@ var init_runtimes = __esm({
54715
56343
  case "7": {
54716
56344
  const file = safeEntryPath(this.dest, name);
54717
56345
  if (!file) return;
54718
- mkdirSync20(dirname21(file), { recursive: true });
56346
+ mkdirSync21(dirname23(file), { recursive: true });
54719
56347
  this.fd = openSync17(file, "w", octal(header, 100, 8) & 511 || 420);
54720
56348
  this.written.push(file);
54721
56349
  return;
@@ -55292,18 +56920,18 @@ var init_legacy_project_db = __esm({
55292
56920
  });
55293
56921
 
55294
56922
  // packages/server/src/frizz-db.ts
55295
- import { existsSync as existsSync17, mkdirSync as mkdirSync21, readdirSync as readdirSync14, renameSync as renameSync12, statSync as statSync24 } from "node:fs";
55296
- import { basename as basename13, dirname as dirname22, join as join44 } from "node:path";
56923
+ import { existsSync as existsSync18, mkdirSync as mkdirSync22, readdirSync as readdirSync14, renameSync as renameSync12, statSync as statSync25 } from "node:fs";
56924
+ import { basename as basename13, dirname as dirname24, join as join46 } from "node:path";
55297
56925
  function frizzDatabaseLocation(options) {
55298
56926
  if (options.stateDir !== void 0) {
55299
- const projectsDir = dirname22(options.stateDir);
56927
+ const projectsDir = dirname24(options.stateDir);
55300
56928
  if (basename13(projectsDir) === "projects") {
55301
- return { path: options.path ?? join44(dirname22(projectsDir), "ui.db"), projectsDir };
56929
+ return { path: options.path ?? join46(dirname24(projectsDir), "ui.db"), projectsDir };
55302
56930
  }
55303
- return { path: options.path ?? join44(options.stateDir, "ui.db"), projectsDir: void 0 };
56931
+ return { path: options.path ?? join46(options.stateDir, "ui.db"), projectsDir: void 0 };
55304
56932
  }
55305
56933
  const data = options.home ? frizzPaths({ home: options.home }).data : frizzRoots().data;
55306
- return { path: options.path ?? join44(data, "ui.db"), projectsDir: join44(data, "projects") };
56934
+ return { path: options.path ?? join46(data, "ui.db"), projectsDir: join46(data, "projects") };
55307
56935
  }
55308
56936
  function ensureFrizzSchema(db) {
55309
56937
  db.exec(`
@@ -55322,7 +56950,7 @@ function ensureFrizzSchema(db) {
55322
56950
  }
55323
56951
  function openFrizzDatabase(options = {}) {
55324
56952
  const { path: path3, projectsDir } = frizzDatabaseLocation(options);
55325
- mkdirSync21(dirname22(path3), { recursive: true });
56953
+ mkdirSync22(dirname24(path3), { recursive: true });
55326
56954
  const db = new sqlite_default(path3);
55327
56955
  db.pragma("busy_timeout = 5000");
55328
56956
  db.pragma("journal_mode = WAL");
@@ -55334,10 +56962,10 @@ function openFrizzDatabase(options = {}) {
55334
56962
  );
55335
56963
  for (const projectId of retiredLegacyProjectIds(projectsDir)) {
55336
56964
  if (alreadyImported.has(projectId)) continue;
55337
- const dir = join44(projectsDir, projectId);
56965
+ const dir = join46(projectsDir, projectId);
55338
56966
  try {
55339
- renameSync12(join44(dir, "ui.db.imported"), join44(dir, "ui.db"));
55340
- db.prepare("INSERT INTO imported_project (project_id, source, rows, imported_at) VALUES (?, ?, 0, ?)").run(projectId, join44(dir, "ui.db"), (/* @__PURE__ */ new Date()).toISOString());
56967
+ renameSync12(join46(dir, "ui.db.imported"), join46(dir, "ui.db"));
56968
+ db.prepare("INSERT INTO imported_project (project_id, source, rows, imported_at) VALUES (?, ?, 0, ?)").run(projectId, join46(dir, "ui.db"), (/* @__PURE__ */ new Date()).toISOString());
55341
56969
  alreadyImported.add(projectId);
55342
56970
  log.info("frizz-db", `${projectId}: restored ui.db.imported to ui.db and recorded it as imported`);
55343
56971
  } catch (error) {
@@ -55347,7 +56975,7 @@ function openFrizzDatabase(options = {}) {
55347
56975
  }
55348
56976
  for (const projectId of legacyProjectIds(projectsDir)) {
55349
56977
  if (alreadyImported.has(projectId)) continue;
55350
- const legacyPath = join44(projectsDir, projectId, "ui.db");
56978
+ const legacyPath = join46(projectsDir, projectId, "ui.db");
55351
56979
  try {
55352
56980
  const rows = importLegacyProject(db, projectId, legacyPath);
55353
56981
  imported.push({ projectId, rows });
@@ -55377,10 +57005,10 @@ function openFrizzDatabase(options = {}) {
55377
57005
  };
55378
57006
  }
55379
57007
  function projectIdsWithFile(projectsDir, file) {
55380
- if (!existsSync17(projectsDir)) return [];
57008
+ if (!existsSync18(projectsDir)) return [];
55381
57009
  return readdirSync14(projectsDir).filter((name) => {
55382
57010
  try {
55383
- return statSync24(join44(projectsDir, name, file)).isFile();
57011
+ return statSync25(join46(projectsDir, name, file)).isFile();
55384
57012
  } catch {
55385
57013
  return false;
55386
57014
  }
@@ -55390,7 +57018,7 @@ function legacyProjectIds(projectsDir) {
55390
57018
  return projectIdsWithFile(projectsDir, "ui.db");
55391
57019
  }
55392
57020
  function retiredLegacyProjectIds(projectsDir) {
55393
- return projectIdsWithFile(projectsDir, "ui.db.imported").filter((id) => !existsSync17(join44(projectsDir, id, "ui.db")));
57021
+ return projectIdsWithFile(projectsDir, "ui.db.imported").filter((id) => !existsSync18(join46(projectsDir, id, "ui.db")));
55394
57022
  }
55395
57023
  function tableColumns(db, schema, table) {
55396
57024
  return db.prepare(`PRAGMA ${schema}.table_info(${table})`).all().map((c) => c.name);
@@ -55546,7 +57174,7 @@ var init_tenant_prime = __esm({
55546
57174
  // packages/server/src/project-teardown.ts
55547
57175
  import { rmSync as rmSync13 } from "node:fs";
55548
57176
  import { homedir as homedir19 } from "node:os";
55549
- import { dirname as dirname23, resolve as resolve10 } from "node:path";
57177
+ import { dirname as dirname25, resolve as resolve10 } from "node:path";
55550
57178
  async function stopProjectWorkers(ctx) {
55551
57179
  let stopped = 0;
55552
57180
  for (const row of ctx.storage.allSessions()) {
@@ -55566,7 +57194,7 @@ function deleteProjectState(projectId, home = homedir19()) {
55566
57194
  if (!/^[\w.-]+$/u.test(projectId) || projectId === "." || projectId === "..") {
55567
57195
  throw new Error(`Refusing to delete state for a malformed project id: ${projectId}`);
55568
57196
  }
55569
- if (dirname23(resolve10(target)) !== resolve10(dirname23(projectStateDir("x", home)))) {
57197
+ if (dirname25(resolve10(target)) !== resolve10(dirname25(projectStateDir("x", home)))) {
55570
57198
  throw new Error(`Refusing to delete ${target} \u2014 it is not a project state directory`);
55571
57199
  }
55572
57200
  rmSync13(target, { recursive: true, force: true });
@@ -55595,11 +57223,11 @@ __export(index_exports, {
55595
57223
  unknownProjectPage: () => unknownProjectPage
55596
57224
  });
55597
57225
  import { createServer } from "node:http";
55598
- import { readFileSync as readFileSync32, existsSync as existsSync18, statSync as statSync25 } from "node:fs";
55599
- import { dirname as dirname24, join as join45, resolve as resolve11, extname as extname4, posix as posix2, sep as sep6 } from "node:path";
57226
+ import { readFileSync as readFileSync33, existsSync as existsSync19, statSync as statSync26 } from "node:fs";
57227
+ import { dirname as dirname26, join as join47, resolve as resolve11, extname as extname4, posix as posix2, sep as sep6 } from "node:path";
55600
57228
  function readServerAddressHolder(path3) {
55601
57229
  try {
55602
- const value = JSON.parse(readFileSync32(path3, "utf8"));
57230
+ const value = JSON.parse(readFileSync33(path3, "utf8"));
55603
57231
  if (typeof value?.pid !== "number") return void 0;
55604
57232
  return { pid: value.pid, port: typeof value.port === "number" ? value.port : -1 };
55605
57233
  } catch {
@@ -55724,12 +57352,12 @@ function staticIsFresh(req, etag, mtimeMs) {
55724
57352
  }
55725
57353
  function serveStatic(distDir, req, res) {
55726
57354
  const rel = posix2.normalize((req.url ?? "/").split("?")[0].replace(/\\/g, "/")).replace(/^(\.\.[/\\])+/, "");
55727
- let file = join45(distDir, rel === "/" ? "index.html" : rel);
55728
- if (!file.startsWith(distDir + sep6)) file = join45(distDir, "index.html");
55729
- if (!existsSync18(file)) file = join45(distDir, "index.html");
57355
+ let file = join47(distDir, rel === "/" ? "index.html" : rel);
57356
+ if (!file.startsWith(distDir + sep6)) file = join47(distDir, "index.html");
57357
+ if (!existsSync19(file)) file = join47(distDir, "index.html");
55730
57358
  try {
55731
- const stats = statSync25(file);
55732
- const cacheControl = file.startsWith(join45(distDir, "assets") + sep6) ? STATIC_CACHE_IMMUTABLE : STATIC_CACHE_REVALIDATE;
57359
+ const stats = statSync26(file);
57360
+ const cacheControl = file.startsWith(join47(distDir, "assets") + sep6) ? STATIC_CACHE_IMMUTABLE : STATIC_CACHE_REVALIDATE;
55733
57361
  const etag = `"${stats.size.toString(16)}-${Math.trunc(stats.mtimeMs).toString(16)}"`;
55734
57362
  const headers = {
55735
57363
  "content-type": MIME[extname4(file)] ?? "application/octet-stream",
@@ -55745,7 +57373,7 @@ function serveStatic(distDir, req, res) {
55745
57373
  res.end();
55746
57374
  return;
55747
57375
  }
55748
- let body = readFileSync32(file);
57376
+ let body = readFileSync33(file);
55749
57377
  const accept = req.headers["accept-encoding"];
55750
57378
  const encoding = negotiateEncoding(Array.isArray(accept) ? accept.join(",") : accept);
55751
57379
  if (encoding && shouldCompress(new Headers({ "content-type": headers["content-type"] }), body.byteLength)) {
@@ -56007,7 +57635,7 @@ async function startServer(opts = {}) {
56007
57635
  if (statusPath && statusIdentity) runtime.removeStatus(statusPath, statusIdentity);
56008
57636
  if (statusPath && statusIdentity) {
56009
57637
  try {
56010
- runtime.removeStatus(serverAddressPathForStateDir(dirname24(statusPath)), statusIdentity);
57638
+ runtime.removeStatus(serverAddressPathForStateDir(dirname26(statusPath)), statusIdentity);
56011
57639
  } catch {
56012
57640
  }
56013
57641
  }
@@ -56145,7 +57773,7 @@ async function startServer(opts = {}) {
56145
57773
  }
56146
57774
  statusPath = serverLockPathFor(ctx.project);
56147
57775
  const webRoot = resolve11(import.meta.dirname, "..", "..", "web");
56148
- const distDir = opts.webDistDir ? resolve11(opts.webDistDir) : join45(webRoot, "dist");
57776
+ const distDir = opts.webDistDir ? resolve11(opts.webDistDir) : join47(webRoot, "dist");
56149
57777
  startupPhase = "Vite";
56150
57778
  if (opts.dev) {
56151
57779
  try {
@@ -56221,7 +57849,7 @@ async function startServer(opts = {}) {
56221
57849
  if (vite) {
56222
57850
  vite.middlewares(req, res, () => {
56223
57851
  try {
56224
- const html = readFileSync32(join45(webRoot, "index.html"), "utf8");
57852
+ const html = readFileSync33(join47(webRoot, "index.html"), "utf8");
56225
57853
  void vite.transformIndexHtml(url, html).then((out) => {
56226
57854
  res.writeHead(200, { "content-type": "text/html" });
56227
57855
  res.end(out);
@@ -56233,7 +57861,7 @@ async function startServer(opts = {}) {
56233
57861
  });
56234
57862
  return;
56235
57863
  }
56236
- if (existsSync18(distDir)) {
57864
+ if (existsSync19(distDir)) {
56237
57865
  serveStatic(distDir, req, res);
56238
57866
  return;
56239
57867
  }
@@ -56431,7 +58059,7 @@ var init_index = __esm({
56431
58059
  }
56432
58060
  };
56433
58061
  isApiUrl = (url) => url === FRIZZ_ROUTE_PREFIX || url.startsWith(`${FRIZZ_ROUTE_PREFIX}/`);
56434
- serverLockPathFor = (project) => join45(project.stateDir, "server.lock");
58062
+ serverLockPathFor = (project) => join47(project.stateDir, "server.lock");
56435
58063
  MIME = {
56436
58064
  ".html": "text/html",
56437
58065
  ".js": "text/javascript",
@@ -56454,8 +58082,8 @@ init_project();
56454
58082
  init_project_launch();
56455
58083
  init_shutdown();
56456
58084
  init_logging();
56457
- import { existsSync as existsSync19 } from "node:fs";
56458
- import { join as join46 } from "node:path";
58085
+ import { existsSync as existsSync20 } from "node:fs";
58086
+ import { join as join48 } from "node:path";
56459
58087
 
56460
58088
  // packages/server/src/native-helper.ts
56461
58089
  import { chmodSync, statSync as statSync4 } from "node:fs";
@@ -56532,9 +58160,9 @@ try {
56532
58160
  ["FRIZZ_SCRIPTS_DIR", process.env.FRIZZ_SCRIPTS_DIR, "index.mjs"],
56533
58161
  ["FRIZZ_WORKER_PLUGIN_DIR", process.env.FRIZZ_WORKER_PLUGIN_DIR, ".claude-plugin/plugin.json"]
56534
58162
  ];
56535
- if (!existsSync19(stableWebDist)) throw new Error("stable artifact launch is missing its verified web directory");
58163
+ if (!existsSync20(stableWebDist)) throw new Error("stable artifact launch is missing its verified web directory");
56536
58164
  for (const [name, directory, requiredFile] of required) {
56537
- if (!directory || !existsSync19(join46(directory, requiredFile)))
58165
+ if (!directory || !existsSync20(join48(directory, requiredFile)))
56538
58166
  throw new Error(`stable artifact launch is missing verified ${name}`);
56539
58167
  }
56540
58168
  }