frayui 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/dist/claude-agent-broker.js +19 -3
  2. package/dist/codex-app-server-daemon.js +1 -1
  3. package/dist/dev-child.js +786 -331
  4. package/dist/frayui.js +1291 -193
  5. package/package.json +8 -7
  6. package/runtime/cc-worker/DECISIONS.md +86 -0
  7. package/runtime/cc-worker/hooks/agent-dispatch.mjs +18 -4
  8. package/runtime/cc-worker/hooks/fence-stop.mjs +290 -0
  9. package/runtime/cc-worker/hooks/hooks.json +2 -1
  10. package/runtime/cc-worker/hooks/scratchpad.mjs +10 -9
  11. package/runtime/cc-worker/hooks/session-seed.mjs +2 -1
  12. package/runtime/cc-worker/skills/gh/SKILL.md +22 -0
  13. package/runtime/cc-worker/skills/handoff/SKILL.md +6 -0
  14. package/web-dist/assets/{TerminalPane-C3CC9EFZ.js → TerminalPane-hMf6ifuS.js} +1 -1
  15. package/web-dist/assets/{abnfDiagram-VRR7QNED-DqX9eyjt.js → abnfDiagram-VRR7QNED-CQj6fwXy.js} +1 -1
  16. package/web-dist/assets/architecture-TIHT7OUA-JX8_jS4I.js +1 -0
  17. package/web-dist/assets/{architectureDiagram-ZJ3FMSHR-M8pzerdN.js → architectureDiagram-ZJ3FMSHR-CoE5ct03.js} +1 -1
  18. package/web-dist/assets/{blockDiagram-677ZJIJ3-DqR6Y2LC.js → blockDiagram-677ZJIJ3-CEudy3zd.js} +1 -1
  19. package/web-dist/assets/{c4Diagram-LMCZKHZV-BVYFjsdW.js → c4Diagram-LMCZKHZV-ChW62IOH.js} +1 -1
  20. package/web-dist/assets/channel-y3u1yiAC.js +1 -0
  21. package/web-dist/assets/{chunk-32BRIVSS-DFjDHOoM.js → chunk-32BRIVSS-Bh-A2le2.js} +1 -1
  22. package/web-dist/assets/{chunk-52WLFC77-CgT8QhTR.js → chunk-52WLFC77-7Gia-F6f.js} +1 -1
  23. package/web-dist/assets/{chunk-C7G6YPKG-CzJ6LiOY.js → chunk-C7G6YPKG-C0MCv0Ym.js} +1 -1
  24. package/web-dist/assets/{chunk-EX3LRPZG-BxLQFQHC.js → chunk-EX3LRPZG-DPRO7Ss0.js} +1 -1
  25. package/web-dist/assets/{chunk-FWX5IMBZ-Dph1CDtO.js → chunk-FWX5IMBZ-Cod9MA7c.js} +2 -2
  26. package/web-dist/assets/{chunk-HOUHSVGY-DkAureev.js → chunk-HOUHSVGY-D1oHrXQx.js} +1 -1
  27. package/web-dist/assets/{chunk-ICXQ74PX-Cz4NMI__.js → chunk-ICXQ74PX-DBLMA0ZN.js} +1 -1
  28. package/web-dist/assets/{chunk-MOJQB5TN-CsLU1DoU.js → chunk-MOJQB5TN-jiLvxNvx.js} +1 -1
  29. package/web-dist/assets/{chunk-OGEWGWER-Aylt7fzg.js → chunk-OGEWGWER-w1RtF7G0.js} +1 -1
  30. package/web-dist/assets/{chunk-PUDLZKDR-BFErObxQ.js → chunk-PUDLZKDR-DQlFMKm5.js} +1 -1
  31. package/web-dist/assets/{chunk-Q4XR5HBZ-Bv6INKwj.js → chunk-Q4XR5HBZ-CfD6vdr0.js} +1 -1
  32. package/web-dist/assets/{chunk-V7JOEXUC-DfztwFA4.js → chunk-V7JOEXUC-BRB1iO1P.js} +1 -1
  33. package/web-dist/assets/{chunk-VAUOI2AC-B0f7ZK3T.js → chunk-VAUOI2AC-CCeJEpxW.js} +1 -1
  34. package/web-dist/assets/{chunk-VR4S4FIN-G-Gcy_AM.js → chunk-VR4S4FIN-DfPRj9a1.js} +1 -1
  35. package/web-dist/assets/{chunk-WYO6CB5R-CMp0iowL.js → chunk-WYO6CB5R-BWd84n6L.js} +1 -1
  36. package/web-dist/assets/{chunk-ZGVPDNZ5-CSWMiX8t.js → chunk-ZGVPDNZ5-Bm-3TA9N.js} +1 -1
  37. package/web-dist/assets/classDiagram-OUVF2IWQ-BL8mggXA.js +1 -0
  38. package/web-dist/assets/classDiagram-v2-EOCWNBFH-BL8mggXA.js +1 -0
  39. package/web-dist/assets/{cynefin-VYW2F7L2-CUMD23BD.js → cynefin-VYW2F7L2-BylEqg5y.js} +1 -1
  40. package/web-dist/assets/{cynefinDiagram-TSTJHNR4-fZTm6GyW.js → cynefinDiagram-TSTJHNR4-CRfGS7Ou.js} +1 -1
  41. package/web-dist/assets/{dagre-VKFMJZFB-CUCi5JnH.js → dagre-VKFMJZFB-CNeTu13Z.js} +1 -1
  42. package/web-dist/assets/{diagram-FQU43EPY-C87Fqvwa.js → diagram-FQU43EPY-BYZKetkJ.js} +1 -1
  43. package/web-dist/assets/{diagram-G47NLZAW-CDxQ-BXA.js → diagram-G47NLZAW-DSOzyl5U.js} +1 -1
  44. package/web-dist/assets/{diagram-NH7WQ7WH-Uceffn93.js → diagram-NH7WQ7WH-DFZCvfJN.js} +1 -1
  45. package/web-dist/assets/{diagram-OA4YK3LP-CSSp0Fa1.js → diagram-OA4YK3LP-m0qY8Zlg.js} +1 -1
  46. package/web-dist/assets/{diagram-WEI45ONY-DS5sAZPp.js → diagram-WEI45ONY-Cg7Qz-0T.js} +1 -1
  47. package/web-dist/assets/{ebnfDiagram-CCIWWBDH-aM331oVj.js → ebnfDiagram-CCIWWBDH-DGvTjoXK.js} +1 -1
  48. package/web-dist/assets/{erDiagram-Q63AITRT-d0i1kydx.js → erDiagram-Q63AITRT-BFqTK5TB.js} +1 -1
  49. package/web-dist/assets/eventmodeling-45OFAUF4-BaCHCepY.js +1 -0
  50. package/web-dist/assets/flowDiagram-23GEKE2U-Cjl6weoT.js +1 -0
  51. package/web-dist/assets/{ganttDiagram-NO4QXBWP-Cd0OpzcB.js → ganttDiagram-NO4QXBWP-KbR9IE0O.js} +1 -1
  52. package/web-dist/assets/{gitGraph-TEB2WS4Q-BeI6iDaZ.js → gitGraph-TEB2WS4Q-hCbzZO_l.js} +1 -1
  53. package/web-dist/assets/{gitGraphDiagram-IHSO6WYX-QvePTKQE.js → gitGraphDiagram-IHSO6WYX-CNPRTwQG.js} +1 -1
  54. package/web-dist/assets/index-BUY5ijyI.css +1 -0
  55. package/web-dist/assets/{index-CJ-cmbhi.js → index-Bj6YWd3g.js} +68 -68
  56. package/web-dist/assets/{info-DKCQHKI2-WwdGk87J.js → info-DKCQHKI2-Zd_TXMfr.js} +1 -1
  57. package/web-dist/assets/{infoDiagram-FWYZ7A6U-C3ARX7jm.js → infoDiagram-FWYZ7A6U-DQ9zmVhJ.js} +1 -1
  58. package/web-dist/assets/{ishikawaDiagram-FXEZZL3T-C_zMKLif.js → ishikawaDiagram-FXEZZL3T-BFyJ5E_L.js} +1 -1
  59. package/web-dist/assets/{journeyDiagram-5HDEW3XC-BcIZu4Cm.js → journeyDiagram-5HDEW3XC-BgjbY7_T.js} +1 -1
  60. package/web-dist/assets/{kanban-definition-HUTT4EX6-wwc4iI2z.js → kanban-definition-HUTT4EX6-mpUEl8xx.js} +1 -1
  61. package/web-dist/assets/{line-BCQpqxAq.js → line-CM_FUYGm.js} +1 -1
  62. package/web-dist/assets/{mermaid-parser.core-8y_8tv09.js → mermaid-parser.core-CTVz2V70.js} +3 -3
  63. package/web-dist/assets/{mermaid.core-gXHP6zd5.js → mermaid.core-BCLNmVvR.js} +3 -3
  64. package/web-dist/assets/{mindmap-definition-LN4V7U3C-DtTZ-e8A.js → mindmap-definition-LN4V7U3C-CgJU-6O7.js} +1 -1
  65. package/web-dist/assets/{packet-7NZHBO7P-DZbgcVcI.js → packet-7NZHBO7P-rkLFb4ZX.js} +1 -1
  66. package/web-dist/assets/{pegDiagram-2B236MQR-DHtrQw0R.js → pegDiagram-2B236MQR-Sy2oYuu1.js} +1 -1
  67. package/web-dist/assets/{pie-RZYD4A2V-CBwiKeCv.js → pie-RZYD4A2V-0yTxNtqZ.js} +1 -1
  68. package/web-dist/assets/{pieDiagram-ENE6RG2P-B-gKjdFc.js → pieDiagram-ENE6RG2P-CQ7YFWhH.js} +1 -1
  69. package/web-dist/assets/{quadrantDiagram-ABIIQ3AL-C3h180Er.js → quadrantDiagram-ABIIQ3AL-ClCTxrOj.js} +1 -1
  70. package/web-dist/assets/{radar-I7S5WNFK-BTAfmGFj.js → radar-I7S5WNFK-JUOW5mdN.js} +1 -1
  71. package/web-dist/assets/{railroad-3IZDKUUU-DjkPg0H7.js → railroad-3IZDKUUU-DJ2IS1Jg.js} +1 -1
  72. package/web-dist/assets/railroad-abnf-AHOZXSZD-CU4HK7Ek.js +1 -0
  73. package/web-dist/assets/railroad-ebnf-EBAXGLYW-CarBAd7R.js +1 -0
  74. package/web-dist/assets/railroad-peg-LSFZ7HO6-ByEhVLyD.js +1 -0
  75. package/web-dist/assets/{railroadDiagram-RFXS5EU6-FVI9rArP.js → railroadDiagram-RFXS5EU6-BNsVGeA3.js} +1 -1
  76. package/web-dist/assets/{requirementDiagram-TGXJPOKE-Cp3pp65R.js → requirementDiagram-TGXJPOKE-BHa3IX0k.js} +1 -1
  77. package/web-dist/assets/{sankeyDiagram-HTMAVEWB-B-EVqI0F.js → sankeyDiagram-HTMAVEWB-CSbsLbtw.js} +1 -1
  78. package/web-dist/assets/{sequenceDiagram-DBY2YBRQ-Bov70rRs.js → sequenceDiagram-DBY2YBRQ-BgVWj1nN.js} +1 -1
  79. package/web-dist/assets/{stateDiagram-2N3HPSRC-BW7fwttw.js → stateDiagram-2N3HPSRC-BKHYNxA3.js} +1 -1
  80. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-C-k0bbcG.js +1 -0
  81. package/web-dist/assets/{swimlanes-5IMT3BWC-BUOdjOgW.js → swimlanes-5IMT3BWC-BluloqBJ.js} +1 -1
  82. package/web-dist/assets/swimlanesDiagram-G3AALYLV-DVjLYZbp.js +8 -0
  83. package/web-dist/assets/{timeline-definition-FHXFAJF6-C20Ho0e9.js → timeline-definition-FHXFAJF6-Bj1WKNgB.js} +1 -1
  84. package/web-dist/assets/{treeView-QDETBFTQ-DHg1NMOK.js → treeView-QDETBFTQ-Dj5T24Qr.js} +1 -1
  85. package/web-dist/assets/{treemap-6X3UGDF4-B-aFFjeG.js → treemap-6X3UGDF4-BPqewrXp.js} +1 -1
  86. package/web-dist/assets/{vennDiagram-L72KCM5P-CEWY6HTS.js → vennDiagram-L72KCM5P-BhggGhQv.js} +1 -1
  87. package/web-dist/assets/{wardley-OPB4EBWU-BpAKElbJ.js → wardley-OPB4EBWU-Di9GWKYt.js} +1 -1
  88. package/web-dist/assets/{wardleyDiagram-EHGQE667-CYer_Ry4.js → wardleyDiagram-EHGQE667-D-mtWLKf.js} +1 -1
  89. package/web-dist/assets/{xychartDiagram-FW5EYKEG-BxMRGqds.js → xychartDiagram-FW5EYKEG-CU0us6D_.js} +1 -1
  90. package/web-dist/index.html +2 -2
  91. package/web-dist/assets/architecture-TIHT7OUA-Dp81OE9d.js +0 -1
  92. package/web-dist/assets/channel-BLJ4T4FR.js +0 -1
  93. package/web-dist/assets/classDiagram-OUVF2IWQ-BQSt_EhF.js +0 -1
  94. package/web-dist/assets/classDiagram-v2-EOCWNBFH-BQSt_EhF.js +0 -1
  95. package/web-dist/assets/eventmodeling-45OFAUF4-qO9fgTC2.js +0 -1
  96. package/web-dist/assets/flowDiagram-23GEKE2U-BUHZX4Ft.js +0 -1
  97. package/web-dist/assets/index-LON0oNCJ.css +0 -1
  98. package/web-dist/assets/railroad-abnf-AHOZXSZD-DlUkYI_B.js +0 -1
  99. package/web-dist/assets/railroad-ebnf-EBAXGLYW-DptyGTmD.js +0 -1
  100. package/web-dist/assets/railroad-peg-LSFZ7HO6-xPHpoacS.js +0 -1
  101. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-BtaoEJDP.js +0 -1
  102. package/web-dist/assets/swimlanesDiagram-G3AALYLV-Bpiu68M1.js +0 -8
package/dist/dev-child.js CHANGED
@@ -1722,6 +1722,186 @@ var init_shutdown = __esm({
1722
1722
  }
1723
1723
  });
1724
1724
 
1725
+ // packages/server/src/logging.ts
1726
+ import {
1727
+ appendFileSync,
1728
+ mkdirSync as mkdirSync5,
1729
+ openSync as openSync4,
1730
+ readdirSync,
1731
+ rmSync as rmSync4,
1732
+ statSync as statSync4,
1733
+ symlinkSync,
1734
+ writeFileSync as writeFileSync4,
1735
+ writeSync
1736
+ } from "node:fs";
1737
+ import { homedir as homedir3 } from "node:os";
1738
+ import { join as join5 } from "node:path";
1739
+ function defaultLogRoot(stateDir, home = homedir3()) {
1740
+ return stateDir ? join5(stateDir, "logs") : join5(home, ".fray", "logs");
1741
+ }
1742
+ function runStamp(at) {
1743
+ const pad = (value) => String(value).padStart(2, "0");
1744
+ return `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())}T${pad(at.getHours())}-${pad(at.getMinutes())}-${pad(at.getSeconds())}`;
1745
+ }
1746
+ function runLogPath(stateDir, at = /* @__PURE__ */ new Date(), pid = process.pid, home = homedir3(), env = process.env) {
1747
+ const name = `fray-${runStamp(at)}-${pid}.log`;
1748
+ const override = env[LOG_PATH_ENV]?.trim();
1749
+ if (override) return override.endsWith(".log") ? override : join5(override, name);
1750
+ return join5(defaultLogRoot(stateDir, home), name);
1751
+ }
1752
+ function latestLogPath(dir) {
1753
+ return join5(dir, "latest.log");
1754
+ }
1755
+ function linkLatest(dir, target) {
1756
+ const link = latestLogPath(dir);
1757
+ try {
1758
+ rmSync4(link, { force: true });
1759
+ symlinkSync(target, link);
1760
+ } catch {
1761
+ try {
1762
+ writeFileSync4(link, `${target}
1763
+ `, { mode: 384 });
1764
+ } catch {
1765
+ }
1766
+ }
1767
+ }
1768
+ function pruneRunLogs(dir, keep = RETAINED_RUNS, days = RETAINED_DAYS, now = Date.now()) {
1769
+ let entries;
1770
+ try {
1771
+ entries = readdirSync(dir).filter((name) => name.endsWith(".log") && name !== "latest.log");
1772
+ } catch {
1773
+ return;
1774
+ }
1775
+ const dated = entries.map((name) => {
1776
+ try {
1777
+ return { name, at: statSync4(join5(dir, name)).mtimeMs };
1778
+ } catch {
1779
+ return { name, at: 0 };
1780
+ }
1781
+ }).sort((a, b) => b.at - a.at);
1782
+ const cutoff = now - days * 24 * 60 * 60 * 1e3;
1783
+ const stale = dated.filter((entry, index) => index >= keep || entry.at < cutoff);
1784
+ for (const entry of stale) {
1785
+ try {
1786
+ rmSync4(join5(dir, entry.name), { force: true });
1787
+ } catch {
1788
+ }
1789
+ }
1790
+ }
1791
+ function formatDiskLine(record) {
1792
+ const at = new Date(record.at);
1793
+ const pad = (value, width = 2) => String(value).padStart(width, "0");
1794
+ const clock = `${pad(at.getHours())}:${pad(at.getMinutes())}:${pad(at.getSeconds())}.${pad(at.getMilliseconds(), 3)}`;
1795
+ return `${clock} ${record.level.toUpperCase().padEnd(5)} ${record.scope.padEnd(12)} ${record.message}
1796
+ `;
1797
+ }
1798
+ function openLogFile(path) {
1799
+ try {
1800
+ mkdirSync5(join5(path, ".."), { recursive: true, mode: 448 });
1801
+ return openSync4(path, "a", 384);
1802
+ } catch {
1803
+ return null;
1804
+ }
1805
+ }
1806
+ function createLogger(options = {}) {
1807
+ const path = options.file === null ? null : options.file ?? runLogPath(options.stateDir);
1808
+ const level = options.level ?? "debug";
1809
+ const now = options.now ?? Date.now;
1810
+ const threshold = LEVEL_ORDER[level];
1811
+ const maxBytes = options.maxBytes ?? MAX_LOG_BYTES;
1812
+ let fd = path === null ? null : openLogFile(path);
1813
+ if (fd !== null && path !== null && options.owner !== false) {
1814
+ const dir = join5(path, "..");
1815
+ pruneRunLogs(dir);
1816
+ linkLatest(dir, path);
1817
+ }
1818
+ let written = 0;
1819
+ const listeners = /* @__PURE__ */ new Set();
1820
+ const emit2 = (recordLevel, scope, message) => {
1821
+ if (LEVEL_ORDER[recordLevel] < threshold) return;
1822
+ const record = { at: now(), level: recordLevel, scope, message };
1823
+ if (fd !== null) {
1824
+ try {
1825
+ const line = formatDiskLine(record);
1826
+ if (written + line.length > maxBytes) {
1827
+ writeSync(fd, `--- log truncated at ${maxBytes} bytes ---
1828
+ `);
1829
+ fd = null;
1830
+ } else {
1831
+ writeSync(fd, line);
1832
+ written += line.length;
1833
+ }
1834
+ } catch {
1835
+ fd = null;
1836
+ }
1837
+ }
1838
+ for (const listener of listeners) {
1839
+ try {
1840
+ listener(record);
1841
+ } catch {
1842
+ }
1843
+ }
1844
+ };
1845
+ return {
1846
+ debug: (scope, message) => emit2("debug", scope, message),
1847
+ info: (scope, message) => emit2("info", scope, message),
1848
+ warn: (scope, message) => emit2("warn", scope, message),
1849
+ error: (scope, message) => emit2("error", scope, message),
1850
+ get file() {
1851
+ return fd === null ? null : path;
1852
+ },
1853
+ onRecord(listener) {
1854
+ listeners.add(listener);
1855
+ return () => listeners.delete(listener);
1856
+ },
1857
+ close() {
1858
+ listeners.clear();
1859
+ fd = null;
1860
+ }
1861
+ };
1862
+ }
1863
+ function formatFeedLine(record) {
1864
+ return formatDiskLine(record).trimEnd();
1865
+ }
1866
+ function attachTerminalMirror(logger, enabled = process.env.FRAY_DEBUG === "1") {
1867
+ if (!enabled) return () => {
1868
+ };
1869
+ return logger.onRecord((record) => process.stderr.write(`${formatFeedLine(record)}
1870
+ `));
1871
+ }
1872
+ function ambientLogger() {
1873
+ if (ambient) return ambient;
1874
+ const inherited = process.env[LOG_FILE_ENV] ?? process.env[LOG_PATH_ENV];
1875
+ const level = process.env[LOG_LEVEL_ENV];
1876
+ const logger = createLogger({
1877
+ ...inherited ? { file: inherited, owner: false } : { file: null },
1878
+ ...level && level in LEVEL_ORDER ? { level } : {}
1879
+ });
1880
+ attachTerminalMirror(logger);
1881
+ ambient = logger;
1882
+ return ambient;
1883
+ }
1884
+ var LEVEL_ORDER, LOG_FILE_ENV, LOG_LEVEL_ENV, LOG_PATH_ENV, RETAINED_RUNS, RETAINED_DAYS, MAX_LOG_BYTES, ambient, log;
1885
+ var init_logging = __esm({
1886
+ "packages/server/src/logging.ts"() {
1887
+ "use strict";
1888
+ LEVEL_ORDER = { debug: 10, info: 20, warn: 30, error: 40 };
1889
+ LOG_FILE_ENV = "FRAY_LOG_FILE";
1890
+ LOG_LEVEL_ENV = "FRAY_LOG_LEVEL";
1891
+ LOG_PATH_ENV = "FRAY_LOG_PATH";
1892
+ RETAINED_RUNS = 20;
1893
+ RETAINED_DAYS = 14;
1894
+ MAX_LOG_BYTES = 32 * 1024 * 1024;
1895
+ ambient = null;
1896
+ log = {
1897
+ debug: (scope, message) => ambientLogger().debug(scope, message),
1898
+ info: (scope, message) => ambientLogger().info(scope, message),
1899
+ warn: (scope, message) => ambientLogger().warn(scope, message),
1900
+ error: (scope, message) => ambientLogger().error(scope, message)
1901
+ };
1902
+ }
1903
+ });
1904
+
1725
1905
  // node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.js
1726
1906
  var util, objectUtil, ZodParsedType, getParsedType;
1727
1907
  var init_util = __esm({
@@ -6151,6 +6331,14 @@ var init_interactions = __esm({
6151
6331
  ...CommonPayload,
6152
6332
  kind: external_exports.literal("permission-approval"),
6153
6333
  permission: InteractionOpaqueId,
6334
+ // The exact text of what is being authorized — a command, a path, a request body. Multi-line and
6335
+ // rendered monospace, because a Bash command is the one thing the operator actually reads and a
6336
+ // single-line label cannot hold a heredoc. Adapters redact and clamp it before it gets here.
6337
+ preview: InteractionPreview.optional(),
6338
+ // A shell prompt to render in FRONT of `preview`, which is how a terminal shows a command and where
6339
+ // the working directory belongs when there is one. Set it only when the preview genuinely IS a shell
6340
+ // command line: a file path or an MCP argument behind a `❯` would be a lie about what runs.
6341
+ promptLabel: plainText(256, 512).optional(),
6154
6342
  resourceLabel: plainText(1024, 2048).optional(),
6155
6343
  workingDirectoryLabel: plainText(1024, 2048).optional(),
6156
6344
  scopeLabel: InteractionDescription.optional(),
@@ -6551,21 +6739,68 @@ function parseAgentMessage(text) {
6551
6739
  function wakeItemTail(item) {
6552
6740
  return `${item.at ? ` at ${item.at}` : ""}${item.url ? `: ${item.url}` : ""}`;
6553
6741
  }
6742
+ function wakeReviewReads({ ref, items }) {
6743
+ const [repo, number] = ref.split("#");
6744
+ const ids = /* @__PURE__ */ new Set();
6745
+ for (const item of items) {
6746
+ const id = /#pullrequestreview-(\d+)$/.exec(item.url ?? "")?.[1];
6747
+ if (id) ids.add(id);
6748
+ }
6749
+ return [...ids].map((id) => `gh api --paginate repos/${repo}/pulls/${number}/reviews/${id}/comments`);
6750
+ }
6751
+ function wakeReviewTail(steer) {
6752
+ const reads = wakeReviewReads(steer);
6753
+ return reads.length ? `
6754
+
6755
+ ${WAKE_REVIEW_LEAD}
6756
+ ${reads.join("\n")}` : "";
6757
+ }
6554
6758
  function formatGithubWakeSteer({ ref, items, omitted }) {
6555
6759
  const icon = items.some((i) => !i.bot) ? "\u{1F464}" : "\u{1F916}";
6760
+ const reviewTail = wakeReviewTail({ ref, items, omitted });
6556
6761
  if (items.length === 1 && omitted === 0) {
6557
6762
  const item = items[0];
6558
6763
  const url = item.url ? `: ${item.url}` : ".";
6559
- return `${icon} New GitHub ${item.label} on ${ref} from @${item.actor}${item.at ? ` at ${item.at}` : ""}. Read that exact ${item.label} \u2014 ${WAKE_SCOPE} \u2014 and continue${url}`;
6764
+ return `${icon} New GitHub ${item.label} on ${ref} from @${item.actor}${item.at ? ` at ${item.at}` : ""}. Read that exact ${item.label} \u2014 ${WAKE_SCOPE} \u2014 and continue${url}${reviewTail}`;
6560
6765
  }
6561
6766
  const more = omitted > 0 ? `
6562
6767
  - \u2026and ${omitted} more not listed \u2014 check ${ref} for the rest` : "";
6563
6768
  const lines = items.map((i) => `- ${i.bot ? "\u{1F916}" : "\u{1F464}"} ${i.label} from @${i.actor}${wakeItemTail(i)}`).join("\n");
6564
6769
  return `${icon} ${items.length + omitted} new GitHub items on ${ref}. Read exactly these \u2014 ${WAKE_SCOPE} \u2014 and continue:
6565
6770
 
6566
- ${lines}${more}`;
6771
+ ${lines}${more}${reviewTail}`;
6772
+ }
6773
+ function parseGithubWakeSteer(text) {
6774
+ const item = (label, actor, bot, at, url) => ({
6775
+ label,
6776
+ actor,
6777
+ bot,
6778
+ ...at ? { at } : {},
6779
+ ...url ? { url } : {}
6780
+ });
6781
+ const lines = text.trim().split("\n").map((line) => line.trim()).filter(Boolean);
6782
+ const single = WAKE_SINGLE.exec(lines[0] ?? "");
6783
+ if (single) {
6784
+ return { ref: single[3], omitted: 0, items: [item(single[2], single[4], single[1] === "\u{1F916}", single[5], single[6])] };
6785
+ }
6786
+ const head = WAKE_MULTI_HEAD.exec(lines[0] ?? "");
6787
+ if (!head) return null;
6788
+ const items = [];
6789
+ let omitted = 0;
6790
+ for (const line of lines.slice(1)) {
6791
+ const more = WAKE_MORE.exec(line);
6792
+ if (more) {
6793
+ omitted = Number(more[1]);
6794
+ continue;
6795
+ }
6796
+ const m = WAKE_ITEM.exec(line);
6797
+ if (!m) continue;
6798
+ items.push(item(m[2], m[3], m[1] === "\u{1F916}", m[4], m[5]));
6799
+ }
6800
+ if (!items.length || items.length + omitted !== Number(head[2])) return null;
6801
+ return { ref: head[3], omitted, items };
6567
6802
  }
6568
- var ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrayStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, NativeInputRequired, AwaitingHint, AWAITING_TIMER_RE, SnoozeUntil, SNOOZE_PROMPT_MAX, SnoozePrompt, ThreadFence, PlanView, LimitWindow, LimitPause, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AuthSnapshot, AccountLogoutInput, AccountLogoutResult, AccountLoginStartInput, AccountLoginStartResult, AccountLoginStatusInput, AccountLoginStatusResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, FollowUpInput, UnqueueFollowUpInput, UnqueueFollowUpResult, SetThreadSnoozeInput, ConfirmAwaitingInput, RenameThreadInput, AiRenameThreadInput, AiRenameThreadResult, SetThreadPermissionInput, SetThreadPermissionResult, ThreadProfileOptionsInput, ThreadProfileOptionsResult, SetThreadProfileInput, SetThreadProfileResult, DISPATCH_TASK_BANNER, DISPATCH_TASK_BANNER_MARKER, GITHUB_DISPATCH_UI_BOUNDARY, WAKE_DELIVERY_TOKEN_TAIL, AGENT_MESSAGE_WRAPPER, WAKE_SCOPE, WAKE_REF, WAKE_SINGLE, WAKE_MULTI_HEAD, GithubStatus, GithubItem, GithubListInput, GithubBatchInput, GithubBatchResult, BoardMeta, ServerEvent, TranscriptEdit, TranscriptTodo, TranscriptToolCall, THOUGHT_EVENT_PREFIX, TranscriptPart, TranscriptMessage, TranscriptPageCursor, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketClientMsg, DEFAULT_PORT, tmuxSessionName;
6803
+ var ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrayStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, NativeInputRequired, AwaitingHint, AWAITING_TIMER_RE, SnoozeUntil, SNOOZE_PROMPT_MAX, SnoozePrompt, ThreadFence, PlanView, LimitWindow, LimitPause, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AuthSnapshot, AccountLogoutInput, AccountLogoutResult, AccountLoginStartInput, AccountLoginStartResult, AccountLoginStatusInput, AccountLoginStatusResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, FollowUpInput, UnqueueFollowUpInput, UnqueueFollowUpResult, SetThreadSnoozeInput, ConfirmAwaitingInput, RenameThreadInput, AiRenameThreadInput, AiRenameThreadResult, SetThreadPermissionInput, SetThreadPermissionResult, ThreadProfileOptionsInput, ThreadProfileOptionsResult, SetThreadProfileInput, SetThreadProfileResult, DISPATCH_TASK_BANNER, DISPATCH_TASK_BANNER_MARKER, GITHUB_DISPATCH_UI_BOUNDARY, WAKE_DELIVERY_TOKEN_TAIL, AGENT_MESSAGE_WRAPPER, GithubWakeItem, GithubWakeSteer, WAKE_SCOPE, WAKE_REVIEW_LEAD, WAKE_REF, WAKE_SINGLE, WAKE_MULTI_HEAD, WAKE_ITEM, WAKE_MORE, GithubStatus, GithubItem, GithubListInput, GithubBatchInput, GithubBatchResult, BoardMeta, ServerEvent, TranscriptEdit, TranscriptTodo, TranscriptToolCall, THOUGHT_EVENT_PREFIX, TranscriptPart, TranscriptMessage, TranscriptPageCursor, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketClientMsg, DEFAULT_PORT, tmuxSessionName;
6569
6804
  var init_src = __esm({
6570
6805
  "packages/shared/src/index.ts"() {
6571
6806
  "use strict";
@@ -7199,7 +7434,13 @@ var init_src = __esm({
7199
7434
  sessionId: external_exports.string().min(1),
7200
7435
  message: external_exports.string().min(1),
7201
7436
  // Generated once before the optimistic clear so a transport replay can be idempotent.
7202
- deliveryId: external_exports.string().min(1).max(200).optional()
7437
+ deliveryId: external_exports.string().min(1).max(200).optional(),
7438
+ // Retire the worker's live process before delivering, so this message lands in a `claude` that has
7439
+ // just started. The operator's "Restart worker" verb — the ONLY caller that sets it — exists because
7440
+ // a worker inherits its plugin/hooks AND its system prompt at process start and can never pick up a
7441
+ // newer fray build in place (hooks are read once, at startup). Everything else that needs a fresh
7442
+ // process derives it server-side; see needsFreshProcessForLimit.
7443
+ freshProcess: external_exports.boolean().optional()
7203
7444
  });
7204
7445
  UnqueueFollowUpInput = external_exports.object({
7205
7446
  slug: ThreadSlug,
@@ -7270,7 +7511,27 @@ ${DISPATCH_TASK_BANNER}
7270
7511
  GITHUB_DISPATCH_UI_BOUNDARY = "<!-- fray-ui:github-dispatch-ui-boundary:v1 -->";
7271
7512
  WAKE_DELIVERY_TOKEN_TAIL = /\n*<!-- fray-wake:[A-Za-z0-9_-]+ -->\s*$/;
7272
7513
  AGENT_MESSAGE_WRAPPER = /^<agent-message from="([^"]*)">\n?([\s\S]*?)\n?<\/agent-message>\s*$/;
7514
+ GithubWakeItem = external_exports.object({
7515
+ label: external_exports.string(),
7516
+ // the activity's noun ("comment", "approval", "change request", …)
7517
+ actor: external_exports.string(),
7518
+ // GitHub login, no leading @
7519
+ bot: external_exports.boolean(),
7520
+ // drives the 🤖/👤 icon; an app files most of what wakes this watcher
7521
+ at: external_exports.string().optional(),
7522
+ // ISO8601
7523
+ url: external_exports.string().optional()
7524
+ // the item's own permalink
7525
+ });
7526
+ GithubWakeSteer = external_exports.object({
7527
+ ref: external_exports.string(),
7528
+ // owner/repo#N
7529
+ items: external_exports.array(GithubWakeItem),
7530
+ omitted: external_exports.number()
7531
+ // fresh items counted but not named (the enumeration cap)
7532
+ });
7273
7533
  WAKE_SCOPE = "ignore older activity you have already handled";
7534
+ WAKE_REVIEW_LEAD = "A review's body is often empty because its substance is inline comments. Read them, one call each:";
7274
7535
  WAKE_REF = String.raw`[A-Za-z0-9][\w.-]*\/[A-Za-z0-9][\w.-]*#\d+`;
7275
7536
  WAKE_SINGLE = new RegExp(
7276
7537
  String.raw`^(👤|🤖) New GitHub (.+?) on (${WAKE_REF}) from @(\S+?)(?: at (\S+?))?\. Read that exact .+? — ` + WAKE_SCOPE + String.raw` — and continue(?::\s*(\S+)|\.)$`
@@ -7278,6 +7539,8 @@ ${DISPATCH_TASK_BANNER}
7278
7539
  WAKE_MULTI_HEAD = new RegExp(
7279
7540
  String.raw`^(👤|🤖) (\d+) new GitHub items on (${WAKE_REF})\. Read exactly these — ` + WAKE_SCOPE + String.raw` — and continue:$`
7280
7541
  );
7542
+ WAKE_ITEM = /^- (👤|🤖) (.+?) from @(\S+?)(?: at (\S+?))?(?:: (\S+))?$/;
7543
+ WAKE_MORE = /^- …and (\d+) more not listed — check .+ for the rest$/;
7281
7544
  GithubStatus = external_exports.object({
7282
7545
  installed: external_exports.boolean(),
7283
7546
  inRepo: external_exports.boolean(),
@@ -7567,11 +7830,20 @@ ${DISPATCH_TASK_BANNER}
7567
7830
  // non-reasoning messages and on any reasoning block whose timing couldn't be derived.
7568
7831
  durationMs: external_exports.number().nonnegative().optional(),
7569
7832
  // A turn-BOUNDARY marker: this `kind:"event"` line was emitted at the position an EXTERNAL wake — a
7570
- // background task/shell completion `<task-notification>` re-invoked the agent and started a fresh
7571
- // turn. The client renders it as a centered divider rule carrying the cause label, so two consecutive
7572
- // assistant turns (each with its own trailing signal) no longer paint as one seamless bubble. Additive
7573
- // + optional: an old client ignores it and shows the plain quiet event line (graceful degrade).
7574
- boundary: external_exports.boolean().optional(),
7833
+ // This event opened a fresh turn, so it renders as a centered divider rule carrying the cause label —
7834
+ // without it, two consecutive assistant turns (each with its own trailing signal) paint as one
7835
+ // seamless bubble. Additive + optional: an old client ignores it and shows the plain quiet event line
7836
+ // (graceful degrade).
7837
+ //
7838
+ // It names WHICH KIND of boundary, because two unrelated events earn the divider and the client has to
7839
+ // tell them apart to put the right glyph on each:
7840
+ // wake — a background task/shell completion `<task-notification>` re-invoked the agent
7841
+ // compaction — the provider rewrote the conversation and dropped everything above this point
7842
+ // It was a bare boolean until the dividers grew icons (a shell glyph on a compaction line is simply
7843
+ // wrong), and the kind has to come from the SERVER: the alternative is the client sniffing the label
7844
+ // text, which is the guess this codebase refuses everywhere else. A string stays truthy, so any
7845
+ // surviving `if (boundary)` reads exactly as it did.
7846
+ boundary: external_exports.enum(["wake", "compaction"]).optional(),
7575
7847
  // Block-ordered content for an assistant turn (see TranscriptPart). Defaults to [] so a pre-restart
7576
7848
  // server (which ships only text/tools) parses; the client renders `parts` when non-empty and falls
7577
7849
  // back to the legacy tools-then-text layout when it's empty. `text`/`tools` stay populated for that
@@ -7600,6 +7872,17 @@ ${DISPATCH_TASK_BANNER}
7600
7872
  // old client ignores it and shows the plain bubble (the previous behavior), and an old server simply
7601
7873
  // never sets it.
7602
7874
  wake: external_exports.boolean().optional(),
7875
+ // The STRUCTURED wake, parsed by the server from the same text the same build formatted. The chat
7876
+ // renders the divider from this rather than re-deriving it from prose in the browser.
7877
+ //
7878
+ // It exists because re-deriving it in the browser is version-skewed by construction. `web/api/boot.ts`
7879
+ // adopts a new server boot id IN PLACE (so an unsent draft survives a restart), so a promoted artifact
7880
+ // swaps the server under tabs that keep their old bundle — and on 2026-07-31 a steer that gained two
7881
+ // agent-facing lines met parsers that predated them, which cost every open tab its card and dumped the
7882
+ // raw `gh api …` text into the transcript instead. Server-side, formatter and parser can never
7883
+ // disagree. Additive + optional: absent from a legacy transcript or an older server, and the client
7884
+ // falls back to `parseGithubWakeSteer` on the text.
7885
+ wakeSteer: GithubWakeSteer.optional(),
7603
7886
  // A SUB-AGENT (or peer session) wrote this user turn, not the human — the same defect class `wake`
7604
7887
  // above corrects. Claude Code's agent-to-agent channel (a background child calling
7605
7888
  // `SendMessage({to:"main"})`) delivers UPWARD into the parent's queue like any follow-up, so the
@@ -9992,15 +10275,15 @@ var init_settings = __esm({
9992
10275
  });
9993
10276
 
9994
10277
  // packages/server/src/discover.ts
9995
- import { readdirSync, statSync as statSync4, openSync as openSync4, readSync, closeSync as closeSync4 } from "node:fs";
9996
- import { join as join5 } from "node:path";
10278
+ import { readdirSync as readdirSync2, statSync as statSync5, openSync as openSync5, readSync, closeSync as closeSync4 } from "node:fs";
10279
+ import { join as join6 } from "node:path";
9997
10280
  function sentinelFor(sessionId) {
9998
10281
  return `threads/${sessionId}/scratch.md`;
9999
10282
  }
10000
10283
  function readHead(path) {
10001
10284
  let fd;
10002
10285
  try {
10003
- fd = openSync4(path, "r");
10286
+ fd = openSync5(path, "r");
10004
10287
  const buf = Buffer.allocUnsafe(HEAD_BYTES);
10005
10288
  const n = readSync(fd, buf, 0, HEAD_BYTES, 0);
10006
10289
  return buf.toString("utf8", 0, n);
@@ -10021,7 +10304,7 @@ function discoverTranscriptId(logDir, sessionId, opts = {}) {
10021
10304
  const sentinel = sentinelFor(sessionId);
10022
10305
  let names;
10023
10306
  try {
10024
- names = readdirSync(logDir);
10307
+ names = readdirSync2(logDir);
10025
10308
  } catch {
10026
10309
  return void 0;
10027
10310
  }
@@ -10030,10 +10313,10 @@ function discoverTranscriptId(logDir, sessionId, opts = {}) {
10030
10313
  if (name.startsWith(".") || !name.endsWith(".jsonl")) continue;
10031
10314
  const id = name.slice(0, -".jsonl".length);
10032
10315
  if (!id || id === sessionId || exclude?.has(id)) continue;
10033
- const path = join5(logDir, name);
10316
+ const path = join6(logDir, name);
10034
10317
  let mtime;
10035
10318
  try {
10036
- mtime = statSync4(path).mtimeMs;
10319
+ mtime = statSync5(path).mtimeMs;
10037
10320
  } catch {
10038
10321
  continue;
10039
10322
  }
@@ -10405,7 +10688,7 @@ function spawnWithRunner(slug, cmd, cwd, env, options, runner) {
10405
10688
  invalidateLiveness();
10406
10689
  return identity;
10407
10690
  } catch {
10408
- console.error(`[fray-ui] tmux worker spawn failed (stage=${stage}, created=${identity ? "yes" : "no"})`);
10691
+ log.error("tmux", `worker spawn failed (stage=${stage}, created=${identity ? "yes" : "no"})`);
10409
10692
  throw new TmuxSpawnError(stage, identity);
10410
10693
  }
10411
10694
  }
@@ -10975,6 +11258,7 @@ var init_tmux = __esm({
10975
11258
  "use strict";
10976
11259
  init_src();
10977
11260
  init_tmux_socket();
11261
+ init_logging();
10978
11262
  init_tmux_socket();
10979
11263
  execFileAsync = promisify(execFile);
10980
11264
  socket = "fray";
@@ -11298,6 +11582,28 @@ A \` \`\`\`question \` block IS the handback \u2014 do not also emit a \`done\`/
11298
11582
  as your next user message, possibly as terse as "1: A, 2: B".`;
11299
11583
  STOP_CRITERION = `## The stop criterion
11300
11584
 
11585
+ **COMING TO REST IS A STOP, and it needs the same justification as a question.** Everything below is
11586
+ about when to ASK \u2014 but the far more common failure is quieter: you finish one part of a multi-part
11587
+ instruction, write it up, and rest with the rest of the mandate untouched. Nobody asked you to stop;
11588
+ you simply produced a handoff because a handoff was the shape closest to hand. That is not a report,
11589
+ it is an abandonment, and it costs the human the same hours a needless question does.
11590
+
11591
+ So: **if the human's instruction still has parts left, do not write up \u2014 do the next part in this same
11592
+ turn.** Keep issuing tool calls. A verified milestone is not a stopping point, a green test run is not
11593
+ a stopping point, and neither is a long turn \u2014 the conversation is summarized automatically, so turn
11594
+ length is never a reason to pause. The only things that end a turn are the stop criteria below and
11595
+ actually being finished.
11596
+
11597
+ Three specific traps, each of which has ended a turn that should have continued:
11598
+
11599
+ - **Announcing the next step instead of taking it.** "Now starting X" followed by no tool call is a
11600
+ stop wearing the costume of progress. If you can write the sentence, you can make the call instead.
11601
+ - **Writing the next action into the scratchpad and feeling done.** Recording work is not doing work.
11602
+ The pad is crash insurance, never a handoff \u2014 see the scratchpad section. If the pad's next action
11603
+ is something the human already told you to do, the only correct move is to do it NOW.
11604
+ - **Pre-emptively worrying about context budget.** You do not get to stop early because the turn feels
11605
+ long. If context genuinely runs out, the harness summarizes and you continue.
11606
+
11301
11607
  **Your default is to DECIDE.** A reversible call costs minutes to redo; a round-trip to the human costs
11302
11608
  hours \u2014 they may not read your question for hours, and the whole effort sits idle until they do. If it
11303
11609
  is derivable from the code, the conventions, or ordinary engineering judgment, it is yours: make it,
@@ -11351,6 +11657,12 @@ rate-limited to once every two minutes so it cannot create a tight stop loop. Re
11351
11657
  \`stop_hook\` to deregister it before genuinely finishing. This is a rest-time nudge, not a timer and
11352
11658
  cannot wake a turn that already stopped.
11353
11659
 
11660
+ - **IT IS OPTIONAL, IT IS NOT A DELIVERABLE, AND WRITING IN IT IS NOT DOING THE WORK.** The pad exists
11661
+ in case you want it \u2014 crash insurance for a long effort, and the file watchers and stop hooks read
11662
+ it. A single direct task usually needs NOTHING here: just do the task. Never let a pad entry stand in
11663
+ for an action. Recording "next: X" when the human asked for X is not progress on X, and a turn that
11664
+ ends right after a pad update is nearly always a turn that stopped for no reason. When it is worth
11665
+ writing, write it AS YOU GO, mid-work, and then keep working.
11354
11666
  - **It is your compaction-survival mechanism, so use it deliberately.** A long effort WILL be
11355
11667
  compacted, and compaction drops the REASONING first \u2014 the plan, the alternatives you ruled out, why
11356
11668
  the human chose what they chose. A summary preserves what you did, not why. So write that here AS
@@ -11390,6 +11702,13 @@ rate-limited to once every two minutes so it cannot create a tight stop loop. Re
11390
11702
  \`stop_hook\` to deregister it before genuinely finishing. This is a rest-time nudge, not a timer and
11391
11703
  cannot wake a turn that already stopped.
11392
11704
 
11705
+ **IT IS OPTIONAL, IT IS NOT A DELIVERABLE, AND WRITING IN IT IS NOT DOING THE WORK.** The pad exists in
11706
+ case you want it \u2014 crash insurance for a long effort, and the file watchers and stop hooks read it. A
11707
+ single direct task usually needs NOTHING here: just do the task. Never let a pad entry stand in for an
11708
+ action. Recording "next: X" when the human asked for X is not progress on X, and a turn that ends right
11709
+ after a pad update is nearly always a turn that stopped for no reason. When it is worth writing, write
11710
+ it AS YOU GO, mid-work, and then keep working.
11711
+
11393
11712
  **It is your compaction-survival mechanism, so use it deliberately.** A long effort WILL be compacted,
11394
11713
  and compaction drops the REASONING first \u2014 the plan, the alternatives you ruled out, why the human
11395
11714
  chose what they chose. A summary preserves what you did, not why. So write that here AS YOU GO rather
@@ -11717,10 +12036,10 @@ var init_detached_daemons = __esm({
11717
12036
  });
11718
12037
 
11719
12038
  // packages/server/src/backend/claude-broker-diagnostics.ts
11720
- import { appendFileSync, mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync4, statSync as statSync5 } from "node:fs";
11721
- import { join as join6 } from "node:path";
12039
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6, readFileSync as readFileSync5, renameSync as renameSync4, statSync as statSync6 } from "node:fs";
12040
+ import { join as join7 } from "node:path";
11722
12041
  function claudeBrokerDiagnosticLogPath(stateDir, sessionId) {
11723
- return join6(stateDir, "claude-broker", `${sessionId}.diagnostics.log`);
12042
+ return join7(stateDir, "claude-broker", `${sessionId}.diagnostics.log`);
11724
12043
  }
11725
12044
  function readClaudeBrokerExit(stateDir, sessionId) {
11726
12045
  let newest = null;
@@ -11764,14 +12083,14 @@ var init_claude_broker_diagnostics = __esm({
11764
12083
  // packages/server/src/backend/claude-broker-host.ts
11765
12084
  import { spawn as spawn2 } from "node:child_process";
11766
12085
  import { createHash as createHash4, randomUUID as randomUUID8 } from "node:crypto";
11767
- import { accessSync, constants as fsConstants, mkdirSync as mkdirSync6, readFileSync as readFileSync6, readdirSync as readdirSync2, unlinkSync } from "node:fs";
11768
- import { delimiter, dirname as dirname4, isAbsolute as isAbsolute4, join as join7 } from "node:path";
12086
+ import { accessSync, constants as fsConstants, mkdirSync as mkdirSync7, readFileSync as readFileSync6, readdirSync as readdirSync3, unlinkSync } from "node:fs";
12087
+ import { delimiter, dirname as dirname4, isAbsolute as isAbsolute4, join as join8 } from "node:path";
11769
12088
  function resolveClaudeExecutableAbsolute(bin, env = process.env) {
11770
12089
  const candidate = bin && bin.length > 0 ? bin : "claude";
11771
12090
  if (isAbsolute4(candidate)) return candidate;
11772
12091
  for (const dir of (env.PATH ?? "").split(delimiter)) {
11773
12092
  if (!dir) continue;
11774
- const full = join7(dir, candidate);
12093
+ const full = join8(dir, candidate);
11775
12094
  try {
11776
12095
  accessSync(full, fsConstants.X_OK);
11777
12096
  return full;
@@ -11783,11 +12102,11 @@ function resolveClaudeExecutableAbsolute(bin, env = process.env) {
11783
12102
  function claudeBrokerSocketPath(stateDir, sessionId) {
11784
12103
  const key = createHash4("sha256").update(stateDir).update("\0").update(sessionId).digest("hex").slice(0, 16);
11785
12104
  if (process.platform === "win32") return `\\\\.\\pipe\\fray-claude-${key}`;
11786
- return join7(process.env.TMPDIR ?? "/tmp", `fray-claude-${key}.sock`);
12105
+ return join8(process.env.TMPDIR ?? "/tmp", `fray-claude-${key}.sock`);
11787
12106
  }
11788
12107
  function claudeBrokerRecordPath(stateDir, sessionId) {
11789
12108
  const key = createHash4("sha256").update(sessionId).digest("hex").slice(0, 16);
11790
- return join7(stateDir, "claude-broker", `${key}.json`);
12109
+ return join8(stateDir, "claude-broker", `${key}.json`);
11791
12110
  }
11792
12111
  function pidAlive(pid) {
11793
12112
  try {
@@ -11816,17 +12135,17 @@ function liveBrokerRecord(recordPath2) {
11816
12135
  return null;
11817
12136
  }
11818
12137
  function liveBrokerRecords(stateDir) {
11819
- const dir = join7(stateDir, "claude-broker");
12138
+ const dir = join8(stateDir, "claude-broker");
11820
12139
  let names;
11821
12140
  try {
11822
- names = readdirSync2(dir);
12141
+ names = readdirSync3(dir);
11823
12142
  } catch {
11824
12143
  return [];
11825
12144
  }
11826
12145
  const out = [];
11827
12146
  for (const name of names) {
11828
12147
  if (!name.endsWith(".json")) continue;
11829
- const record = liveBrokerRecord(join7(dir, name));
12148
+ const record = liveBrokerRecord(join8(dir, name));
11830
12149
  if (record) out.push(record);
11831
12150
  }
11832
12151
  return out;
@@ -11834,7 +12153,7 @@ function liveBrokerRecords(stateDir) {
11834
12153
  function forkBroker(options) {
11835
12154
  const socketPath = claudeBrokerSocketPath(options.stateDir, options.sessionId);
11836
12155
  const recordPath2 = claudeBrokerRecordPath(options.stateDir, options.sessionId);
11837
- mkdirSync6(dirname4(recordPath2), { recursive: true });
12156
+ mkdirSync7(dirname4(recordPath2), { recursive: true });
11838
12157
  const config = {
11839
12158
  socketPath,
11840
12159
  cwd: options.cwd,
@@ -12053,6 +12372,17 @@ var init_claude_broker_client = __esm({
12053
12372
  });
12054
12373
 
12055
12374
  // packages/server/src/backend/claude-agent-sdk-protocol.ts
12375
+ function hasUndeliverableInputText(value) {
12376
+ for (const character of value) {
12377
+ const code = character.codePointAt(0);
12378
+ if (code >= 55296 && code <= 57343) return true;
12379
+ if (code === 8232 || code === 8233) return true;
12380
+ if (code <= 31 || code >= 127 && code <= 159) {
12381
+ if (!DELIVERABLE_CONTROL_CODES.has(code)) return true;
12382
+ }
12383
+ }
12384
+ return false;
12385
+ }
12056
12386
  function utf8Bytes(value) {
12057
12387
  return encoder2.encode(value).byteLength;
12058
12388
  }
@@ -12133,8 +12463,8 @@ function validateInputMessage(value) {
12133
12463
  if (utf8Bytes(value.text) > CLAUDE_AGENT_SDK_MAX_INPUT_BYTES) {
12134
12464
  throw new ClaudeAgentSdkProtocolError(`input.text exceeds ${CLAUDE_AGENT_SDK_MAX_INPUT_BYTES} bytes`);
12135
12465
  }
12136
- const text = safeText(value.text, "input.text", CLAUDE_AGENT_SDK_MAX_INPUT_BYTES);
12137
- if (text !== value.text) throw new ClaudeAgentSdkProtocolError("input.text contains unsafe text");
12466
+ const text = value.text;
12467
+ if (hasUndeliverableInputText(text)) throw new ClaudeAgentSdkProtocolError("input.text contains unsafe text");
12138
12468
  const parentToolUseId = boundedOptionalId(value.parentToolUseId, "input.parentToolUseId");
12139
12469
  return parentToolUseId === void 0 ? { id, text } : { id, text, parentToolUseId };
12140
12470
  }
@@ -12164,7 +12494,7 @@ function validatePermissionDecision(value) {
12164
12494
  }
12165
12495
  throw new ClaudeAgentSdkProtocolError("permission decision behavior is unsupported");
12166
12496
  }
12167
- var CLAUDE_AGENT_SDK_MAX_INPUT_BYTES, CLAUDE_AGENT_SDK_MAX_JSON_BYTES, CLAUDE_AGENT_SDK_MAX_EVENT_TEXT_BYTES, CLAUDE_AGENT_SDK_MAX_DIAGNOSTIC_BYTES, encoder2, UNSAFE_TEXT2, ID_PATTERN, RESERVED_KEYS, CLAUDE_BROKER_CAPABILITY_SUBAGENT_STEER, CLAUDE_BROKER_CAPABILITY_CANCEL_INPUT, CLAUDE_BROKER_CAPABILITY_STOP_TASK, ClaudeAgentSdkProtocolError;
12497
+ var CLAUDE_AGENT_SDK_MAX_INPUT_BYTES, CLAUDE_AGENT_SDK_MAX_JSON_BYTES, CLAUDE_AGENT_SDK_MAX_EVENT_TEXT_BYTES, CLAUDE_AGENT_SDK_MAX_DIAGNOSTIC_BYTES, encoder2, UNSAFE_TEXT2, ID_PATTERN, RESERVED_KEYS, DELIVERABLE_CONTROL_CODES, CLAUDE_BROKER_CAPABILITY_SUBAGENT_STEER, CLAUDE_BROKER_CAPABILITY_CANCEL_INPUT, CLAUDE_BROKER_CAPABILITY_STOP_TASK, ClaudeAgentSdkProtocolError;
12168
12498
  var init_claude_agent_sdk_protocol = __esm({
12169
12499
  "packages/server/src/backend/claude-agent-sdk-protocol.ts"() {
12170
12500
  "use strict";
@@ -12176,6 +12506,7 @@ var init_claude_agent_sdk_protocol = __esm({
12176
12506
  UNSAFE_TEXT2 = /[\p{Cf}\p{Cs}\p{Zl}\p{Zp}\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu;
12177
12507
  ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/;
12178
12508
  RESERVED_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
12509
+ DELIVERABLE_CONTROL_CODES = /* @__PURE__ */ new Set([9, 10, 13]);
12179
12510
  CLAUDE_BROKER_CAPABILITY_SUBAGENT_STEER = "subagent-steer-v1";
12180
12511
  CLAUDE_BROKER_CAPABILITY_CANCEL_INPUT = "cancel-input-v1";
12181
12512
  CLAUDE_BROKER_CAPABILITY_STOP_TASK = "stop-task-v1";
@@ -12335,28 +12666,35 @@ var init_credential_redaction = __esm({
12335
12666
  });
12336
12667
 
12337
12668
  // packages/server/src/backend/claude-permission-interactions.ts
12669
+ import { homedir as homedir4 } from "node:os";
12338
12670
  function clip(text, max) {
12339
12671
  return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
12340
12672
  }
12673
+ function tildePath(path) {
12674
+ const home = homedir4();
12675
+ if (!path.startsWith(home)) return path;
12676
+ const rest = path.slice(home.length);
12677
+ return rest === "" ? "~" : rest.startsWith("/") ? `~${rest}` : path;
12678
+ }
12679
+ function inputPreview(input, shown) {
12680
+ if (!input) return "";
12681
+ const subject = SUBJECT_KEYS.find((key) => typeof input[key] === "string" && input[key] !== "");
12682
+ const lines = Object.entries(input).filter((entry) => entry[0] !== subject && entry[1] !== "" && entry[1] !== null && entry[1] !== shown).map((entry) => `${entry[0]}: ${typeof entry[1] === "string" ? entry[1] : JSON.stringify(entry[1])}`);
12683
+ if (subject) lines.unshift(String(input[subject]));
12684
+ const text = redactCredentialSyntax(lines.join("\n\n")).split("\n");
12685
+ return clip(text.length > PREVIEW_MAX_LINES ? `${text.slice(0, PREVIEW_MAX_LINES).join("\n")}
12686
+ \u2026` : text.join("\n"), PREVIEW_MAX_CHARS);
12687
+ }
12341
12688
  function buildClaudePermissionInteraction(request, owner) {
12342
12689
  const tool = request.toolName || "tool";
12343
- let inputSummary = "";
12344
- try {
12345
- inputSummary = clip(redactCredentialSyntax(JSON.stringify(request.input ?? {})), 3800);
12346
- } catch {
12347
- inputSummary = "(uninspectable input)";
12348
- }
12349
- const title = clip(`Approve ${tool}?`, 150);
12690
+ const command = typeof request.input?.command === "string" ? request.input.command : "";
12691
+ const title = clip(command ? "Run a command?" : `Approve ${tool}?`, 150);
12692
+ const description = request.description ? clip(redactCredentialSyntax(request.description), 1e3) : "";
12350
12693
  const message = clip(
12351
- `A ${tool} tool call needs your approval before it runs.` + (request.description ? `
12352
-
12353
- ${clip(redactCredentialSyntax(request.description), 1e3)}` : "") + (inputSummary ? `
12354
-
12355
- Input: ${inputSummary}` : "") + (request.blockedPath ? `
12356
-
12357
- Path: ${clip(request.blockedPath, 500)}` : ""),
12358
- 7900
12694
+ [description, request.blockedPath ? `Blocked path: ${clip(request.blockedPath, 500)}` : ""].filter(Boolean).join("\n\n"),
12695
+ 2e3
12359
12696
  );
12697
+ const preview = inputPreview(request.input, request.description);
12360
12698
  const parsed = InteractionRequest.safeParse({
12361
12699
  protocolVersion: INTERACTION_PROTOCOL_VERSION,
12362
12700
  contentFormat: "plain-text",
@@ -12376,12 +12714,14 @@ Path: ${clip(request.blockedPath, 500)}` : ""),
12376
12714
  providerRequestId: clip(request.requestId, 500),
12377
12715
  allowedDecisions: ALLOWED_DECISIONS,
12378
12716
  payload: {
12379
- title,
12380
- message,
12381
12717
  kind: "permission-approval",
12382
- permission: clip(request.requestId, 500),
12383
- resourceLabel: clip(tool, 2e3),
12384
- workingDirectoryLabel: clip(owner.cwd, 2e3)
12718
+ title,
12719
+ // `permission` is the card's readable identity, the way Codex's is "network+filesystem". It was
12720
+ // the requestId, which put a bare UUID on screen and told the operator nothing.
12721
+ permission: singleLine(tool, 250, "tool"),
12722
+ ...message ? { message } : {},
12723
+ ...preview ? { preview } : {},
12724
+ ...command ? { promptLabel: singleLine(tildePath(owner.cwd), 250, "~", false) } : { workingDirectoryLabel: clip(owner.cwd, 2e3) }
12385
12725
  },
12386
12726
  expiresAt: null
12387
12727
  });
@@ -12535,7 +12875,7 @@ function claudeQuestionDecisionFor(spec, decisionId, values) {
12535
12875
  message: "Fray could not return the operator's answer in a form this tool accepts. Ask the question in your final message instead."
12536
12876
  };
12537
12877
  }
12538
- var CLAUDE_PERMISSION_DECISIONS, ALLOWED_DECISIONS, CLAUDE_ASK_USER_QUESTION_TOOL, CLAUDE_QUESTION_DECISIONS, QUESTION_DECISIONS, MAX_QUESTIONS, MAX_OPTIONS, MAX_QUESTION_KEY_BYTES, encoder3, UNSAFE_TEXT_SOURCE, SCRUB_UNSAFE_TEXT, HAS_UNSAFE_TEXT, CLAUDE_NOTES_ONLY, CLAUDE_QUESTION_DECLINED;
12878
+ var CLAUDE_PERMISSION_DECISIONS, ALLOWED_DECISIONS, SUBJECT_KEYS, PREVIEW_MAX_CHARS, PREVIEW_MAX_LINES, CLAUDE_ASK_USER_QUESTION_TOOL, CLAUDE_QUESTION_DECISIONS, QUESTION_DECISIONS, MAX_QUESTIONS, MAX_OPTIONS, MAX_QUESTION_KEY_BYTES, encoder3, UNSAFE_TEXT_SOURCE, SCRUB_UNSAFE_TEXT, HAS_UNSAFE_TEXT, CLAUDE_NOTES_ONLY, CLAUDE_QUESTION_DECLINED;
12539
12879
  var init_claude_permission_interactions = __esm({
12540
12880
  "packages/server/src/backend/claude-permission-interactions.ts"() {
12541
12881
  "use strict";
@@ -12550,6 +12890,9 @@ var init_claude_permission_interactions = __esm({
12550
12890
  { id: CLAUDE_PERMISSION_DECISIONS.allow, semantic: "approve", label: "Grant for turn", description: "Allow this tool call to run once." },
12551
12891
  { id: CLAUDE_PERMISSION_DECISIONS.deny, semantic: "deny", label: "Deny", description: "Block this tool call." }
12552
12892
  ];
12893
+ SUBJECT_KEYS = ["command", "file_path", "notebook_path", "path", "url", "pattern", "query", "prompt"];
12894
+ PREVIEW_MAX_CHARS = 6e3;
12895
+ PREVIEW_MAX_LINES = 200;
12553
12896
  CLAUDE_ASK_USER_QUESTION_TOOL = "AskUserQuestion";
12554
12897
  CLAUDE_QUESTION_DECISIONS = { answer: "answer", decline: "decline" };
12555
12898
  QUESTION_DECISIONS = [
@@ -12576,6 +12919,20 @@ function claudeBrokerBridgeEnabled(env = process.env) {
12576
12919
  function inputIdFor(deliveryId) {
12577
12920
  return deliveryId && UUID_SHAPE.test(deliveryId) ? deliveryId : randomUUID9();
12578
12921
  }
12922
+ function validatedInput(message) {
12923
+ try {
12924
+ return validateInputMessage(message);
12925
+ } catch (error) {
12926
+ const detail = error instanceof Error ? error.message : String(error);
12927
+ if (/exceeds/.test(detail)) {
12928
+ throw new Error(`This message is too long to send (the limit is ${CLAUDE_AGENT_SDK_MAX_INPUT_BYTES / 1024}KB). Shorten it and resend.`);
12929
+ }
12930
+ if (/unsafe text/.test(detail)) {
12931
+ throw new Error("This message holds a control or incomplete character fray can't send. Edit and resend.");
12932
+ }
12933
+ throw new Error(`fray could not send this message: ${detail}`);
12934
+ }
12935
+ }
12579
12936
  function createClaudeAgentBrokerBridge(deps) {
12580
12937
  const sessions = /* @__PURE__ */ new Map();
12581
12938
  const executablePath = resolveClaudeExecutableAbsolute(deps.executablePath, deps.env);
@@ -12695,11 +13052,13 @@ function createClaudeAgentBrokerBridge(deps) {
12695
13052
  killBroker(deps.stateDir, prior.sessionId);
12696
13053
  sessions.delete(input.threadSlug);
12697
13054
  }
13055
+ const message = validatedInput({ id: randomUUID9(), text: input.prompt });
12698
13056
  const session = await attach3(input.threadSlug, input.sessionId, input.cwd, input.permissionMode ?? "default", { appendSystemPrompt: input.appendSystemPrompt, model: input.model, effort: input.effort });
12699
- session.client.sendInput({ id: randomUUID9(), text: input.prompt });
13057
+ session.client.sendInput(message);
12700
13058
  return { binding: { threadSlug: input.threadSlug, sessionId: input.sessionId, cwd: input.cwd, generation: session.generation, state: "active" } };
12701
13059
  },
12702
13060
  async followUp(input) {
13061
+ const message = validatedInput({ id: inputIdFor(input.deliveryId), text: input.text });
12703
13062
  let held = current(input.threadSlug, input.sessionId);
12704
13063
  if (held && !holdsLiveDaemon(held)) {
12705
13064
  held.client.close();
@@ -12719,7 +13078,7 @@ function createClaudeAgentBrokerBridge(deps) {
12719
13078
  input.permissionMode ?? "default",
12720
13079
  { resume: true, appendSystemPrompt: input.appendSystemPrompt, model: input.model, effort: input.effort }
12721
13080
  );
12722
- session.client.sendInput({ id: inputIdFor(input.deliveryId), text: input.text });
13081
+ session.client.sendInput(message);
12723
13082
  },
12724
13083
  async cancelFollowUp(input) {
12725
13084
  const held = current(input.threadSlug, input.sessionId);
@@ -12841,15 +13200,15 @@ var init_claude_agent_broker_bridge = __esm({
12841
13200
  });
12842
13201
 
12843
13202
  // packages/server/src/backend/claude-quota.ts
12844
- import { join as join8 } from "node:path";
12845
- import { homedir as homedir3, platform } from "node:os";
13203
+ import { join as join9 } from "node:path";
13204
+ import { homedir as homedir5, platform } from "node:os";
12846
13205
  import { createHash as createHash5 } from "node:crypto";
12847
13206
  import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
12848
13207
  import { execFile as execFile2 } from "node:child_process";
12849
13208
  import { promisify as promisify2 } from "node:util";
12850
13209
  function claudeConfigDir() {
12851
13210
  const override = process.env.CLAUDE_CONFIG_DIR;
12852
- return override && override.trim() ? override : join8(homedir3(), ".claude");
13211
+ return override && override.trim() ? override : join9(homedir5(), ".claude");
12853
13212
  }
12854
13213
  function tokenFromCredentialsJson(raw2) {
12855
13214
  let doc;
@@ -12878,7 +13237,7 @@ async function readKeychainToken() {
12878
13237
  }
12879
13238
  async function readAccessToken(configDir) {
12880
13239
  try {
12881
- const fromFile = tokenFromCredentialsJson(await readFile(join8(configDir, ".credentials.json"), "utf8"));
13240
+ const fromFile = tokenFromCredentialsJson(await readFile(join9(configDir, ".credentials.json"), "utf8"));
12882
13241
  if (fromFile) return fromFile;
12883
13242
  } catch {
12884
13243
  }
@@ -12947,8 +13306,8 @@ function parseClaudeUsage(body, planType) {
12947
13306
  function cachePaths(cacheDir, configDir) {
12948
13307
  const profile = createHash5("sha256").update(configDir).digest("hex").slice(0, 12);
12949
13308
  return {
12950
- data: join8(cacheDir, `claude-${profile}.json`),
12951
- lock: join8(cacheDir, `claude-${profile}.lock`)
13309
+ data: join9(cacheDir, `claude-${profile}.json`),
13310
+ lock: join9(cacheDir, `claude-${profile}.lock`)
12952
13311
  };
12953
13312
  }
12954
13313
  async function readShared(path) {
@@ -13140,7 +13499,7 @@ function refreshSharedInBackground(paths, claudeBin, deps, now) {
13140
13499
  async function refreshClaudeQuotaInBackground(claudeBin = "claude", deps = {}) {
13141
13500
  const now = (deps.now ?? Date.now)();
13142
13501
  const configDir = claudeConfigDir();
13143
- const cacheDir = deps.cacheDir ?? join8(homedir3(), ".fray", "quota-cache");
13502
+ const cacheDir = deps.cacheDir ?? join9(homedir5(), ".fray", "quota-cache");
13144
13503
  try {
13145
13504
  await mkdir(cacheDir, { recursive: true, mode: 448 });
13146
13505
  } catch {
@@ -13151,7 +13510,7 @@ async function refreshClaudeQuotaInBackground(claudeBin = "claude", deps = {}) {
13151
13510
  async function readClaudeQuota(claudeBin = "claude", deps = {}, options = {}) {
13152
13511
  const now = (deps.now ?? Date.now)();
13153
13512
  const configDir = claudeConfigDir();
13154
- const cacheDir = deps.cacheDir ?? join8(homedir3(), ".fray", "quota-cache");
13513
+ const cacheDir = deps.cacheDir ?? join9(homedir5(), ".fray", "quota-cache");
13155
13514
  const paths = cachePaths(cacheDir, configDir);
13156
13515
  try {
13157
13516
  await mkdir(cacheDir, { recursive: true, mode: 448 });
@@ -13239,19 +13598,19 @@ var init_claude_quota = __esm({
13239
13598
  });
13240
13599
 
13241
13600
  // packages/server/src/backend/auth-status.ts
13242
- import { join as join9 } from "node:path";
13243
- import { homedir as homedir4, platform as platform2 } from "node:os";
13601
+ import { join as join10 } from "node:path";
13602
+ import { homedir as homedir6, platform as platform2 } from "node:os";
13244
13603
  import { readFileSync as readFileSync7 } from "node:fs";
13245
13604
  import { execFile as execFile3 } from "node:child_process";
13246
13605
  import { promisify as promisify3 } from "node:util";
13247
13606
  function claudeConfigDir2() {
13248
13607
  const override = process.env.CLAUDE_CONFIG_DIR;
13249
- return override && override.trim() ? override : join9(homedir4(), ".claude");
13608
+ return override && override.trim() ? override : join10(homedir6(), ".claude");
13250
13609
  }
13251
13610
  function claudeFileState(configDir) {
13252
13611
  let raw2;
13253
13612
  try {
13254
- raw2 = readFileSync7(join9(configDir, ".credentials.json"), "utf8");
13613
+ raw2 = readFileSync7(join10(configDir, ".credentials.json"), "utf8");
13255
13614
  } catch (err) {
13256
13615
  return err.code === "ENOENT" ? "absent" : "error";
13257
13616
  }
@@ -13284,7 +13643,7 @@ function readCodexAuthState(codexHome = defaultCodexHome()) {
13284
13643
  if (process.env.OPENAI_API_KEY || process.env.CODEX_API_KEY || process.env.CODEX_ACCESS_TOKEN) return "authed";
13285
13644
  let raw2;
13286
13645
  try {
13287
- raw2 = readFileSync7(join9(codexHome, "auth.json"), "utf8");
13646
+ raw2 = readFileSync7(join10(codexHome, "auth.json"), "utf8");
13288
13647
  } catch (err) {
13289
13648
  return err.code === "ENOENT" ? "signed-out" : "unknown";
13290
13649
  }
@@ -13374,7 +13733,7 @@ var init_auth_status = __esm({
13374
13733
  // packages/server/src/fray.ts
13375
13734
  import { execFile as execFile4 } from "node:child_process";
13376
13735
  import { lstatSync, realpathSync as realpathSync4 } from "node:fs";
13377
- import { basename as basename3, dirname as dirname5, join as join10, resolve as resolve5 } from "node:path";
13736
+ import { basename as basename3, dirname as dirname5, join as join11, resolve as resolve5 } from "node:path";
13378
13737
  import { promisify as promisify4 } from "node:util";
13379
13738
  function frayScriptsDir() {
13380
13739
  if (process.env.FRAY_SCRIPTS_DIR) return process.env.FRAY_SCRIPTS_DIR;
@@ -13383,7 +13742,7 @@ function frayScriptsDir() {
13383
13742
  function directFrayRoot(projectDir) {
13384
13743
  try {
13385
13744
  const projectRoot = realpathSync4(projectDir);
13386
- const path = join10(projectRoot, ".fray");
13745
+ const path = join11(projectRoot, ".fray");
13387
13746
  const stat2 = lstatSync(path);
13388
13747
  if (!stat2.isDirectory() || stat2.isSymbolicLink()) return null;
13389
13748
  const real = realpathSync4(path);
@@ -13397,7 +13756,7 @@ function frayDirExists(projectDir) {
13397
13756
  }
13398
13757
  async function readBoard(projectDir, scriptsDir = frayScriptsDir()) {
13399
13758
  if (!directFrayRoot(projectDir)) throw new Error("unsafe or missing .fray directory");
13400
- const { stdout } = await execFileP("node", [join10(scriptsDir, "index.mjs"), "--json"], {
13759
+ const { stdout } = await execFileP("node", [join11(scriptsDir, "index.mjs"), "--json"], {
13401
13760
  cwd: projectDir,
13402
13761
  env: { ...process.env, CLAUDE_PROJECT_DIR: projectDir },
13403
13762
  maxBuffer: 32 * 1024 * 1024
@@ -13422,7 +13781,7 @@ async function readBoard(projectDir, scriptsDir = frayScriptsDir()) {
13422
13781
  };
13423
13782
  }
13424
13783
  async function runThreadUpdate(projectDir, slug, args, scriptsDir = frayScriptsDir()) {
13425
- await execFileP("node", [join10(scriptsDir, "thread-update.mjs"), slug, ...args], {
13784
+ await execFileP("node", [join11(scriptsDir, "thread-update.mjs"), slug, ...args], {
13426
13785
  cwd: projectDir,
13427
13786
  env: { ...process.env, CLAUDE_PROJECT_DIR: projectDir }
13428
13787
  });
@@ -13437,12 +13796,12 @@ var init_fray = __esm({
13437
13796
  });
13438
13797
 
13439
13798
  // packages/server/src/session-files.ts
13440
- import { lstatSync as lstatSync2, rmSync as rmSync4 } from "node:fs";
13799
+ import { lstatSync as lstatSync2, rmSync as rmSync5 } from "node:fs";
13441
13800
  import { tmpdir as tmpdir2 } from "node:os";
13442
- import { join as join11 } from "node:path";
13801
+ import { join as join12 } from "node:path";
13443
13802
  function systemPromptPath(sessionId) {
13444
13803
  if (!SESSION_ID_RE.test(sessionId)) throw new Error("invalid session id");
13445
- return join11(SYSTEM_PROMPT_DIR, `${sessionId}.md`);
13804
+ return join12(SYSTEM_PROMPT_DIR, `${sessionId}.md`);
13446
13805
  }
13447
13806
  function isDirectDirectory(path) {
13448
13807
  try {
@@ -13462,9 +13821,9 @@ function pathAbsent(path) {
13462
13821
  }
13463
13822
  function unlinkDirectChild(parent, filename) {
13464
13823
  if (!isDirectDirectory(parent)) return pathAbsent(parent);
13465
- const child = join11(parent, filename);
13824
+ const child = join12(parent, filename);
13466
13825
  try {
13467
- rmSync4(child, { force: true });
13826
+ rmSync5(child, { force: true });
13468
13827
  } catch {
13469
13828
  return false;
13470
13829
  }
@@ -13472,14 +13831,14 @@ function unlinkDirectChild(parent, filename) {
13472
13831
  }
13473
13832
  function cleanupAdoptionSessionFiles(projectDir, sessionId) {
13474
13833
  if (!SESSION_ID_RE.test(sessionId)) return false;
13475
- const frayDir = join11(projectDir, ".fray");
13834
+ const frayDir = join12(projectDir, ".fray");
13476
13835
  let clean = true;
13477
13836
  if (isDirectDirectory(frayDir)) {
13478
- const threads = join11(frayDir, "threads");
13837
+ const threads = join12(frayDir, "threads");
13479
13838
  if (isDirectDirectory(threads)) {
13480
- const child = join11(threads, sessionId);
13839
+ const child = join12(threads, sessionId);
13481
13840
  try {
13482
- rmSync4(child, { recursive: true, force: true });
13841
+ rmSync5(child, { recursive: true, force: true });
13483
13842
  } catch {
13484
13843
  clean = false;
13485
13844
  }
@@ -13492,7 +13851,7 @@ var SYSTEM_PROMPT_DIR, SESSION_ID_RE;
13492
13851
  var init_session_files = __esm({
13493
13852
  "packages/server/src/session-files.ts"() {
13494
13853
  "use strict";
13495
- SYSTEM_PROMPT_DIR = join11(tmpdir2(), "fray-sysprompts");
13854
+ SYSTEM_PROMPT_DIR = join12(tmpdir2(), "fray-sysprompts");
13496
13855
  SESSION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,199}$/;
13497
13856
  }
13498
13857
  });
@@ -13729,8 +14088,8 @@ var init_adoption_recovery = __esm({
13729
14088
  });
13730
14089
 
13731
14090
  // packages/server/src/dispatch.ts
13732
- import { closeSync as closeSync5, constants, existsSync as existsSync3, fstatSync, lstatSync as lstatSync3, openSync as openSync5, readFileSync as readFileSync8, realpathSync as realpathSync5, statSync as statSync6, writeFileSync as writeFileSync4, renameSync as renameSync5, mkdirSync as mkdirSync7, rmSync as rmSync5 } from "node:fs";
13733
- import { basename as basename4, join as join12, dirname as dirname6 } from "node:path";
14091
+ import { closeSync as closeSync5, constants, existsSync as existsSync3, fstatSync, lstatSync as lstatSync3, openSync as openSync6, readFileSync as readFileSync8, realpathSync as realpathSync5, statSync as statSync7, writeFileSync as writeFileSync5, renameSync as renameSync5, mkdirSync as mkdirSync8, rmSync as rmSync6 } from "node:fs";
14092
+ import { basename as basename4, join as join13, dirname as dirname6 } from "node:path";
13734
14093
  import { fileURLToPath as fileURLToPath2 } from "node:url";
13735
14094
  import { createHash as createHash6, randomUUID as randomUUID11 } from "node:crypto";
13736
14095
  function fallbackTitle(prompt) {
@@ -13750,7 +14109,7 @@ function fallbackTitle(prompt) {
13750
14109
  }
13751
14110
  function resolveSlug(frayDir, base, taken) {
13752
14111
  base = ThreadSlug.parse(base);
13753
- const isTaken = (slug) => existsSync3(join12(frayDir, `${slug}.md`)) || (taken?.(slug) ?? false);
14112
+ const isTaken = (slug) => existsSync3(join13(frayDir, `${slug}.md`)) || (taken?.(slug) ?? false);
13754
14113
  if (!isTaken(base)) return base;
13755
14114
  for (let n = 2; ; n++) {
13756
14115
  const suffix = `-${n}`;
@@ -13767,12 +14126,12 @@ function resolveLegacyThreadFile(projectDir, value) {
13767
14126
  if (!parsed.success) return null;
13768
14127
  try {
13769
14128
  const projectRoot = realpathSync5(projectDir);
13770
- const frayPath = join12(projectRoot, ".fray");
14129
+ const frayPath = join13(projectRoot, ".fray");
13771
14130
  const frayStat = lstatSync3(frayPath);
13772
14131
  if (!frayStat.isDirectory() || frayStat.isSymbolicLink()) return null;
13773
14132
  const realFray = realpathSync5(frayPath);
13774
14133
  if (dirname6(realFray) !== projectRoot || basename4(realFray) !== ".fray") return null;
13775
- const path = join12(realFray, `${parsed.data}.md`);
14134
+ const path = join13(realFray, `${parsed.data}.md`);
13776
14135
  const before = lstatSync3(path);
13777
14136
  if (!before.isFile() || before.isSymbolicLink()) return null;
13778
14137
  const realPath = realpathSync5(path);
@@ -13780,7 +14139,7 @@ function resolveLegacyThreadFile(projectDir, value) {
13780
14139
  let contents;
13781
14140
  let openedBefore;
13782
14141
  let openedAfter;
13783
- const fd = openSync5(path, constants.O_RDONLY | constants.O_NOFOLLOW);
14142
+ const fd = openSync6(path, constants.O_RDONLY | constants.O_NOFOLLOW);
13784
14143
  try {
13785
14144
  openedBefore = fstatSync(fd);
13786
14145
  contents = readFileSync8(fd);
@@ -13816,9 +14175,9 @@ function boardAuthorizesAdoption(board, slug) {
13816
14175
  return !board.errorItems.some((item) => item.file === `${slug}.md`);
13817
14176
  }
13818
14177
  function ensureSafeDirectDirectory(parent, name) {
13819
- const path = join12(parent, name);
14178
+ const path = join13(parent, name);
13820
14179
  try {
13821
- mkdirSync7(path);
14180
+ mkdirSync8(path);
13822
14181
  } catch (error) {
13823
14182
  const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
13824
14183
  if (code !== "EEXIST") throw error;
@@ -13889,14 +14248,14 @@ function writeScratchpad(projectDir, sessionId, title, kind = "claude") {
13889
14248
  const threadsDir = ensureSafeDirectDirectory(frayDir, "threads");
13890
14249
  const dir = ensureSafeDirectDirectory(threadsDir, sessionId);
13891
14250
  const rel = scratchpadRelPath(sessionId);
13892
- const path = join12(dir, "scratch.md");
13893
- const tmp = join12(dir, ".scratch.tmp");
14251
+ const path = join13(dir, "scratch.md");
14252
+ const tmp = join13(dir, ".scratch.tmp");
13894
14253
  try {
13895
- writeFileSync4(tmp, scratchpadContent(title, kind), { flag: "wx", mode: 384 });
14254
+ writeFileSync5(tmp, scratchpadContent(title, kind), { flag: "wx", mode: 384 });
13896
14255
  if (existsSync3(path)) throw new Error("scratchpad already exists");
13897
14256
  renameSync5(tmp, path);
13898
14257
  } catch (error) {
13899
- rmSync5(tmp, { force: true });
14258
+ rmSync6(tmp, { force: true });
13900
14259
  throw error;
13901
14260
  }
13902
14261
  return rel;
@@ -13914,8 +14273,9 @@ function codexScratchpadHookConfig(hookScript, sessionId) {
13914
14273
  }
13915
14274
  ]
13916
14275
  });
13917
- const bashBackgroundHook = join12(dirname6(hookScript), "bash-background.mjs");
13918
- const scratchpadStopHook = join12(dirname6(hookScript), "scratchpad-stop.mjs");
14276
+ const bashBackgroundHook = join13(dirname6(hookScript), "bash-background.mjs");
14277
+ const scratchpadStopHook = join13(dirname6(hookScript), "scratchpad-stop.mjs");
14278
+ const fenceStopHook = join13(dirname6(hookScript), "fence-stop.mjs");
13919
14279
  return {
13920
14280
  bypass_hook_trust: true,
13921
14281
  hooks: {
@@ -13930,12 +14290,19 @@ function codexScratchpadHookConfig(hookScript, sessionId) {
13930
14290
  }]
13931
14291
  }],
13932
14292
  // An optional scratchpad-frontmatter reminder can keep a worker from forgetting owned work when
13933
- // it tries to rest. The hook itself persists the two-minute anti-loop cooldown.
14293
+ // it tries to rest, and the fence check catches a rest that hands a decision back in prose
14294
+ // instead of a ```question card. Each hook persists its own anti-loop guard.
13934
14295
  Stop: [{
13935
- hooks: [{
13936
- type: "command",
13937
- command: `node ${JSON.stringify(scratchpadStopHook)} --session=${JSON.stringify(sessionId)}`
13938
- }]
14296
+ hooks: [
14297
+ {
14298
+ type: "command",
14299
+ command: `node ${JSON.stringify(scratchpadStopHook)} --session=${JSON.stringify(sessionId)}`
14300
+ },
14301
+ {
14302
+ type: "command",
14303
+ command: `node ${JSON.stringify(fenceStopHook)} --session=${JSON.stringify(sessionId)}`
14304
+ }
14305
+ ]
13939
14306
  }],
13940
14307
  // Native Codex children inherit the root scratchpad mandate even with `fork_turns:"none"`.
13941
14308
  // Constrain it structurally at child start: shared writes, but only merge-style scoped edits.
@@ -13948,10 +14315,10 @@ function codexScratchpadHookConfig(hookScript, sessionId) {
13948
14315
  }
13949
14316
  function scratchpadHookScript() {
13950
14317
  const plugin = workerPluginDir();
13951
- return plugin ? join12(plugin, "hooks", "scratchpad.mjs") : void 0;
14318
+ return plugin ? join13(plugin, "hooks", "scratchpad.mjs") : void 0;
13952
14319
  }
13953
14320
  function composePrompt(sessionId, prompt, kind = "claude") {
13954
- const scratch = kind === "codex" ? `Your scratchpad is \`.fray/threads/${sessionId}/scratch.md\` \u2014 the CANONICAL record of this thread, your compaction-survival mechanism, and a shared progress document for native sub-agents. Keep your task list, the approach and what you rejected, the human's decisions, and anything that must outlive your context IN it, written as you go; re-read it after any compaction or resume before asserting anything. Each native sub-agent should merge its own scoped progress into it rather than leaving the root as its sole writer, but must re-read before each edit, preserve all existing content, and never delete, truncate, reinitialize, move, or replace the whole file.` : `Your scratchpad is \`.fray/threads/${sessionId}/scratch.md\` \u2014 the CANONICAL record of this thread, your compaction-survival mechanism, and the shared blackboard for your sub-agents. Keep your task list, the approach and what you rejected, the human's decisions, and anything that must outlive your context IN it, written as you go; re-read it after any compaction or resume, and pass its path to every sub-agent you dispatch. Each sub-agent should merge its own scoped progress into it rather than leaving the root as its sole writer, but must re-read before each edit, preserve all existing content, and never delete, truncate, reinitialize, move, or replace the whole file.`;
14321
+ const scratch = kind === "codex" ? `Your scratchpad is \`.fray/threads/${sessionId}/scratch.md\` \u2014 an OPTIONAL scratch file kept for you, not a deliverable. A single direct task usually needs nothing in it, and writing in it never substitutes for doing the work. On a long effort it is useful crash insurance and a shared progress document for native sub-agents: keep the approach, what you rejected, and the human's decisions in it as you go, mid-work, then keep working; re-read it after any compaction or resume before asserting anything. Each native sub-agent should merge its own scoped progress into it rather than leaving the root as its sole writer, but must re-read before each edit, preserve all existing content, and never delete, truncate, reinitialize, move, or replace the whole file.` : `Your scratchpad is \`.fray/threads/${sessionId}/scratch.md\` \u2014 an OPTIONAL scratch file kept for you, not a deliverable. A single direct task usually needs nothing in it, and writing in it never substitutes for doing the work. On a long effort it is useful crash insurance and the shared blackboard for your sub-agents: keep the approach, what you rejected, and the human's decisions in it as you go, mid-work, then keep working; re-read it after any compaction or resume, and pass its path to every sub-agent you dispatch. Each sub-agent should merge its own scoped progress into it rather than leaving the root as its sole writer, but must re-read before each edit, preserve all existing content, and never delete, truncate, reinitialize, move, or replace the whole file.`;
13955
14322
  const handoff = "\n\nEverything above the banner below is fray system orientation. Everything below it is the human operator's own prompt, verbatim \u2014 that, and nothing else, is your task.";
13956
14323
  return `${scratch}${handoff}
13957
14324
 
@@ -13959,16 +14326,16 @@ function composePrompt(sessionId, prompt, kind = "claude") {
13959
14326
  ${DISPATCH_TASK_BANNER_MARKER}${prompt}`;
13960
14327
  }
13961
14328
  function scratchpadOrientation(sessionId, planPath, kind = "claude") {
13962
- const scratch = kind === "codex" ? `SCRATCHPAD: .fray/threads/${sessionId}/scratch.md \u2014 the CANONICAL record of this thread, your compaction-survival mechanism, and a shared progress document for native sub-agents (write your task list, the approach and what you rejected, and anything that must outlive your context there, as you go; re-read it after any compaction or resume). Each native sub-agent should merge its own scoped progress into it rather than leaving the root as its sole writer, but must re-read before each edit, preserve all existing content, and never delete, truncate, reinitialize, move, or replace the whole file.` : `SCRATCHPAD: .fray/threads/${sessionId}/scratch.md \u2014 the CANONICAL record of this thread, your compaction-survival mechanism, and the shared blackboard for your sub-agents (write your task list, the approach and what you rejected, and anything that must outlive your context there, as you go; re-read it after any compaction or resume; pass this path in every sub-agent prompt). Each sub-agent should merge its own scoped progress into it rather than leaving the root as its sole writer, but must re-read before each edit, preserve all existing content, and never delete, truncate, reinitialize, move, or replace the whole file.`;
14329
+ const scratch = kind === "codex" ? `SCRATCHPAD (optional): .fray/threads/${sessionId}/scratch.md \u2014 a scratch file kept FOR YOU, not a deliverable. A single direct task usually needs nothing in it; writing in it is never a substitute for doing the work. Useful on a long effort as crash insurance, and a shared progress document for native sub-agents (write your task list, the approach and what you rejected, and anything that must outlive your context there, as you go; re-read it after any compaction or resume). Each native sub-agent should merge its own scoped progress into it rather than leaving the root as its sole writer, but must re-read before each edit, preserve all existing content, and never delete, truncate, reinitialize, move, or replace the whole file.` : `SCRATCHPAD (optional): .fray/threads/${sessionId}/scratch.md \u2014 a scratch file kept FOR YOU, not a deliverable. A single direct task usually needs nothing in it; writing in it is never a substitute for doing the work. Useful on a long effort as crash insurance, and the shared blackboard for your sub-agents (write your task list, the approach and what you rejected, and anything that must outlive your context there, as you go; re-read it after any compaction or resume; pass this path in every sub-agent prompt). Each sub-agent should merge its own scoped progress into it rather than leaving the root as its sole writer, but must re-read before each edit, preserve all existing content, and never delete, truncate, reinitialize, move, or replace the whole file.`;
13963
14330
  const lines = [scratch];
13964
14331
  if (planPath) lines.push(`PLAN: ${planPath} \u2014 the durable plan artifact this thread works from; read it FIRST.`);
13965
14332
  return lines.join("\n");
13966
14333
  }
13967
14334
  function frayConfigBlock(projectDir) {
13968
- const path = join12(projectDir, "FRAY.md");
14335
+ const path = join13(projectDir, "FRAY.md");
13969
14336
  let body;
13970
14337
  try {
13971
- const st = statSync6(path);
14338
+ const st = statSync7(path);
13972
14339
  if (!st.isFile() || st.size > FRAY_MD_MAX_BYTES) return "";
13973
14340
  body = readFileSync8(path, "utf8").trim();
13974
14341
  } catch {
@@ -13984,7 +14351,7 @@ ${clipped}`;
13984
14351
  }
13985
14352
  function validPlanPath(projectDir, planPath) {
13986
14353
  if (!planPath || !PLAN_PATH_RE.test(planPath)) return null;
13987
- return existsSync3(join12(projectDir, planPath)) ? planPath : null;
14354
+ return existsSync3(join13(projectDir, planPath)) ? planPath : null;
13988
14355
  }
13989
14356
  function workerPermissionMode(m) {
13990
14357
  return m === "plan" ? "auto" : m;
@@ -13996,15 +14363,15 @@ function effectivePermissionMode(kind, mode) {
13996
14363
  }
13997
14364
  function systemPromptFlags(sessionId, system) {
13998
14365
  if (!system) return [];
13999
- mkdirSync7(SYSTEM_PROMPT_DIR, { recursive: true });
14366
+ mkdirSync8(SYSTEM_PROMPT_DIR, { recursive: true });
14000
14367
  const path = systemPromptPath(sessionId);
14001
- writeFileSync4(path, system);
14368
+ writeFileSync5(path, system);
14002
14369
  return ["--append-system-prompt-file", path];
14003
14370
  }
14004
14371
  function resolveFrayMcp(stateDir, moduleUrl = import.meta.url, env = process.env) {
14005
14372
  const pluginDir = resolveWorkerPluginDir(moduleUrl, env);
14006
14373
  if (!pluginDir) return void 0;
14007
- const scriptPath = join12(pluginDir, "bin", FRAY_MCP.script);
14374
+ const scriptPath = join13(pluginDir, "bin", FRAY_MCP.script);
14008
14375
  if (!existsSync3(scriptPath)) return void 0;
14009
14376
  return { scriptPath, stateDir };
14010
14377
  }
@@ -14042,12 +14409,12 @@ function buildClaudeCommand(opts) {
14042
14409
  }
14043
14410
  function resolveWorkerPluginDir(moduleUrl = import.meta.url, env = process.env) {
14044
14411
  const override = env.FRAY_WORKER_PLUGIN_DIR;
14045
- if (override && existsSync3(join12(override, ".claude-plugin", "plugin.json")))
14412
+ if (override && existsSync3(join13(override, ".claude-plugin", "plugin.json")))
14046
14413
  return override;
14047
14414
  let current = dirname6(fileURLToPath2(moduleUrl));
14048
14415
  for (; ; ) {
14049
- const candidate = join12(current, "cc-worker");
14050
- if (existsSync3(join12(candidate, ".claude-plugin", "plugin.json"))) return candidate;
14416
+ const candidate = join13(current, "cc-worker");
14417
+ if (existsSync3(join13(candidate, ".claude-plugin", "plugin.json"))) return candidate;
14051
14418
  const parent = dirname6(current);
14052
14419
  if (parent === current) return void 0;
14053
14420
  current = parent;
@@ -14091,7 +14458,7 @@ function createDispatcher(deps) {
14091
14458
  const killPane2 = deps.killPane ?? killPane;
14092
14459
  const killSession2 = deps.killSession ?? killSession;
14093
14460
  const readBoardSource = deps.readBoard ?? readBoard;
14094
- const frayDir = join12(deps.project.dir, ".fray");
14461
+ const frayDir = join13(deps.project.dir, ".fray");
14095
14462
  const adoptionRuntime = deps.adoptionRuntime ?? {
14096
14463
  lookupAdoptionPane,
14097
14464
  findAdoptionPane,
@@ -14131,14 +14498,14 @@ function createDispatcher(deps) {
14131
14498
  }
14132
14499
  function writePrewrites(built) {
14133
14500
  for (const file of built.prewrite) {
14134
- if (file.mode === void 0) writeFileSync4(file.path, file.contents);
14135
- else writeFileSync4(file.path, file.contents, { mode: file.mode });
14501
+ if (file.mode === void 0) writeFileSync5(file.path, file.contents);
14502
+ else writeFileSync5(file.path, file.contents, { mode: file.mode });
14136
14503
  }
14137
14504
  }
14138
14505
  function cleanupPrewrites(built) {
14139
14506
  for (const path of new Set(built.prewrite.map((file) => file.path))) {
14140
14507
  try {
14141
- rmSync5(path, { force: true });
14508
+ rmSync6(path, { force: true });
14142
14509
  } catch {
14143
14510
  }
14144
14511
  }
@@ -14146,7 +14513,7 @@ function createDispatcher(deps) {
14146
14513
  function cleanupDispatchFiles(scratchRel, built, sessionId) {
14147
14514
  cleanupPrewrites(built);
14148
14515
  try {
14149
- rmSync5(join12(deps.project.dir, scratchRel), { force: true });
14516
+ rmSync6(join13(deps.project.dir, scratchRel), { force: true });
14150
14517
  } catch {
14151
14518
  }
14152
14519
  cleanupAdoptionSessionFiles(deps.project.dir, sessionId);
@@ -14934,10 +15301,10 @@ var init_permission_controller = __esm({
14934
15301
  });
14935
15302
 
14936
15303
  // packages/server/src/backend/codex-models.ts
14937
- import { join as join13 } from "node:path";
15304
+ import { join as join14 } from "node:path";
14938
15305
  import { readFileSync as readFileSync9 } from "node:fs";
14939
15306
  function cachePath(codexHome) {
14940
- return join13(codexHome, "models_cache.json");
15307
+ return join14(codexHome, "models_cache.json");
14941
15308
  }
14942
15309
  function toCodexModel(raw2) {
14943
15310
  if (!raw2 || typeof raw2 !== "object") return void 0;
@@ -15699,7 +16066,7 @@ var init_delivery_ledger = __esm({
15699
16066
  });
15700
16067
 
15701
16068
  // packages/server/src/codex-subagents.ts
15702
- import { closeSync as closeSync6, openSync as openSync6, readSync as readSync2, statSync as statSync7 } from "node:fs";
16069
+ import { closeSync as closeSync6, openSync as openSync7, readSync as readSync2, statSync as statSync8 } from "node:fs";
15703
16070
  function createCodexSubAgentTracker(deps) {
15704
16071
  const findRollouts = deps.findRollouts ?? ((ids) => findRolloutsByIds(ids, deps.codexHome));
15705
16072
  const readAppended = deps.readAppended ?? defaultReadAppended;
@@ -15882,7 +16249,7 @@ function agentLabel(path) {
15882
16249
  function defaultReadAppended(path, offset) {
15883
16250
  let size;
15884
16251
  try {
15885
- size = statSync7(path).size;
16252
+ size = statSync8(path).size;
15886
16253
  } catch {
15887
16254
  return void 0;
15888
16255
  }
@@ -15890,7 +16257,7 @@ function defaultReadAppended(path, offset) {
15890
16257
  const from = restarted ? 0 : offset;
15891
16258
  if (size <= from) return { text: "", offset: from, restarted };
15892
16259
  try {
15893
- const fd = openSync6(path, "r");
16260
+ const fd = openSync7(path, "r");
15894
16261
  try {
15895
16262
  const buf = Buffer.allocUnsafe(size - from);
15896
16263
  const read = readSync2(fd, buf, 0, buf.length, from);
@@ -15993,15 +16360,15 @@ var init_completion_relay = __esm({
15993
16360
 
15994
16361
  // packages/server/src/tail-cache.ts
15995
16362
  import { createHash as createHash7 } from "node:crypto";
15996
- import { closeSync as closeSync7, openSync as openSync7, readFileSync as readFileSync10, readSync as readSync3, statSync as statSync8 } from "node:fs";
15997
- import { join as join14 } from "node:path";
16363
+ import { closeSync as closeSync7, openSync as openSync8, readFileSync as readFileSync10, readSync as readSync3, statSync as statSync9 } from "node:fs";
16364
+ import { join as join15 } from "node:path";
15998
16365
  function foldSchemaDigest(dir = import.meta.dirname) {
15999
16366
  if (foldSchemaMemo) return foldSchemaMemo;
16000
16367
  const hash = createHash7("sha256").update("fray-tail-state-v1\0");
16001
16368
  let read = 0;
16002
16369
  for (const name of FOLD_SOURCES) {
16003
16370
  try {
16004
- hash.update(name).update("\0").update(readFileSync10(join14(dir, name)));
16371
+ hash.update(name).update("\0").update(readFileSync10(join15(dir, name)));
16005
16372
  read++;
16006
16373
  } catch {
16007
16374
  hash.update(name).update("\0missing\0");
@@ -16031,9 +16398,9 @@ function measureFence(path, offset) {
16031
16398
  if (!Number.isSafeInteger(offset) || offset <= 0) return null;
16032
16399
  let fd;
16033
16400
  try {
16034
- const stat2 = statSync8(path);
16401
+ const stat2 = statSync9(path);
16035
16402
  if (stat2.size < offset) return null;
16036
- fd = openSync7(path, "r");
16403
+ fd = openSync8(path, "r");
16037
16404
  const window = Math.min(SAMPLE_BYTES, offset);
16038
16405
  const head = Buffer.allocUnsafe(window);
16039
16406
  const headRead = readSync3(fd, head, 0, window, 0);
@@ -16197,9 +16564,9 @@ var init_tail_cache = __esm({
16197
16564
  });
16198
16565
 
16199
16566
  // packages/server/src/tailer.ts
16200
- import { statSync as statSync9, openSync as openSync8, readSync as readSync4, closeSync as closeSync8, readdirSync as readdirSync3, mkdirSync as mkdirSync8, writeFileSync as writeFileSync5, readFileSync as readFileSync11 } from "node:fs";
16201
- import { join as join15 } from "node:path";
16202
- import { homedir as homedir5, tmpdir as tmpdir3 } from "node:os";
16567
+ import { statSync as statSync10, openSync as openSync9, readSync as readSync4, closeSync as closeSync8, readdirSync as readdirSync4, mkdirSync as mkdirSync9, writeFileSync as writeFileSync6, readFileSync as readFileSync11 } from "node:fs";
16568
+ import { join as join16 } from "node:path";
16569
+ import { homedir as homedir7, tmpdir as tmpdir3 } from "node:os";
16203
16570
  function activityMinute(at) {
16204
16571
  if (!at) return "";
16205
16572
  const ms = Date.parse(at);
@@ -16453,7 +16820,7 @@ function launchTaskId(text) {
16453
16820
  function readDescendantSidecars(sessionDir, mtimeMs) {
16454
16821
  let names;
16455
16822
  try {
16456
- names = readdirSync3(join15(sessionDir, "subagents"));
16823
+ names = readdirSync4(join16(sessionDir, "subagents"));
16457
16824
  } catch {
16458
16825
  return [];
16459
16826
  }
@@ -16464,7 +16831,7 @@ function readDescendantSidecars(sessionDir, mtimeMs) {
16464
16831
  if (!agentId) continue;
16465
16832
  let parsed;
16466
16833
  try {
16467
- parsed = JSON.parse(readFileSync11(join15(sessionDir, "subagents", name), "utf8"));
16834
+ parsed = JSON.parse(readFileSync11(join16(sessionDir, "subagents", name), "utf8"));
16468
16835
  } catch {
16469
16836
  continue;
16470
16837
  }
@@ -16478,7 +16845,7 @@ function readDescendantSidecars(sessionDir, mtimeMs) {
16478
16845
  agentType: text(meta.agentType),
16479
16846
  parentAgentId: text(meta.parentAgentId),
16480
16847
  spawnDepth: typeof meta.spawnDepth === "number" && Number.isFinite(meta.spawnDepth) ? meta.spawnDepth : void 0,
16481
- spawnedAtMs: mtimeMs(join15(sessionDir, "subagents", name))
16848
+ spawnedAtMs: mtimeMs(join16(sessionDir, "subagents", name))
16482
16849
  });
16483
16850
  }
16484
16851
  return out;
@@ -16914,13 +17281,13 @@ function isPermMarker(v) {
16914
17281
  }
16915
17282
  function defaultMtimeMs(path) {
16916
17283
  try {
16917
- return statSync9(path).mtimeMs;
17284
+ return statSync10(path).mtimeMs;
16918
17285
  } catch {
16919
17286
  return void 0;
16920
17287
  }
16921
17288
  }
16922
17289
  function defaultLogDir(project) {
16923
- return join15(homedir5(), ".claude", "projects", project.cwdSlug);
17290
+ return join16(homedir7(), ".claude", "projects", project.cwdSlug);
16924
17291
  }
16925
17292
  function defaultReadPermMarker(project) {
16926
17293
  if (!project.stateDir) return () => void 0;
@@ -17069,7 +17436,7 @@ function createTailer(deps) {
17069
17436
  return captured.kind === "captured" ? captured.text : "";
17070
17437
  }
17071
17438
  const defaultBackend = {
17072
- transcriptPath: (sessionId) => join15(logDir, `${sessionId}.jsonl`),
17439
+ transcriptPath: (sessionId) => join16(logDir, `${sessionId}.jsonl`),
17073
17440
  foldLine: (state, line) => {
17074
17441
  const rec = parseLine(line);
17075
17442
  if (rec) applyRecord(state, rec);
@@ -17171,7 +17538,7 @@ ${ask}`;
17171
17538
  return state.path.replace(/\.jsonl$/, "");
17172
17539
  }
17173
17540
  function descendantSidecars(state) {
17174
- const at = mtimeMs(join15(sessionDirOf(state), "subagents"));
17541
+ const at = mtimeMs(join16(sessionDirOf(state), "subagents"));
17175
17542
  const cached = descendantIndex.get(state.slug);
17176
17543
  if (cached && cached.at === at) return cached.all;
17177
17544
  const all = readDescendantSidecars(sessionDirOf(state), mtimeMs);
@@ -17185,7 +17552,7 @@ ${ask}`;
17185
17552
  return descendantIndex.get(state.slug)?.byToolUse.get(id);
17186
17553
  }
17187
17554
  function descendantTranscript(state, meta) {
17188
- return join15(sessionDirOf(state), "subagents", `agent-${meta.agentId}.jsonl`);
17555
+ return join16(sessionDirOf(state), "subagents", `agent-${meta.agentId}.jsonl`);
17189
17556
  }
17190
17557
  function descendantState(state, meta) {
17191
17558
  const task = deps.runtimeTasks?.(state.sessionId).find((entry) => entry.taskId === meta.agentId);
@@ -17410,7 +17777,7 @@ ${ask}`;
17410
17777
  function scanForeign(nowMs) {
17411
17778
  let names;
17412
17779
  try {
17413
- names = readdirSync3(logDir);
17780
+ names = readdirSync4(logDir);
17414
17781
  } catch {
17415
17782
  return [];
17416
17783
  }
@@ -17425,10 +17792,10 @@ ${ask}`;
17425
17792
  if (name.startsWith(".") || !name.endsWith(".jsonl")) continue;
17426
17793
  const id = name.slice(0, -".jsonl".length);
17427
17794
  if (!id || registered.has(id)) continue;
17428
- const path = join15(logDir, name);
17795
+ const path = join16(logDir, name);
17429
17796
  let mtime;
17430
17797
  try {
17431
- mtime = statSync9(path).mtimeMs;
17798
+ mtime = statSync10(path).mtimeMs;
17432
17799
  } catch {
17433
17800
  continue;
17434
17801
  }
@@ -17618,7 +17985,7 @@ ${ask}`;
17618
17985
  function consume(state, backend, onLine) {
17619
17986
  let size;
17620
17987
  try {
17621
- size = statSync9(state.path).size;
17988
+ size = statSync10(state.path).size;
17622
17989
  } catch {
17623
17990
  return;
17624
17991
  }
@@ -17629,7 +17996,7 @@ ${ask}`;
17629
17996
  if (size <= state.offset) return;
17630
17997
  let chunk = "";
17631
17998
  try {
17632
- const fd = openSync8(state.path, "r");
17999
+ const fd = openSync9(state.path, "r");
17633
18000
  try {
17634
18001
  const buf = Buffer.allocUnsafe(size - state.offset);
17635
18002
  const read = readSync4(fd, buf, 0, buf.length, state.offset);
@@ -17669,13 +18036,14 @@ ${ask}`;
17669
18036
  const authFailure = row.backend !== "codex" && isClaudeAuthErrorText(pane);
17670
18037
  if (authFailure) state.authFault = "authentication_rejected";
17671
18038
  const detail = authFailure ? "(claude authentication failure \u2014 pane content redacted; sign in and retry)" : pane.trim() || "(pane empty / unavailable)";
17672
- console.error(
17673
- `[fray-ui] thread ${row.slug} (session ${row.session_id}): no transcript ${DISCOVERY_GRACE_MS / 1e3}s after dispatch \u2014 likely a boot failure. Pane:
18039
+ log.error(
18040
+ "tailer",
18041
+ `thread ${row.slug} (session ${row.session_id}): no transcript ${DISCOVERY_GRACE_MS / 1e3}s after dispatch \u2014 likely a boot failure. Pane:
17674
18042
  ${detail.slice(0, 4e3)}`
17675
18043
  );
17676
18044
  try {
17677
- mkdirSync8(STALL_LOG_DIR, { recursive: true });
17678
- writeFileSync5(join15(STALL_LOG_DIR, `${row.slug}.stall.log`), `session_id: ${row.session_id}
18045
+ mkdirSync9(STALL_LOG_DIR, { recursive: true });
18046
+ writeFileSync6(join16(STALL_LOG_DIR, `${row.slug}.stall.log`), `session_id: ${row.session_id}
17679
18047
  captured_at: ${new Date(now()).toISOString()}
17680
18048
 
17681
18049
  ${detail}
@@ -17687,7 +18055,7 @@ ${detail}
17687
18055
  if (state.offset > 0) return true;
17688
18056
  let size = 0;
17689
18057
  try {
17690
- size = statSync9(state.path).size;
18058
+ size = statSync10(state.path).size;
17691
18059
  } catch {
17692
18060
  size = 0;
17693
18061
  }
@@ -17714,7 +18082,7 @@ ${detail}
17714
18082
  committed = false;
17715
18083
  }
17716
18084
  if (!committed) return false;
17717
- state.path = join15(logDir, `${found}.jsonl`);
18085
+ state.path = join16(logDir, `${found}.jsonl`);
17718
18086
  state.offset = 0;
17719
18087
  state.partial = "";
17720
18088
  state.primed = false;
@@ -17742,7 +18110,7 @@ ${detail}
17742
18110
  let state = states.get(row.slug);
17743
18111
  const runtimeGeneration = row.runtime_generation ?? 0;
17744
18112
  if (!state || state.sessionId !== row.session_id || state.nativeSessionId !== nativeId || state.runtimeGeneration !== runtimeGeneration) {
17745
- const path = backend.transcriptPath(nativeId) ?? join15(logDir, `${nativeId}.jsonl`);
18113
+ const path = backend.transcriptPath(nativeId) ?? join16(logDir, `${nativeId}.jsonl`);
17746
18114
  state = newTailState(row.slug, row.session_id, path, false, nativeId, runtimeGeneration);
17747
18115
  hydrateFromCache(state, row, nativeId);
17748
18116
  states.set(row.slug, state);
@@ -17981,8 +18349,9 @@ ${detail}
17981
18349
  if (elapsed > POLL_MS3) {
17982
18350
  overBudgetTicks++;
17983
18351
  if (overBudgetTicks === 1 || overBudgetTicks % 30 === 0) {
17984
- console.error(
17985
- `[fray-ui] tailer tick took ${Math.round(elapsed)}ms (poll ${POLL_MS3}ms, ${states.size} sessions) \u2014 the event loop is blocked for that long, so RPCs and board pushes are delayed (occurrence ${overBudgetTicks})`
18352
+ log.warn(
18353
+ "tailer",
18354
+ `tick took ${Math.round(elapsed)}ms (poll ${POLL_MS3}ms, ${states.size} sessions) \u2014 the event loop is blocked for that long, so RPCs and board pushes are delayed (occurrence ${overBudgetTicks})`
17986
18355
  );
17987
18356
  }
17988
18357
  }
@@ -17992,7 +18361,7 @@ ${detail}
17992
18361
  function reportTickFailure(error) {
17993
18362
  tickFailures++;
17994
18363
  if (tickFailures === 1 || tickFailures % 50 === 0) {
17995
- console.error(`[fray-ui] tailer tick threw (occurrence ${tickFailures}; the loop keeps running): ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
18364
+ log.error("tailer", `tick threw (occurrence ${tickFailures}; the loop keeps running): ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
17996
18365
  }
17997
18366
  }
17998
18367
  function scheduleTick() {
@@ -18112,6 +18481,7 @@ var init_tailer = __esm({
18112
18481
  init_codex_subagents();
18113
18482
  init_completion_relay();
18114
18483
  init_tail_cache();
18484
+ init_logging();
18115
18485
  IDLE_BACKSTOP_MS = 5e3;
18116
18486
  RUNTIME_CHASE_MAX = 20;
18117
18487
  POLL_MS3 = 1e3;
@@ -18125,7 +18495,7 @@ var init_tailer = __esm({
18125
18495
  CACHE_FLUSH_MS = 3e4;
18126
18496
  PRIME_PROGRESS_EVERY = 20;
18127
18497
  DISCOVER_RETRY_MS = 15e3;
18128
- STALL_LOG_DIR = join15(tmpdir3(), "fray-worker-logs");
18498
+ STALL_LOG_DIR = join16(tmpdir3(), "fray-worker-logs");
18129
18499
  PERM_YES_OPTION = /(^|\n)\s*(❯\s*)?1\.\s+Yes\b/;
18130
18500
  PERM_QUESTION = /\b(?:Do you want|Would you like)\b/;
18131
18501
  PERM_FOOTER = /\bEsc to (cancel|reject)\b/;
@@ -18185,14 +18555,14 @@ var init_tailer = __esm({
18185
18555
  });
18186
18556
 
18187
18557
  // packages/server/src/backend/codex.ts
18188
- import { join as join16 } from "node:path";
18189
- import { homedir as homedir6 } from "node:os";
18190
- import { readdirSync as readdirSync4, statSync as statSync10 } from "node:fs";
18558
+ import { join as join17 } from "node:path";
18559
+ import { homedir as homedir8 } from "node:os";
18560
+ import { readdirSync as readdirSync5, statSync as statSync11 } from "node:fs";
18191
18561
  function defaultCodexHome() {
18192
- return process.env.CODEX_HOME && process.env.CODEX_HOME.trim() ? process.env.CODEX_HOME : join16(homedir6(), ".codex");
18562
+ return process.env.CODEX_HOME && process.env.CODEX_HOME.trim() ? process.env.CODEX_HOME : join17(homedir8(), ".codex");
18193
18563
  }
18194
18564
  function sessionsDir(codexHome) {
18195
- return join16(codexHome, "sessions");
18565
+ return join17(codexHome, "sessions");
18196
18566
  }
18197
18567
  function codexSandbox(mode) {
18198
18568
  switch (mode) {
@@ -18609,23 +18979,23 @@ function collectRollouts(dir, out, budget) {
18609
18979
  const files = entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).sort(descByName);
18610
18980
  for (const d of dirs) {
18611
18981
  if (budget.n <= 0) return;
18612
- collectRollouts(join16(dir, d.name), out, budget);
18982
+ collectRollouts(join17(dir, d.name), out, budget);
18613
18983
  }
18614
18984
  for (const f of files) {
18615
18985
  if (budget.n <= 0) return;
18616
18986
  let mtimeMs;
18617
18987
  try {
18618
- mtimeMs = statSync10(join16(dir, f.name)).mtimeMs;
18988
+ mtimeMs = statSync11(join17(dir, f.name)).mtimeMs;
18619
18989
  } catch {
18620
18990
  continue;
18621
18991
  }
18622
- out.push({ path: join16(dir, f.name), mtimeMs });
18992
+ out.push({ path: join17(dir, f.name), mtimeMs });
18623
18993
  budget.n--;
18624
18994
  }
18625
18995
  }
18626
18996
  function safeReaddir(dir) {
18627
18997
  try {
18628
- return readdirSync4(dir, { withFileTypes: true });
18998
+ return readdirSync5(dir, { withFileTypes: true });
18629
18999
  } catch {
18630
19000
  return [];
18631
19001
  }
@@ -22473,23 +22843,64 @@ var init_wrapper = __esm({
22473
22843
  }
22474
22844
  });
22475
22845
 
22846
+ // packages/server/src/backend/codex-mcp.ts
22847
+ function tomlString(value) {
22848
+ return JSON.stringify(value);
22849
+ }
22850
+ function inlineTable(entries) {
22851
+ return `{${entries.map(([k, v]) => `${k}=${v}`).join(",")}}`;
22852
+ }
22853
+ function serverTable(command, args, env) {
22854
+ const entries = [
22855
+ ["command", tomlString(command)],
22856
+ ["args", `[${args.map(tomlString).join(",")}]`]
22857
+ ];
22858
+ if (env && Object.keys(env).length > 0) {
22859
+ entries.push(["env", inlineTable(Object.entries(env).map(([k, v]) => [k, tomlString(v)]))]);
22860
+ }
22861
+ return inlineTable(entries);
22862
+ }
22863
+ function codexMcpConfigArgs(frayMcp, nodeBin = process.execPath) {
22864
+ const args = [
22865
+ "-c",
22866
+ `mcp_servers.${CHROME_DEVTOOLS_MCP.name}=${serverTable(CHROME_DEVTOOLS_MCP.command, CHROME_DEVTOOLS_MCP.args)}`
22867
+ ];
22868
+ if (frayMcp) {
22869
+ args.push(
22870
+ "-c",
22871
+ `mcp_servers.${FRAY_MCP.name}=${serverTable(nodeBin, [frayMcp.scriptPath], { FRAY_STATE_DIR: frayMcp.stateDir })}`
22872
+ );
22873
+ }
22874
+ args.push("-c", `default_tools_approval_mode=${tomlString("approve")}`);
22875
+ return args;
22876
+ }
22877
+ function codexAppServerArgv(transport, frayMcp) {
22878
+ return ["app-server", ...codexMcpConfigArgs(frayMcp), ...transport];
22879
+ }
22880
+ var init_codex_mcp = __esm({
22881
+ "packages/server/src/backend/codex-mcp.ts"() {
22882
+ "use strict";
22883
+ init_types2();
22884
+ }
22885
+ });
22886
+
22476
22887
  // packages/server/src/backend/codex-app-server-native.ts
22477
22888
  import { spawn as spawn3 } from "node:child_process";
22478
22889
  import { connect } from "node:net";
22479
22890
  import { createHash as createHash8, randomUUID as randomUUID12 } from "node:crypto";
22480
- import { existsSync as existsSync4, mkdirSync as mkdirSync9, readFileSync as readFileSync12, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "node:fs";
22481
- import { join as join17 } from "node:path";
22891
+ import { existsSync as existsSync4, mkdirSync as mkdirSync10, readFileSync as readFileSync12, unlinkSync as unlinkSync2, writeFileSync as writeFileSync7 } from "node:fs";
22892
+ import { join as join18 } from "node:path";
22482
22893
  import { PassThrough, Writable } from "node:stream";
22483
22894
  import { StringDecoder } from "node:string_decoder";
22484
22895
  function nativeDir(stateDir) {
22485
- return join17(stateDir, "codex-app-server-native");
22896
+ return join18(stateDir, "codex-app-server-native");
22486
22897
  }
22487
22898
  function nativeRecordPath(stateDir, projectId) {
22488
- return join17(nativeDir(stateDir), `${projectId}.json`);
22899
+ return join18(nativeDir(stateDir), `${projectId}.json`);
22489
22900
  }
22490
22901
  function nativeListenSocketPath(stateDir, projectId) {
22491
22902
  const key = createHash8("sha256").update(stateDir).update("\0").update(projectId).digest("hex").slice(0, 16);
22492
- return join17(process.env.TMPDIR ?? "/tmp", `fray-codex-native-${key}.sock`);
22903
+ return join18(process.env.TMPDIR ?? "/tmp", `fray-codex-native-${key}.sock`);
22493
22904
  }
22494
22905
  function pidAlive2(pid) {
22495
22906
  try {
@@ -22648,7 +23059,7 @@ function attach(record, timeoutMs) {
22648
23059
  }
22649
23060
  async function startListener(options) {
22650
23061
  const { stateDir, projectId } = options;
22651
- mkdirSync9(nativeDir(stateDir), { recursive: true });
23062
+ mkdirSync10(nativeDir(stateDir), { recursive: true });
22652
23063
  const socketPath = nativeListenSocketPath(stateDir, projectId);
22653
23064
  if (existsSync4(socketPath) && !await socketAccepting(socketPath)) {
22654
23065
  try {
@@ -22657,7 +23068,7 @@ async function startListener(options) {
22657
23068
  }
22658
23069
  }
22659
23070
  const generation = randomUUID12();
22660
- const child = spawn3(options.codexBin, ["app-server", "--listen", `unix://${socketPath}`], {
23071
+ const child = spawn3(options.codexBin, codexAppServerArgv(["--listen", `unix://${socketPath}`], options.frayMcp), {
22661
23072
  cwd: options.cwd,
22662
23073
  env: options.env,
22663
23074
  detached: true,
@@ -22671,7 +23082,7 @@ async function startListener(options) {
22671
23082
  socketPath,
22672
23083
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
22673
23084
  };
22674
- writeFileSync6(nativeRecordPath(stateDir, projectId), JSON.stringify(record));
23085
+ writeFileSync7(nativeRecordPath(stateDir, projectId), JSON.stringify(record));
22675
23086
  const deadline = Date.now() + (options.timeoutMs ?? 3e4);
22676
23087
  for (; ; ) {
22677
23088
  if (existsSync4(socketPath) && await socketAccepting(socketPath)) return record;
@@ -22685,6 +23096,8 @@ var init_codex_app_server_native = __esm({
22685
23096
  "packages/server/src/backend/codex-app-server-native.ts"() {
22686
23097
  "use strict";
22687
23098
  init_wrapper();
23099
+ init_codex_mcp();
23100
+ init_logging();
22688
23101
  PRESUMED_LOSSY_REJOIN = 1;
22689
23102
  nativeListenCodexAppServerHost = async (options) => {
22690
23103
  const timeoutMs = options.timeoutMs ?? 3e4;
@@ -22725,8 +23138,8 @@ var init_codex_app_server_native = __esm({
22725
23138
  droppedWhileDetached: PRESUMED_LOSSY_REJOIN
22726
23139
  };
22727
23140
  }
22728
- console.error(`[fray-ui] codex app-server native listener unavailable (${error.message}); falling back to an in-process app-server \u2014 turns will NOT survive a fray restart`);
22729
- const child = spawn3(options.codexBin, ["app-server", "--stdio"], { cwd: options.cwd, env: options.env, stdio: ["pipe", "pipe", "pipe"] });
23141
+ log.error("codex", `codex app-server native listener unavailable (${error.message}); falling back to an in-process app-server \u2014 turns will NOT survive a fray restart`);
23142
+ const child = spawn3(options.codexBin, codexAppServerArgv(["--stdio"], options.frayMcp), { cwd: options.cwd, env: options.env, stdio: ["pipe", "pipe", "pipe"] });
22730
23143
  return { process: child, generation: randomUUID12(), reattached: false, daemonPid: process.pid, droppedWhileDetached: 0 };
22731
23144
  }
22732
23145
  };
@@ -22737,20 +23150,20 @@ var init_codex_app_server_native = __esm({
22737
23150
  import { spawn as spawn4 } from "node:child_process";
22738
23151
  import { createConnection } from "node:net";
22739
23152
  import { createHash as createHash9, randomUUID as randomUUID13 } from "node:crypto";
22740
- import { existsSync as existsSync5, mkdirSync as mkdirSync10, readFileSync as readFileSync13, unlinkSync as unlinkSync3 } from "node:fs";
22741
- import { join as join18 } from "node:path";
23153
+ import { existsSync as existsSync5, mkdirSync as mkdirSync11, readFileSync as readFileSync13, unlinkSync as unlinkSync3 } from "node:fs";
23154
+ import { join as join19 } from "node:path";
22742
23155
  import { PassThrough as PassThrough2, Writable as Writable2 } from "node:stream";
22743
23156
  import { StringDecoder as StringDecoder2 } from "node:string_decoder";
22744
23157
  function daemonDir(stateDir) {
22745
- return join18(stateDir, "codex-app-server");
23158
+ return join19(stateDir, "codex-app-server");
22746
23159
  }
22747
23160
  function recordPath(stateDir, projectId) {
22748
- return join18(daemonDir(stateDir), `${projectId}.json`);
23161
+ return join19(daemonDir(stateDir), `${projectId}.json`);
22749
23162
  }
22750
23163
  function codexAppServerSocketPath(stateDir, projectId) {
22751
23164
  const key = createHash9("sha256").update(stateDir).update("\0").update(projectId).digest("hex").slice(0, 16);
22752
23165
  if (process.platform === "win32") return `\\\\.\\pipe\\fray-codex-${key}`;
22753
- return join18(process.env.TMPDIR ?? "/tmp", `fray-codex-${key}.sock`);
23166
+ return join19(process.env.TMPDIR ?? "/tmp", `fray-codex-${key}.sock`);
22754
23167
  }
22755
23168
  function pidAlive3(pid) {
22756
23169
  try {
@@ -22834,7 +23247,7 @@ async function stopCodexAppServerDaemon(stateDir, projectId, timeoutMs = 1e4) {
22834
23247
  }
22835
23248
  function forkDaemon(options) {
22836
23249
  const { stateDir, projectId } = options;
22837
- mkdirSync10(daemonDir(stateDir), { recursive: true });
23250
+ mkdirSync11(daemonDir(stateDir), { recursive: true });
22838
23251
  const record = recordPath(stateDir, projectId);
22839
23252
  try {
22840
23253
  unlinkSync3(record);
@@ -22854,6 +23267,10 @@ function forkDaemon(options) {
22854
23267
  generation,
22855
23268
  clientInfo: options.clientInfo,
22856
23269
  capabilities: options.capabilities,
23270
+ // The argv is built HERE, not in the daemon: the daemon is a bare forked entry that should not have
23271
+ // to resolve fray's MCP descriptors, and building it once keeps this transport identical to the
23272
+ // native listener's.
23273
+ appServerArgs: codexAppServerArgv(["--stdio"], options.frayMcp),
22857
23274
  ...options.reachabilityCheckMs === void 0 ? {} : { reachabilityCheckMs: options.reachabilityCheckMs }
22858
23275
  });
22859
23276
  const child = spawn4(process.execPath, [options.daemonEntry ?? daemonEntry()], {
@@ -22964,7 +23381,7 @@ function attach2(record, timeoutMs) {
22964
23381
  });
22965
23382
  }
22966
23383
  function inProcessCodexAppServer(options) {
22967
- const child = spawn4(options.codexBin, ["app-server", "--stdio"], {
23384
+ const child = spawn4(options.codexBin, codexAppServerArgv(["--stdio"], options.frayMcp), {
22968
23385
  cwd: options.cwd,
22969
23386
  env: options.env,
22970
23387
  stdio: ["pipe", "pipe", "pipe"]
@@ -22973,7 +23390,7 @@ function inProcessCodexAppServer(options) {
22973
23390
  }
22974
23391
  function directChildHost(spawnChild, generationId = randomUUID13) {
22975
23392
  return async (options) => ({
22976
- process: spawnChild(options.codexBin, ["app-server", "--stdio"], { cwd: options.cwd, env: options.env }),
23393
+ process: spawnChild(options.codexBin, codexAppServerArgv(["--stdio"], options.frayMcp), { cwd: options.cwd, env: options.env }),
22977
23394
  generation: generationId(),
22978
23395
  reattached: false,
22979
23396
  daemonPid: process.pid,
@@ -22986,6 +23403,8 @@ var init_codex_app_server_host = __esm({
22986
23403
  "use strict";
22987
23404
  init_detached_daemons();
22988
23405
  init_codex_app_server_native();
23406
+ init_codex_mcp();
23407
+ init_logging();
22989
23408
  daemonEntry = () => resolveDetachedDaemonEntry(import.meta.url, "codex-app-server-daemon");
22990
23409
  daemonCodexAppServerHost = async (options) => {
22991
23410
  const existing = liveDaemonRecord(options.stateDir, options.projectId);
@@ -23015,7 +23434,7 @@ var init_codex_app_server_host = __esm({
23015
23434
  const attached = await attach2(record, timeoutMs);
23016
23435
  return { ...attached, generation: record.generation, reattached: false, daemonPid: record.daemonPid };
23017
23436
  } catch (error) {
23018
- console.error(`[fray-ui] codex app-server daemon unavailable (${error.message}); falling back to an in-process app-server \u2014 turns will NOT survive a fray restart`);
23437
+ log.error("codex", `codex app-server daemon unavailable (${error.message}); falling back to an in-process app-server \u2014 turns will NOT survive a fray restart`);
23019
23438
  return inProcessCodexAppServer(options);
23020
23439
  }
23021
23440
  };
@@ -23579,6 +23998,7 @@ var init_codex_app_server = __esm({
23579
23998
  init_credential_redaction();
23580
23999
  init_codex_app_server_host();
23581
24000
  init_codex_app_server_native();
24001
+ init_logging();
23582
24002
  CODEX_APP_SERVER_PROVIDER = "codex-app-server";
23583
24003
  CODEX_NATIVE_LISTEN_FLAG = "FRAY_CODEX_NATIVE_LISTEN";
23584
24004
  CODEX_APP_SERVER_SUPPORTED_VERSION = "0.146.0";
@@ -24904,7 +25324,8 @@ var init_codex_app_server = __esm({
24904
25324
  // environment; no value is copied into argv, SQLite, diagnostics, or logs.
24905
25325
  env: codexAppServerEnvironment(),
24906
25326
  clientInfo: CLIENT_INFO,
24907
- capabilities: CLIENT_CAPABILITIES
25327
+ capabilities: CLIENT_CAPABILITIES,
25328
+ frayMcp: this.options.frayMcp
24908
25329
  });
24909
25330
  const child = attachment.process;
24910
25331
  let connection;
@@ -24943,7 +25364,7 @@ var init_codex_app_server = __esm({
24943
25364
  });
24944
25365
  if (version !== aheadVersionWarned) {
24945
25366
  aheadVersionWarned = version;
24946
- console.warn(`[fray-ui] ${verdict.message}`);
25367
+ log.warn("codex", verdict.message);
24947
25368
  }
24948
25369
  }
24949
25370
  handshaking = false;
@@ -25814,17 +26235,17 @@ var init_codex_app_server = __esm({
25814
26235
  });
25815
26236
 
25816
26237
  // packages/server/src/backend/codex-quota.ts
25817
- import { join as join19 } from "node:path";
25818
- import { readdirSync as readdirSync5, statSync as statSync11, openSync as openSync9, readSync as readSync5, fstatSync as fstatSync2, closeSync as closeSync9 } from "node:fs";
26238
+ import { join as join20 } from "node:path";
26239
+ import { readdirSync as readdirSync6, statSync as statSync12, openSync as openSync10, readSync as readSync5, fstatSync as fstatSync2, closeSync as closeSync9 } from "node:fs";
25819
26240
  import { spawn as spawn5 } from "node:child_process";
25820
26241
  function sessionsDir2(codexHome) {
25821
- return join19(codexHome, "sessions");
26242
+ return join20(codexHome, "sessions");
25822
26243
  }
25823
26244
  function newestRollouts(dir, out, budget) {
25824
26245
  if (budget.n <= 0) return;
25825
26246
  let entries;
25826
26247
  try {
25827
- entries = readdirSync5(dir, { withFileTypes: true });
26248
+ entries = readdirSync6(dir, { withFileTypes: true });
25828
26249
  } catch {
25829
26250
  return;
25830
26251
  }
@@ -25832,18 +26253,18 @@ function newestRollouts(dir, out, budget) {
25832
26253
  const files = entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).map((e) => e.name).sort(descByName2);
25833
26254
  for (const d of dirs) {
25834
26255
  if (budget.n <= 0) return;
25835
- newestRollouts(join19(dir, d), out, budget);
26256
+ newestRollouts(join20(dir, d), out, budget);
25836
26257
  }
25837
26258
  for (const f of files) {
25838
26259
  if (budget.n <= 0) return;
25839
- out.push(join19(dir, f));
26260
+ out.push(join20(dir, f));
25840
26261
  budget.n--;
25841
26262
  }
25842
26263
  }
25843
26264
  function readTail(path) {
25844
26265
  let fd;
25845
26266
  try {
25846
- fd = openSync9(path, "r");
26267
+ fd = openSync10(path, "r");
25847
26268
  const size = fstatSync2(fd).size;
25848
26269
  const start = Math.max(0, size - TAIL_BYTES);
25849
26270
  const len = size - start;
@@ -25908,7 +26329,7 @@ function readCodexQuotaFromRollouts(codexHome = defaultCodexHome()) {
25908
26329
  newestRollouts(sessionsDir2(codexHome), rollouts, { n: 8 });
25909
26330
  const byMtime = rollouts.map((path) => {
25910
26331
  try {
25911
- return { path, mtimeMs: statSync11(path).mtimeMs };
26332
+ return { path, mtimeMs: statSync12(path).mtimeMs };
25912
26333
  } catch {
25913
26334
  return void 0;
25914
26335
  }
@@ -26084,7 +26505,7 @@ async function readQuota(opts = {}) {
26084
26505
  if (fx && process.env.NODE_ENV !== "production") {
26085
26506
  const snap = await fixture(fx);
26086
26507
  if (snap) {
26087
- console.warn(`[fray-ui] quota: serving FIXTURE "${fx}" (FRAY_QUOTA_FIXTURE set; dev-only seam)`);
26508
+ log.warn("quota", `serving FIXTURE "${fx}" (FRAY_QUOTA_FIXTURE set; dev-only seam)`);
26088
26509
  return snap;
26089
26510
  }
26090
26511
  }
@@ -26099,6 +26520,7 @@ var init_quota = __esm({
26099
26520
  "use strict";
26100
26521
  init_codex_quota();
26101
26522
  init_claude_quota();
26523
+ init_logging();
26102
26524
  }
26103
26525
  });
26104
26526
 
@@ -26108,20 +26530,20 @@ import {
26108
26530
  constants as constants2,
26109
26531
  fstatSync as fstatSync3,
26110
26532
  lstatSync as lstatSync4,
26111
- openSync as openSync10,
26533
+ openSync as openSync11,
26112
26534
  readFileSync as readFileSync14,
26113
- readdirSync as readdirSync6,
26535
+ readdirSync as readdirSync7,
26114
26536
  realpathSync as realpathSync6,
26115
26537
  unlinkSync as unlinkSync4
26116
26538
  } from "node:fs";
26117
26539
  import { createHash as createHash11 } from "node:crypto";
26118
- import { basename as basename5, dirname as dirname7, join as join20 } from "node:path";
26540
+ import { basename as basename5, dirname as dirname7, join as join21 } from "node:path";
26119
26541
  function sameStat(a, b) {
26120
26542
  return a.dev === b.dev && a.ino === b.ino && a.mode === b.mode && a.size === b.size && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
26121
26543
  }
26122
26544
  function directDirectory(parentRealPath, name) {
26123
26545
  try {
26124
- const path = join20(parentRealPath, name);
26546
+ const path = join21(parentRealPath, name);
26125
26547
  const stat2 = lstatSync4(path);
26126
26548
  if (!stat2.isDirectory() || stat2.isSymbolicLink()) return null;
26127
26549
  const realPath = realpathSync6(path);
@@ -26159,7 +26581,7 @@ function resolvePlanFile(projectDir, value, hooks = {}) {
26159
26581
  const directoryBefore = planDirectory(projectDir);
26160
26582
  if (!directoryBefore) return null;
26161
26583
  hooks.afterDirectoryCheck?.();
26162
- const path = join20(directoryBefore.plans.realPath, filename);
26584
+ const path = join21(directoryBefore.plans.realPath, filename);
26163
26585
  const before = lstatSync4(path);
26164
26586
  if (!before.isFile() || before.isSymbolicLink()) return null;
26165
26587
  const realPath = realpathSync6(path);
@@ -26168,7 +26590,7 @@ function resolvePlanFile(projectDir, value, hooks = {}) {
26168
26590
  let contents;
26169
26591
  let openedBefore;
26170
26592
  let openedAfter;
26171
- const fd = openSync10(path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
26593
+ const fd = openSync11(path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
26172
26594
  try {
26173
26595
  openedBefore = fstatSync3(fd);
26174
26596
  contents = readFileSync14(fd);
@@ -26213,7 +26635,7 @@ function listPlanFiles(projectDir, hooks = {}) {
26213
26635
  if (!directoryBefore) return [];
26214
26636
  try {
26215
26637
  hooks.afterDirectoryCheck?.();
26216
- const names = readdirSync6(directoryBefore.plans.realPath).sort();
26638
+ const names = readdirSync7(directoryBefore.plans.realPath).sort();
26217
26639
  const files = names.flatMap((filename) => {
26218
26640
  const relativePath = `.fray/plans/${filename}`;
26219
26641
  if (!filenameFromPlanPath(relativePath)) return [];
@@ -26238,7 +26660,7 @@ import {
26238
26660
  existsSync as existsSync6,
26239
26661
  watch as fsWatch
26240
26662
  } from "node:fs";
26241
- import { join as join21 } from "node:path";
26663
+ import { join as join22 } from "node:path";
26242
26664
  import watcher from "@parcel/watcher";
26243
26665
  function appServerTurnStalled(liveness, lastActivityAt, nowMs) {
26244
26666
  if (!liveness) return false;
@@ -26339,7 +26761,7 @@ function deriveAwaitingBackground(row, tele, runtime, hasActionableInteraction =
26339
26761
  }
26340
26762
  function scratchpadPathIfExists(projectDir, sessionId) {
26341
26763
  const rel = scratchpadRelPath(sessionId);
26342
- return existsSync6(join21(projectDir, rel)) ? rel : void 0;
26764
+ return existsSync6(join22(projectDir, rel)) ? rel : void 0;
26343
26765
  }
26344
26766
  function resolveSessionProfile(row, tele) {
26345
26767
  const persistedModel = row.model?.trim() || void 0;
@@ -26698,7 +27120,7 @@ function createBoard(project, storage, bus, tailer, bootId, deps = {}) {
26698
27120
  if (parcelSub || stopped) return Promise.resolve();
26699
27121
  if (watchSetup) return watchSetup;
26700
27122
  const setup = (async () => {
26701
- const next = await subscribe(join21(project.dir, ".fray"), () => scheduleRebuild());
27123
+ const next = await subscribe(join22(project.dir, ".fray"), () => scheduleRebuild());
26702
27124
  if (stopped) {
26703
27125
  await next.unsubscribe();
26704
27126
  return;
@@ -27496,7 +27918,7 @@ function createScheduler(deps) {
27496
27918
  const autoResumeOnLimit = deps.autoResumeOnLimit ?? (() => true);
27497
27919
  const fetchPr = deps.fetchPr ?? defaultFetchPr;
27498
27920
  const fetchGithubReview = deps.fetchGithubReview ?? createGithubReviewFetcher({ now });
27499
- const log = deps.log ?? ((m) => console.log(`[fray-ui] ${m}`));
27921
+ const log2 = deps.log ?? ((m) => log.info("scheduler", m));
27500
27922
  const tickMs = deps.tickMs ?? 1e4;
27501
27923
  const pollMs = deps.pollMs ?? 6e4;
27502
27924
  const deliveryLeaseMs = Math.max(1, deps.deliveryLeaseMs ?? 3e4);
@@ -27726,14 +28148,14 @@ function createScheduler(deps) {
27726
28148
  return;
27727
28149
  }
27728
28150
  const suppressed = prior?.suppressed ? ` (${prior.suppressed} identical repeats suppressed)` : "";
27729
- log(`waker: GitHub review check failed for ${key} (${slug}) [${result.failure.kind}] \u2014 ${result.failure.message}${suppressed}`);
28151
+ log2(`waker: GitHub review check failed for ${key} (${slug}) [${result.failure.kind}] \u2014 ${result.failure.message}${suppressed}`);
27730
28152
  reviewFailures.set(key, { signature, loggedAt: at, suppressed: 0 });
27731
28153
  }
27732
28154
  function recordReviewSuccess(key, slug) {
27733
28155
  const prior = reviewFailures.get(key);
27734
28156
  if (!prior) return;
27735
28157
  const suppressed = prior.suppressed ? `; ${prior.suppressed} identical repeats were suppressed` : "";
27736
- log(`waker: GitHub review check recovered for ${key} (${slug})${suppressed}`);
28158
+ log2(`waker: GitHub review check recovered for ${key} (${slug})${suppressed}`);
27737
28159
  reviewFailures.delete(key);
27738
28160
  }
27739
28161
  async function evalThread(slug, sessionId, fence, nowMs, fenceAt) {
@@ -27781,9 +28203,9 @@ function createScheduler(deps) {
27781
28203
  try {
27782
28204
  const s = await fetchPr(ref);
27783
28205
  if (s) st.prCache.set(k, s);
27784
- else log(`waker: gh check skipped for ${k} (${slug}) \u2014 gh unavailable / not authed / rate-limited`);
28206
+ else log2(`waker: gh check skipped for ${k} (${slug}) \u2014 gh unavailable / not authed / rate-limited`);
27785
28207
  } catch (err) {
27786
- log(`waker: gh check errored for ${k} (${slug}): ${err instanceof Error ? err.message : String(err)}`);
28208
+ log2(`waker: gh check errored for ${k} (${slug}): ${err instanceof Error ? err.message : String(err)}`);
27787
28209
  }
27788
28210
  }),
27789
28211
  ...[...reviewRefs].map(async ([k, ref]) => {
@@ -27828,7 +28250,7 @@ function createScheduler(deps) {
27828
28250
  }, nowMs).delivery;
27829
28251
  st.fired = true;
27830
28252
  if (isPrWatchHint(h.kind)) deps.storage.setSnoozedUntil(slug, null);
27831
- log(`waker: queued ${slug} \u2014 ${verdict.reason}`);
28253
+ log2(`waker: queued ${slug} \u2014 ${verdict.reason}`);
27832
28254
  checkpoint("after-enqueue", item);
27833
28255
  return;
27834
28256
  }
@@ -27879,7 +28301,7 @@ function createScheduler(deps) {
27879
28301
  try {
27880
28302
  quota = await deps.readQuota();
27881
28303
  } catch (err) {
27882
- log(`waker: quota read failed while checking limit resumes: ${err instanceof Error ? err.message : String(err)}`);
28304
+ log2(`waker: quota read failed while checking limit resumes: ${err instanceof Error ? err.message : String(err)}`);
27883
28305
  }
27884
28306
  }
27885
28307
  for (const c of candidates) {
@@ -27896,7 +28318,7 @@ function createScheduler(deps) {
27896
28318
  message: limitResumeSteer(c.fault.window),
27897
28319
  reason: `${c.fault.window} usage limit reset (interrupted ${c.fault.at})`
27898
28320
  }, nowMs).delivery;
27899
- log(`waker: queued ${c.slug} \u2014 ${item.reason}`);
28321
+ log2(`waker: queued ${c.slug} \u2014 ${item.reason}`);
27900
28322
  checkpoint("after-enqueue", item);
27901
28323
  }
27902
28324
  }
@@ -27919,7 +28341,7 @@ function createScheduler(deps) {
27919
28341
  message: relayMessage(report),
27920
28342
  reason: `sub-agent report never reached the model (${report.summary ?? report.taskId})`
27921
28343
  }, nowMs).delivery;
27922
- log(`waker: queued ${row.slug} \u2014 ${item.reason}`);
28344
+ log2(`waker: queued ${row.slug} \u2014 ${item.reason}`);
27923
28345
  checkpoint("after-enqueue", item);
27924
28346
  }
27925
28347
  }
@@ -27940,7 +28362,7 @@ function createScheduler(deps) {
27940
28362
  message: armed.prompt,
27941
28363
  reason: `snooze elapsed (${armed.until})`
27942
28364
  }, nowMs).delivery;
27943
- log(`waker: queued ${row.slug} \u2014 ${item.reason}`);
28365
+ log2(`waker: queued ${row.slug} \u2014 ${item.reason}`);
27944
28366
  checkpoint("after-enqueue", item);
27945
28367
  }
27946
28368
  }
@@ -27975,7 +28397,7 @@ function createScheduler(deps) {
27975
28397
  );
27976
28398
  if (recovered?.state === "exhausted") {
27977
28399
  settleSnooze(item);
27978
- log(`waker: delivery EXHAUSTED for ${item.slug} after ${recovered.attempts} attempts \u2014 ${recovered.lastError ?? "unknown error"}`);
28400
+ log2(`waker: delivery EXHAUSTED for ${item.slug} after ${recovered.attempts} attempts \u2014 ${recovered.lastError ?? "unknown error"}`);
27979
28401
  }
27980
28402
  }
27981
28403
  }
@@ -28015,7 +28437,7 @@ function createScheduler(deps) {
28015
28437
  if (error?.terminalDelivery === true) {
28016
28438
  outbox.supersede(item.id, failedAt, message);
28017
28439
  settleSnooze(item);
28018
- log(`waker: delivery ABANDONED for ${item.slug} (terminal, no retry): ${message}`);
28440
+ log2(`waker: delivery ABANDONED for ${item.slug} (terminal, no retry): ${message}`);
28019
28441
  continue;
28020
28442
  }
28021
28443
  outbox.deferFailure(
@@ -28025,14 +28447,14 @@ function createScheduler(deps) {
28025
28447
  failedAt + Math.max(deliveryLeaseMs, retryDelay(item.attempts)),
28026
28448
  message
28027
28449
  );
28028
- log(`waker: delivery FAILED for ${item.slug} (attempt ${item.attempts} of ${maxDeliveryAttempts}): ${message}`);
28450
+ log2(`waker: delivery FAILED for ${item.slug} (attempt ${item.attempts} of ${maxDeliveryAttempts}): ${message}`);
28029
28451
  continue;
28030
28452
  }
28031
28453
  const retried = item.attempts > 1 ? ` (on attempt ${item.attempts})` : "";
28032
- log(`waker: delivered ${item.slug} \u2014 ${item.reason}${retried}`);
28454
+ log2(`waker: delivered ${item.slug} \u2014 ${item.reason}${retried}`);
28033
28455
  checkpoint("after-delivery", item);
28034
28456
  if (!outbox.acknowledge(item.id, deliveryOwner, now())) {
28035
- log(`waker: delivery acknowledgement lost ownership for ${item.slug}; preserving the authoritative terminal state`);
28457
+ log2(`waker: delivery acknowledgement lost ownership for ${item.slug}; preserving the authoritative terminal state`);
28036
28458
  continue;
28037
28459
  }
28038
28460
  settleSnooze(item);
@@ -28058,7 +28480,7 @@ function createScheduler(deps) {
28058
28480
  await evalThread(row.slug, row.session_id, fence, nowMs, fenceAt);
28059
28481
  } catch (err) {
28060
28482
  if (err instanceof InjectedSchedulerCrash) throw err;
28061
- log(`waker: tick error for ${row.slug}: ${err instanceof Error ? err.message : String(err)}`);
28483
+ log2(`waker: tick error for ${row.slug}: ${err instanceof Error ? err.message : String(err)}`);
28062
28484
  }
28063
28485
  }));
28064
28486
  const crashed = candidateResults.find((result) => result.status === "rejected");
@@ -28075,19 +28497,19 @@ function createScheduler(deps) {
28075
28497
  await evalLimits(now());
28076
28498
  } catch (err) {
28077
28499
  if (err instanceof InjectedSchedulerCrash) throw err;
28078
- log(`waker: limit-resume pass failed: ${err instanceof Error ? err.message : String(err)}`);
28500
+ log2(`waker: limit-resume pass failed: ${err instanceof Error ? err.message : String(err)}`);
28079
28501
  }
28080
28502
  try {
28081
28503
  evalSnoozes(now());
28082
28504
  } catch (err) {
28083
28505
  if (err instanceof InjectedSchedulerCrash) throw err;
28084
- log(`waker: snooze-bump pass failed: ${err instanceof Error ? err.message : String(err)}`);
28506
+ log2(`waker: snooze-bump pass failed: ${err instanceof Error ? err.message : String(err)}`);
28085
28507
  }
28086
28508
  try {
28087
28509
  repairDroppedReports(now());
28088
28510
  } catch (err) {
28089
28511
  if (err instanceof InjectedSchedulerCrash) throw err;
28090
- log(`waker: report-repair pass failed: ${err instanceof Error ? err.message : String(err)}`);
28512
+ log2(`waker: report-repair pass failed: ${err instanceof Error ? err.message : String(err)}`);
28091
28513
  }
28092
28514
  reconcileOutbox(now());
28093
28515
  await deliverDue();
@@ -28111,9 +28533,9 @@ function createScheduler(deps) {
28111
28533
  start() {
28112
28534
  if (timer) return;
28113
28535
  if (stopped) throw new ProducerStoppedError("wake scheduler");
28114
- void tick().catch((error) => log(`waker: tick failed: ${error instanceof Error ? error.message : String(error)}`));
28536
+ void tick().catch((error) => log2(`waker: tick failed: ${error instanceof Error ? error.message : String(error)}`));
28115
28537
  timer = setInterval(() => {
28116
- void tick().catch((error) => log(`waker: tick failed: ${error instanceof Error ? error.message : String(error)}`));
28538
+ void tick().catch((error) => log2(`waker: tick failed: ${error instanceof Error ? error.message : String(error)}`));
28117
28539
  }, tickMs);
28118
28540
  timer.unref?.();
28119
28541
  },
@@ -28138,6 +28560,7 @@ var init_scheduler = __esm({
28138
28560
  init_completion_relay();
28139
28561
  init_github_review();
28140
28562
  init_github_review();
28563
+ init_logging();
28141
28564
  execFileAsync5 = promisify6(execFile6);
28142
28565
  PR_REF_RE = /(?:https?:\/\/github\.com\/)?([A-Za-z0-9][\w.-]*)\/([A-Za-z0-9][\w.-]*?)(?:\/pull\/|\/pulls\/|#)(\d+)/;
28143
28566
  LIMIT_FENCE_PREFIX = "limit";
@@ -28157,7 +28580,7 @@ var init_scheduler = __esm({
28157
28580
  });
28158
28581
 
28159
28582
  // packages/server/src/resume.ts
28160
- import { writeFileSync as writeFileSync7 } from "node:fs";
28583
+ import { writeFileSync as writeFileSync8 } from "node:fs";
28161
28584
  import { randomUUID as randomUUID16 } from "node:crypto";
28162
28585
  import { setTimeout as delay2 } from "node:timers/promises";
28163
28586
  function permissionModeForRow(row, settings) {
@@ -28198,7 +28621,7 @@ function spawnPinnedSession(deps, tx, row, permissionMode, message, options, lau
28198
28621
  env: claudeWorkerEnvironment(),
28199
28622
  prewrite: []
28200
28623
  };
28201
- for (const f of built.prewrite) writeFileSync7(f.path, f.contents);
28624
+ for (const f of built.prewrite) writeFileSync8(f.path, f.contents);
28202
28625
  return tx.spawn(row.slug, built.argv, deps.project.dir, {
28203
28626
  ...built.env,
28204
28627
  FRAY_UI_THREAD: row.slug,
@@ -29217,7 +29640,7 @@ var init_resume = __esm({
29217
29640
  });
29218
29641
 
29219
29642
  // packages/server/src/backend/claude.ts
29220
- import { join as join22 } from "node:path";
29643
+ import { join as join23 } from "node:path";
29221
29644
  function toolResultText2(content) {
29222
29645
  if (typeof content === "string") return content;
29223
29646
  if (!Array.isArray(content)) return "";
@@ -29306,7 +29729,7 @@ function createClaudeBackend(opts) {
29306
29729
  return { argv, env: claudeWorkerEnvironment(), prewrite: [] };
29307
29730
  },
29308
29731
  transcriptPath(sessionId) {
29309
- return join22(opts.logDir, `${sessionId}.jsonl`);
29732
+ return join23(opts.logDir, `${sessionId}.jsonl`);
29310
29733
  },
29311
29734
  parseLine(line) {
29312
29735
  return parseClaudeLine(line);
@@ -30277,27 +30700,27 @@ var init_profile_controller = __esm({
30277
30700
  });
30278
30701
 
30279
30702
  // packages/server/src/backend/codex-app-server-diagnostics.ts
30280
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync11, renameSync as renameSync6, statSync as statSync12 } from "node:fs";
30281
- import { join as join23 } from "node:path";
30703
+ import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync12, renameSync as renameSync6, statSync as statSync13 } from "node:fs";
30704
+ import { join as join24 } from "node:path";
30282
30705
  function codexDiagnosticLogPath(stateDir, projectId) {
30283
- return join23(stateDir, "codex-app-server", `${projectId}.diagnostics.log`);
30706
+ return join24(stateDir, "codex-app-server", `${projectId}.diagnostics.log`);
30284
30707
  }
30285
30708
  function createCodexDiagnosticSink(stateDir, projectId, now = () => /* @__PURE__ */ new Date()) {
30286
- const dir = join23(stateDir, "codex-app-server");
30709
+ const dir = join24(stateDir, "codex-app-server");
30287
30710
  const path = codexDiagnosticLogPath(stateDir, projectId);
30288
30711
  let ensured = false;
30289
30712
  return (event) => {
30290
30713
  if (event.event === "stderr") return;
30291
30714
  try {
30292
30715
  if (!ensured) {
30293
- mkdirSync11(dir, { recursive: true });
30716
+ mkdirSync12(dir, { recursive: true });
30294
30717
  ensured = true;
30295
30718
  }
30296
30719
  try {
30297
- if (statSync12(path).size > MAX_BYTES2) renameSync6(path, `${path}.1`);
30720
+ if (statSync13(path).size > MAX_BYTES2) renameSync6(path, `${path}.1`);
30298
30721
  } catch {
30299
30722
  }
30300
- appendFileSync2(path, `${JSON.stringify({ at: now().toISOString(), ...event })}
30723
+ appendFileSync3(path, `${JSON.stringify({ at: now().toISOString(), ...event })}
30301
30724
  `);
30302
30725
  } catch {
30303
30726
  }
@@ -30313,15 +30736,15 @@ var init_codex_app_server_diagnostics = __esm({
30313
30736
 
30314
30737
  // packages/server/src/backend/stale-socket-sweep.ts
30315
30738
  import { execFile as execFile8 } from "node:child_process";
30316
- import { readdirSync as readdirSync7, unlinkSync as unlinkSync5 } from "node:fs";
30739
+ import { readdirSync as readdirSync8, unlinkSync as unlinkSync5 } from "node:fs";
30317
30740
  import { connect as connectSocket } from "node:net";
30318
- import { join as join24 } from "node:path";
30741
+ import { join as join25 } from "node:path";
30319
30742
  function sweepStaleSockets(options, deps = {}) {
30320
30743
  if (process.platform === "win32") return;
30321
30744
  const keep = new Set(options.keep ?? []);
30322
30745
  let candidates;
30323
30746
  try {
30324
- candidates = (deps.readdir ?? readdirSync7)(options.dir).filter((name) => name.startsWith(options.prefix) && name.endsWith(".sock")).map((name) => join24(options.dir, name)).filter((path) => !keep.has(path));
30747
+ candidates = (deps.readdir ?? readdirSync8)(options.dir).filter((name) => name.startsWith(options.prefix) && name.endsWith(".sock")).map((name) => join25(options.dir, name)).filter((path) => !keep.has(path));
30325
30748
  } catch {
30326
30749
  return;
30327
30750
  }
@@ -30634,7 +31057,7 @@ var init_orphan_reaper = __esm({
30634
31057
  });
30635
31058
 
30636
31059
  // packages/server/src/context.ts
30637
- import { join as join25 } from "node:path";
31060
+ import { join as join26 } from "node:path";
30638
31061
  import { randomUUID as randomUUID17 } from "node:crypto";
30639
31062
  function reconcileSessions(storage) {
30640
31063
  for (const row of storage.allSessions()) {
@@ -30787,7 +31210,7 @@ function createContextUnchecked(opts, resources) {
30787
31210
  projectId: project.id,
30788
31211
  projectDir: project.dir
30789
31212
  }, project.tmuxSocketManaged !== false);
30790
- const dbPath = join25(project.stateDir, "ui.db");
31213
+ const dbPath = join26(project.stateDir, "ui.db");
30791
31214
  const storage = createStorage(dbPath);
30792
31215
  resources.storage = storage;
30793
31216
  const bus = new Bus();
@@ -30850,7 +31273,7 @@ function createContextUnchecked(opts, resources) {
30850
31273
  contextUnsubscribers.push(() => clearInterval(quotaRefreshTimer));
30851
31274
  }
30852
31275
  if (!process.env.FRAY_ORPHAN_REAPER_OFF) {
30853
- contextUnsubscribers.push(startOrphanReaper({ log: (m) => console.log(`[fray-ui] ${m}`) }));
31276
+ contextUnsubscribers.push(startOrphanReaper({ log: (m) => log.info("reaper", m) }));
30854
31277
  }
30855
31278
  opts.startup?.afterPhase?.("orphan reaper");
30856
31279
  reconcileSessions(storage);
@@ -30865,6 +31288,9 @@ function createContextUnchecked(opts, resources) {
30865
31288
  // The detached app-server daemon's socket + record live under the project state dir, so a
30866
31289
  // later fray generation can find the app-server this one left running.
30867
31290
  stateDir: project.stateDir,
31291
+ // Codex's MCP servers mount PROCESS-wide on the app-server, not per thread, so the descriptor
31292
+ // is resolved once here — the codex twin of the per-dispatch resolveFrayMcp on the claude side.
31293
+ frayMcp: resolveFrayMcp(project.stateDir),
30868
31294
  dbPath,
30869
31295
  interactions: storage.interactions,
30870
31296
  codexBin: opts.codexBin,
@@ -30934,7 +31360,7 @@ function createContextUnchecked(opts, resources) {
30934
31360
  // per death rather than a card nobody asked for.
30935
31361
  onDiagnostic: (slug, _sessionId, diagnostic) => {
30936
31362
  if (diagnostic.kind !== "lifecycle" || diagnostic.phase !== "crashed") return;
30937
- console.log(`[fray-ui] claude broker ${slug}: ${diagnostic.message ?? "died without a recorded cause"}`);
31363
+ log.warn("broker", `claude broker ${slug}: ${diagnostic.message ?? "died without a recorded cause"}`);
30938
31364
  }
30939
31365
  }) : void 0;
30940
31366
  resources.claudeBroker = claudeBroker;
@@ -31156,6 +31582,7 @@ var init_context = __esm({
31156
31582
  init_adoption_recovery();
31157
31583
  init_orphan_reaper();
31158
31584
  init_shutdown();
31585
+ init_logging();
31159
31586
  CONTEXT_STARTUP_CLEANUP_TIMEOUT_MS = 4e3;
31160
31587
  QUOTA_REFRESH_INTERVAL_MS = 2 * 6e4;
31161
31588
  ContextStartupError = class extends Error {
@@ -33809,7 +34236,7 @@ var init_server = __esm({
33809
34236
  });
33810
34237
 
33811
34238
  // packages/server/src/repair.ts
33812
- import { existsSync as existsSync7, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
34239
+ import { existsSync as existsSync7, readFileSync as readFileSync15, writeFileSync as writeFileSync9 } from "node:fs";
33813
34240
  import { basename as basename6, dirname as dirname9, resolve as resolve7 } from "node:path";
33814
34241
  function quoteValue(v) {
33815
34242
  if (/^[\w./#:+-]+$/.test(v)) return v;
@@ -33840,7 +34267,7 @@ function repairThreadFile(frayDir, file) {
33840
34267
  "---",
33841
34268
  ""
33842
34269
  ].join("\n");
33843
- writeFileSync8(abs, frontmatter + content);
34270
+ writeFileSync9(abs, frontmatter + content);
33844
34271
  return { slug };
33845
34272
  }
33846
34273
  var RepairError, REPAIR_STATUS_TEXT;
@@ -33991,11 +34418,11 @@ var init_bash_background = __esm({
33991
34418
  });
33992
34419
 
33993
34420
  // packages/server/src/transcript.ts
33994
- import { closeSync as closeSync11, existsSync as existsSync8, fstatSync as fstatSync4, mkdirSync as mkdirSync12, openSync as openSync11, readdirSync as readdirSync8, readFileSync as readFileSync17, readSync as readSync6, renameSync as renameSync7, statSync as statSync13, unlinkSync as unlinkSync6, writeFileSync as writeFileSync9 } from "node:fs";
34421
+ import { closeSync as closeSync11, existsSync as existsSync8, fstatSync as fstatSync4, mkdirSync as mkdirSync13, openSync as openSync12, readdirSync as readdirSync9, readFileSync as readFileSync17, readSync as readSync6, renameSync as renameSync7, statSync as statSync14, unlinkSync as unlinkSync6, writeFileSync as writeFileSync10 } from "node:fs";
33995
34422
  import { createHash as createHash13 } from "node:crypto";
33996
34423
  import { StringDecoder as StringDecoder4 } from "node:string_decoder";
33997
- import { join as join26 } from "node:path";
33998
- import { homedir as homedir7, tmpdir as tmpdir4 } from "node:os";
34424
+ import { join as join27 } from "node:path";
34425
+ import { homedir as homedir9, tmpdir as tmpdir4 } from "node:os";
33999
34426
  function isInjectedNoise(text) {
34000
34427
  const t = text.trimStart();
34001
34428
  return NOISE_PREFIXES.some((p) => t.startsWith(p));
@@ -34010,7 +34437,7 @@ function compactionLabel(preTokens, postTokens) {
34010
34437
  return shrank ? `Context compacted \u2014 ${formatTokens(preTokens)} \u2192 ${formatTokens(postTokens)} tokens` : "Context compacted";
34011
34438
  }
34012
34439
  function compactionMessage(sourceId, at, preTokens, postTokens) {
34013
- return { sourceId, role: "assistant", kind: "event", boundary: true, text: compactionLabel(preTokens, postTokens), tools: [], parts: [], at };
34440
+ return { sourceId, role: "assistant", kind: "event", boundary: "compaction", text: compactionLabel(preTokens, postTokens), tools: [], parts: [], at };
34014
34441
  }
34015
34442
  function normalizeNewlines(s) {
34016
34443
  return stripDeliveryMarkers(s).replace(/\r\n?/g, "\n");
@@ -34053,7 +34480,9 @@ function userProjection(text, first) {
34053
34480
  const peer = parseAgentMessage(text);
34054
34481
  if (peer) return { displayText: peer.body, peerFrom: peer.from };
34055
34482
  const displayText2 = userDisplayText(text, first);
34056
- return { ...displayText2 ? { displayText: displayText2 } : {}, ...isWakeDelivery(text) ? { wake: true } : {} };
34483
+ if (!isWakeDelivery(text)) return { ...displayText2 ? { displayText: displayText2 } : {} };
34484
+ const steer = parseGithubWakeSteer(displayText2 ?? text);
34485
+ return { ...displayText2 ? { displayText: displayText2 } : {}, wake: true, ...steer ? { wakeSteer: steer } : {} };
34057
34486
  }
34058
34487
  function pushTextPart(m, text) {
34059
34488
  const last = m.parts[m.parts.length - 1];
@@ -34116,6 +34545,7 @@ function createTranscriptFold(identityPrefix = "claude") {
34116
34545
  const backgroundShells = /* @__PURE__ */ new Map();
34117
34546
  const backgroundTaskIds = /* @__PURE__ */ new Map();
34118
34547
  const childDispatchIds = /* @__PURE__ */ new Map();
34548
+ const dispatchLabels = /* @__PURE__ */ new Map();
34119
34549
  let prevTs;
34120
34550
  let thinkingMsgId = null;
34121
34551
  const queuedPending = [];
@@ -34229,14 +34659,13 @@ function createTranscriptFold(identityPrefix = "claude") {
34229
34659
  if (entry) {
34230
34660
  resolveQueued(prompt);
34231
34661
  if (dispatchId && entry.message.peerFrom) entry.message.peerDispatchId = dispatchId;
34232
- const dispatched = dispatchId ? agentDispatches.get(dispatchId) : void 0;
34233
- const described = dispatched?.call.detail?.trim();
34662
+ const described = dispatchId ? dispatchLabels.get(dispatchId) : void 0;
34234
34663
  if (described && entry.message.peerFrom) entry.message.peerFrom = described;
34235
34664
  if (thisTs) prevTs = thisTs;
34236
34665
  lastAssistantId = null;
34237
34666
  } else {
34238
34667
  const parsed = prompt.trim() ? parseAgentMessage(prompt) : void 0;
34239
- const describedHere = (dispatchId ? agentDispatches.get(dispatchId) : void 0)?.call.detail?.trim();
34668
+ const describedHere = dispatchId ? dispatchLabels.get(dispatchId) : void 0;
34240
34669
  const from = describedHere || str(peerOrigin.from) || str(peerOrigin.name) || parsed?.from || "";
34241
34670
  const body = parsed?.body ?? (typeof peerOrigin.body === "string" ? peerOrigin.body : "");
34242
34671
  if (from && body.trim()) {
@@ -34333,9 +34762,10 @@ ${block.text}` : block.text;
34333
34762
  pushToolPart(m, call);
34334
34763
  m.tools.push(call);
34335
34764
  if (call.agentId) agentDispatches.set(call.agentId, { at: rec.timestamp, call });
34765
+ if (call.agentId && call.detail?.trim()) dispatchLabels.set(call.agentId, call.detail.trim());
34336
34766
  if (call.sendTo && call.sendTo !== "main") {
34337
34767
  const dispatchId = childDispatchIds.get(call.sendTo);
34338
- const described = dispatchId ? agentDispatches.get(dispatchId)?.call.detail?.trim() : void 0;
34768
+ const described = dispatchId ? dispatchLabels.get(dispatchId) : void 0;
34339
34769
  if (dispatchId) call.sendDispatchId = dispatchId;
34340
34770
  if (described) call.sendTargetLabel = described;
34341
34771
  }
@@ -34485,18 +34915,18 @@ function looksLikeImage(buf, ext) {
34485
34915
  }
34486
34916
  function pruneScreenshotCache() {
34487
34917
  try {
34488
- const entries = readdirSync8(SCREENSHOT_CACHE_DIR).filter((n) => !n.startsWith("."));
34918
+ const entries = readdirSync9(SCREENSHOT_CACHE_DIR).filter((n) => !n.startsWith("."));
34489
34919
  if (entries.length <= SCREENSHOT_CACHE_MAX) return;
34490
34920
  const byMtime = entries.map((n) => {
34491
34921
  try {
34492
- return { n, m: statSync13(join26(SCREENSHOT_CACHE_DIR, n)).mtimeMs };
34922
+ return { n, m: statSync14(join27(SCREENSHOT_CACHE_DIR, n)).mtimeMs };
34493
34923
  } catch {
34494
34924
  return { n, m: 0 };
34495
34925
  }
34496
34926
  }).sort((a, b) => b.m - a.m);
34497
34927
  for (const { n } of byMtime.slice(SCREENSHOT_CACHE_MAX)) {
34498
34928
  try {
34499
- unlinkSync6(join26(SCREENSHOT_CACHE_DIR, n));
34929
+ unlinkSync6(join27(SCREENSHOT_CACHE_DIR, n));
34500
34930
  } catch {
34501
34931
  }
34502
34932
  }
@@ -34526,15 +34956,15 @@ function persistBase64Image(mediaType, data, idKey) {
34526
34956
  const ext = IMAGE_MEDIA_EXT[typeof mediaType === "string" ? mediaType.toLowerCase() : ""];
34527
34957
  if (!ext || !data) return void 0;
34528
34958
  const name = createHash13("sha256").update(idKey).digest("hex").slice(0, 32);
34529
- const path = join26(SCREENSHOT_CACHE_DIR, `${name}.${ext}`);
34959
+ const path = join27(SCREENSHOT_CACHE_DIR, `${name}.${ext}`);
34530
34960
  try {
34531
34961
  if (existsSync8(path)) return path;
34532
34962
  if (data.length > SCREENSHOT_MAX_BASE64) return void 0;
34533
34963
  const buf = Buffer.from(data, "base64");
34534
34964
  if (buf.length === 0 || !looksLikeImage(buf, ext)) return void 0;
34535
- mkdirSync12(SCREENSHOT_CACHE_DIR, { recursive: true });
34536
- const tmp = join26(SCREENSHOT_CACHE_DIR, `.${name}.${process.pid}.${screenshotTmpSeq++}.tmp`);
34537
- writeFileSync9(tmp, buf);
34965
+ mkdirSync13(SCREENSHOT_CACHE_DIR, { recursive: true });
34966
+ const tmp = join27(SCREENSHOT_CACHE_DIR, `.${name}.${process.pid}.${screenshotTmpSeq++}.tmp`);
34967
+ writeFileSync10(tmp, buf);
34538
34968
  renameSync7(tmp, path);
34539
34969
  pruneScreenshotCache();
34540
34970
  return path;
@@ -34548,15 +34978,15 @@ function persistSentFile(srcPath, idKey) {
34548
34978
  const outExt = ext === "jpeg" ? "jpg" : ext;
34549
34979
  try {
34550
34980
  const name = createHash13("sha256").update(idKey).digest("hex").slice(0, 32);
34551
- const dest = join26(SCREENSHOT_CACHE_DIR, `${name}.${outExt}`);
34981
+ const dest = join27(SCREENSHOT_CACHE_DIR, `${name}.${outExt}`);
34552
34982
  if (existsSync8(dest)) return dest;
34553
- const size = statSync13(srcPath).size;
34983
+ const size = statSync14(srcPath).size;
34554
34984
  if (size === 0 || size > SENT_IMAGE_MAX_BYTES) return void 0;
34555
34985
  const buf = readFileSync17(srcPath);
34556
34986
  if (!looksLikeImage(buf, outExt)) return void 0;
34557
- mkdirSync12(SCREENSHOT_CACHE_DIR, { recursive: true });
34558
- const tmp = join26(SCREENSHOT_CACHE_DIR, `.${name}.${process.pid}.${screenshotTmpSeq++}.tmp`);
34559
- writeFileSync9(tmp, buf);
34987
+ mkdirSync13(SCREENSHOT_CACHE_DIR, { recursive: true });
34988
+ const tmp = join27(SCREENSHOT_CACHE_DIR, `.${name}.${process.pid}.${screenshotTmpSeq++}.tmp`);
34989
+ writeFileSync10(tmp, buf);
34560
34990
  renameSync7(tmp, dest);
34561
34991
  pruneScreenshotCache();
34562
34992
  return dest;
@@ -34856,7 +35286,7 @@ function completionEvents(rec, dispatches, backgroundShells, backgroundTaskIds)
34856
35286
  const elapsedMs2 = elapsedBetween(shell.at, rec.timestamp);
34857
35287
  shell.call.status = status === "completed" ? "completed" : status === "killed" ? "cancelled" : "failed";
34858
35288
  if (elapsedMs2 !== void 0) shell.call.durationMs = elapsedMs2;
34859
- out.push({ role: "assistant", kind: "event", boundary: true, text: backgroundWakeLabel(shell.call, status, block), tools: [], parts: [], at });
35289
+ out.push({ role: "assistant", kind: "event", boundary: "wake", text: backgroundWakeLabel(shell.call, status, block), tools: [], parts: [], at });
34860
35290
  continue;
34861
35291
  }
34862
35292
  dispatches.delete(id);
@@ -34904,11 +35334,11 @@ function retainedFoldEntry(path, identityPrefix, fileId, size) {
34904
35334
  return { entry, hit };
34905
35335
  }
34906
35336
  function readTranscript(project, sessionId) {
34907
- const path = join26(homedir7(), ".claude", "projects", project.cwdSlug, `${sessionId}.jsonl`);
35337
+ const path = join27(homedir9(), ".claude", "projects", project.cwdSlug, `${sessionId}.jsonl`);
34908
35338
  const identityPrefix = `claude:${sessionId}`;
34909
35339
  let fd;
34910
35340
  try {
34911
- fd = openSync11(path, "r");
35341
+ fd = openSync12(path, "r");
34912
35342
  const st = fstatSync4(fd);
34913
35343
  const size = st.size;
34914
35344
  const fileId = `${st.dev}:${st.ino}:${Math.trunc(st.birthtimeMs)}`;
@@ -34952,7 +35382,7 @@ function verifyIncrementalParse(path, identityPrefix, incremental) {
34952
35382
  }
34953
35383
  }
34954
35384
  function logDirOf(project) {
34955
- return join26(homedir7(), ".claude", "projects", project.cwdSlug);
35385
+ return join27(homedir9(), ".claude", "projects", project.cwdSlug);
34956
35386
  }
34957
35387
  function projectCodexTranscript(raw2, identityPrefix = "codex") {
34958
35388
  const out = [];
@@ -35858,7 +36288,7 @@ function sourceForThread(project, storage, slug, backendFor) {
35858
36288
  if (row) {
35859
36289
  const backend = row.backend === "codex" ? "codex" : "claude";
35860
36290
  const nativeId = backend === "codex" ? row.agent_session_id ?? row.session_id : row.transcript_id ?? row.session_id;
35861
- const path = backend === "codex" ? (backendFor?.("codex") ?? defaultCodexBackend()).transcriptPath(nativeId) : join26(logDirOf(project), `${nativeId}.jsonl`);
36291
+ const path = backend === "codex" ? (backendFor?.("codex") ?? defaultCodexBackend()).transcriptPath(nativeId) : join27(logDirOf(project), `${nativeId}.jsonl`);
35862
36292
  if (!path) return void 0;
35863
36293
  return {
35864
36294
  slug,
@@ -35876,7 +36306,7 @@ function sourceForThread(project, storage, slug, backendFor) {
35876
36306
  nativeId: slug,
35877
36307
  backend: "claude",
35878
36308
  runtimeGeneration: 0,
35879
- path: join26(logDirOf(project), `${slug}.jsonl`)
36309
+ path: join27(logDirOf(project), `${slug}.jsonl`)
35880
36310
  };
35881
36311
  }
35882
36312
  function discoveredClaudeSource(project, storage, slug, expectedNativeId) {
@@ -35897,13 +36327,13 @@ function discoveredClaudeSource(project, storage, slug, expectedNativeId) {
35897
36327
  nativeId,
35898
36328
  backend: "claude",
35899
36329
  runtimeGeneration: row.runtime_generation ?? 0,
35900
- path: join26(logDirOf(project), `${nativeId}.jsonl`)
36330
+ path: join27(logDirOf(project), `${nativeId}.jsonl`)
35901
36331
  };
35902
36332
  }
35903
36333
  function fixedSnapshot(source) {
35904
36334
  let fd;
35905
36335
  try {
35906
- fd = openSync11(source.path, "r");
36336
+ fd = openSync12(source.path, "r");
35907
36337
  const before = fstatSync4(fd);
35908
36338
  if (!Number.isSafeInteger(before.size) || before.size < 0) throw new Error("transcript is too large to page safely");
35909
36339
  const bytes = Buffer.allocUnsafe(before.size);
@@ -36202,7 +36632,7 @@ var init_transcript = __esm({
36202
36632
  "image/gif": "gif",
36203
36633
  "image/webp": "webp"
36204
36634
  };
36205
- SCREENSHOT_CACHE_DIR = join26(tmpdir4(), "fray-tool-images");
36635
+ SCREENSHOT_CACHE_DIR = join27(tmpdir4(), "fray-tool-images");
36206
36636
  SCREENSHOT_CACHE_MAX = 200;
36207
36637
  SCREENSHOT_MAX_BASE64 = 32e6;
36208
36638
  screenshotTmpSeq = 0;
@@ -36252,9 +36682,9 @@ var init_open_external = __esm({
36252
36682
 
36253
36683
  // packages/server/src/local-file.ts
36254
36684
  import { spawn as spawn7 } from "node:child_process";
36255
- import { realpathSync as realpathSync7, statSync as statSync14 } from "node:fs";
36256
- import { homedir as homedir8 } from "node:os";
36257
- import { isAbsolute as isAbsolute5, join as join27, resolve as resolve8, sep as sep2 } from "node:path";
36685
+ import { realpathSync as realpathSync7, statSync as statSync15 } from "node:fs";
36686
+ import { homedir as homedir10 } from "node:os";
36687
+ import { isAbsolute as isAbsolute5, join as join28, resolve as resolve8, sep as sep2 } from "node:path";
36258
36688
  function isUnder(real, root) {
36259
36689
  let rootReal;
36260
36690
  try {
@@ -36274,17 +36704,17 @@ function resolveLocalFile(rawPath, roots) {
36274
36704
  }
36275
36705
  if (!roots.some((root) => isUnder(real, root))) throw new Error("Local file is outside Fray's trusted roots");
36276
36706
  try {
36277
- if (!statSync14(real).isFile()) throw new Error("Local path is not a regular file");
36707
+ if (!statSync15(real).isFile()) throw new Error("Local path is not a regular file");
36278
36708
  } catch (error) {
36279
36709
  if (error instanceof Error && error.message === "Local path is not a regular file") throw error;
36280
36710
  throw new Error("Local file was not found");
36281
36711
  }
36282
36712
  return real;
36283
36713
  }
36284
- function resolveOpenableFile(raw2, projectDir, roots, home = homedir8()) {
36714
+ function resolveOpenableFile(raw2, projectDir, roots, home = homedir10()) {
36285
36715
  const trimmed = raw2.trim().replace(/:\d+(?::\d+)?$/, "");
36286
36716
  if (!trimmed) return null;
36287
- const abs = trimmed === "~" ? home : trimmed.startsWith("~/") ? join27(home, trimmed.slice(2)) : isAbsolute5(trimmed) ? trimmed : resolve8(projectDir, trimmed);
36717
+ const abs = trimmed === "~" ? home : trimmed.startsWith("~/") ? join28(home, trimmed.slice(2)) : isAbsolute5(trimmed) ? trimmed : resolve8(projectDir, trimmed);
36288
36718
  try {
36289
36719
  return resolveLocalFile(abs, roots);
36290
36720
  } catch {
@@ -36628,11 +37058,11 @@ var init_external_terminal = __esm({
36628
37058
  });
36629
37059
 
36630
37060
  // packages/server/src/background-shell-output.ts
36631
- import { closeSync as closeSync12, fstatSync as fstatSync5, openSync as openSync12, readSync as readSync7 } from "node:fs";
37061
+ import { closeSync as closeSync12, fstatSync as fstatSync5, openSync as openSync13, readSync as readSync7 } from "node:fs";
36632
37062
  function readBackgroundShellOutput(path, maxBytes = OUTPUT_TAIL_BYTES) {
36633
37063
  let fd;
36634
37064
  try {
36635
- fd = openSync12(path, "r");
37065
+ fd = openSync13(path, "r");
36636
37066
  const size = fstatSync5(fd).size;
36637
37067
  const length = Math.min(size, maxBytes);
36638
37068
  const offset = Math.max(0, size - length);
@@ -36662,7 +37092,7 @@ var init_background_shell_output = __esm({
36662
37092
 
36663
37093
  // packages/server/src/router.ts
36664
37094
  import { readFileSync as readFileSync18 } from "node:fs";
36665
- import { join as join28 } from "node:path";
37095
+ import { join as join29 } from "node:path";
36666
37096
  function validateGithubDispatchProfile(input) {
36667
37097
  validateThreadProfile(input.backend, input.model, input.effort);
36668
37098
  }
@@ -36803,7 +37233,7 @@ async function stopAndForgetRegisteredRuntime(storage, row, runtime = tmux_expor
36803
37233
  return forgotten;
36804
37234
  }
36805
37235
  function createRouter(ctx) {
36806
- const frayDir = join28(ctx.project.dir, ".fray");
37236
+ const frayDir = join29(ctx.project.dir, ".fray");
36807
37237
  const openRoots = openableFileRoots(ctx.project);
36808
37238
  const claudeRename = createClaudeRenameController({ storage: ctx.storage, tailer: ctx.tailer, board: ctx.board });
36809
37239
  function isAutoTitledSession(slug) {
@@ -37141,6 +37571,14 @@ function createRouter(ctx) {
37141
37571
  if (hasPendingPermissionChange(row)) {
37142
37572
  throw new Error("Wait for the current permission change to finish before sending a follow-up");
37143
37573
  }
37574
+ if (input.freshProcess) {
37575
+ if (!(row?.backend === "claude" && row.claude_runtime === "broker")) {
37576
+ throw new Error("Only a broker-backed Claude worker can be restarted in place");
37577
+ }
37578
+ if ((ctx.tailer.get(input.slug)?.subAgents ?? []).some((agent) => agent.state === "running")) {
37579
+ throw new Error("This worker has sub-agents still running; restarting it would kill them. Wait for them to finish, then restart.");
37580
+ }
37581
+ }
37144
37582
  if (row?.backend === "codex") {
37145
37583
  const bridge = ctx.codexAppServer;
37146
37584
  if (!bridge) throw new Error("Codex app-server is unavailable; cannot deliver this follow-up");
@@ -37202,7 +37640,12 @@ function createRouter(ctx) {
37202
37640
  // needs the same treatment: while the process is still latched on its own 429, delivering
37203
37641
  // into it does nothing at all. Restart it instead — otherwise the button is a no-op and
37204
37642
  // reads as fray having ignored the click.
37205
- freshProcess: needsFreshProcessForLimit(
37643
+ //
37644
+ // `input.freshProcess` is the operator asking for it OUTRIGHT (the "Restart worker" verb),
37645
+ // which the server cannot derive: only the human knows they want the worker back on a newer
37646
+ // build. It is OR'd in rather than replacing the derivation, so a restart clicked on a
37647
+ // limit-latched thread still behaves. The live-sub-agent refusal above applies to both.
37648
+ freshProcess: input.freshProcess === true || needsFreshProcessForLimit(
37206
37649
  ctx.tailer.get(input.slug)?.limitFault,
37207
37650
  Date.now(),
37208
37651
  (ctx.tailer.get(input.slug)?.subAgents ?? []).some((agent) => agent.state === "running")
@@ -37549,7 +37992,7 @@ function createRouter(ctx) {
37549
37992
  const row = ctx.storage.getSession(input.slug);
37550
37993
  if (!row) return { markdown: "" };
37551
37994
  try {
37552
- return { markdown: readFileSync18(join28(ctx.project.dir, scratchpadRelPath(row.session_id)), "utf8") };
37995
+ return { markdown: readFileSync18(join29(ctx.project.dir, scratchpadRelPath(row.session_id)), "utf8") };
37553
37996
  } catch {
37554
37997
  return { markdown: "" };
37555
37998
  }
@@ -38036,7 +38479,7 @@ var init_local_origin = __esm({
38036
38479
  });
38037
38480
 
38038
38481
  // packages/server/src/local-image.ts
38039
- import { readFileSync as readFileSync19, realpathSync as realpathSync8, statSync as statSync15 } from "node:fs";
38482
+ import { readFileSync as readFileSync19, realpathSync as realpathSync8, statSync as statSync16 } from "node:fs";
38040
38483
  import { extname, isAbsolute as isAbsolute6 } from "node:path";
38041
38484
  function resolveLocalImage(rawPath) {
38042
38485
  if (!rawPath || !isAbsolute6(rawPath)) return { status: 400 };
@@ -38049,7 +38492,7 @@ function resolveLocalImage(rawPath) {
38049
38492
  return { status: 404 };
38050
38493
  }
38051
38494
  try {
38052
- if (!statSync15(real).isFile()) return { status: 404 };
38495
+ if (!statSync16(real).isFile()) return { status: 404 };
38053
38496
  return { status: 200, contentType, body: readFileSync19(real) };
38054
38497
  } catch {
38055
38498
  return { status: 404 };
@@ -38070,11 +38513,11 @@ var init_local_image = __esm({
38070
38513
  });
38071
38514
 
38072
38515
  // packages/server/src/local-visualization.ts
38073
- import { readdirSync as readdirSync9, readFileSync as readFileSync20, realpathSync as realpathSync9, statSync as statSync16 } from "node:fs";
38074
- import { join as join29, sep as sep3 } from "node:path";
38516
+ import { readdirSync as readdirSync10, readFileSync as readFileSync20, realpathSync as realpathSync9, statSync as statSync17 } from "node:fs";
38517
+ import { join as join30, sep as sep3 } from "node:path";
38075
38518
  function children(path, pattern) {
38076
38519
  try {
38077
- return readdirSync9(path, { withFileTypes: true }).filter((entry) => entry.isDirectory() && pattern.test(entry.name)).map((entry) => entry.name).sort().reverse();
38520
+ return readdirSync10(path, { withFileTypes: true }).filter((entry) => entry.isDirectory() && pattern.test(entry.name)).map((entry) => entry.name).sort().reverse();
38078
38521
  } catch {
38079
38522
  return [];
38080
38523
  }
@@ -38083,7 +38526,7 @@ function isUnder2(path, root) {
38083
38526
  return path === root || path.startsWith(root.endsWith(sep3) ? root : root + sep3);
38084
38527
  }
38085
38528
  function resolveFragment(projectDir, sessionId, file) {
38086
- const base = join29(projectDir, ".codex", "visualizations");
38529
+ const base = join30(projectDir, ".codex", "visualizations");
38087
38530
  let projectReal;
38088
38531
  let baseReal;
38089
38532
  try {
@@ -38094,15 +38537,15 @@ function resolveFragment(projectDir, sessionId, file) {
38094
38537
  }
38095
38538
  if (!isUnder2(baseReal, projectReal)) return null;
38096
38539
  for (const year of children(base, YEAR_PART)) {
38097
- const yearDir = join29(base, year);
38540
+ const yearDir = join30(base, year);
38098
38541
  for (const month of children(yearDir, DATE_PART)) {
38099
- const monthDir = join29(yearDir, month);
38542
+ const monthDir = join30(yearDir, month);
38100
38543
  for (const day of children(monthDir, DATE_PART)) {
38101
- const sessionRoot = join29(baseReal, year, month, day, sessionId);
38102
- const candidate = join29(monthDir, day, sessionId, file);
38544
+ const sessionRoot = join30(baseReal, year, month, day, sessionId);
38545
+ const candidate = join30(monthDir, day, sessionId, file);
38103
38546
  try {
38104
38547
  const real = realpathSync9(candidate);
38105
- if (isUnder2(real, sessionRoot) && statSync16(real).isFile()) return real;
38548
+ if (isUnder2(real, sessionRoot) && statSync17(real).isFile()) return real;
38106
38549
  } catch {
38107
38550
  }
38108
38551
  }
@@ -38183,7 +38626,7 @@ function resolveLocalVisualization(projectDir, sessionId, file) {
38183
38626
  if (!path) return { status: 404 };
38184
38627
  let fragment;
38185
38628
  try {
38186
- const size = statSync16(path).size;
38629
+ const size = statSync17(path).size;
38187
38630
  if (size > MAX_FRAGMENT_BYTES) return { status: 413 };
38188
38631
  fragment = readFileSync20(path, "utf8");
38189
38632
  } catch {
@@ -38227,8 +38670,8 @@ var init_local_visualization = __esm({
38227
38670
  });
38228
38671
 
38229
38672
  // packages/server/src/app.ts
38230
- import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync10 } from "node:fs";
38231
- import { join as join30 } from "node:path";
38673
+ import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync11 } from "node:fs";
38674
+ import { join as join31 } from "node:path";
38232
38675
  import { randomUUID as randomUUID18 } from "node:crypto";
38233
38676
  function createApp(ctx, options = {}) {
38234
38677
  const app = new Hono2();
@@ -38305,11 +38748,11 @@ function createApp(ctx, options = {}) {
38305
38748
  if (typeof body.data !== "string" || body.data.length > ATTACHMENT_MAX_BASE64_CHARS) return c.json({ error: "bad payload" }, 400);
38306
38749
  const ext = `.${attachmentExtension(name)}`;
38307
38750
  const buf = Buffer.from(body.data, "base64");
38308
- const dir = join30(ctx.project.stateDir, "attachments");
38309
- mkdirSync13(dir, { recursive: true });
38751
+ const dir = join31(ctx.project.stateDir, "attachments");
38752
+ mkdirSync14(dir, { recursive: true });
38310
38753
  const base = name.replace(/\.[^.]+$/, "").replace(/[^a-zA-Z0-9_-]+/g, "-").slice(0, 40) || "file";
38311
- const path = join30(dir, `${Date.now()}-${randomUUID18().slice(0, 8)}-${base}${ext}`);
38312
- writeFileSync10(path, buf);
38754
+ const path = join31(dir, `${Date.now()}-${randomUUID18().slice(0, 8)}-${base}${ext}`);
38755
+ writeFileSync11(path, buf);
38313
38756
  return c.json({ path });
38314
38757
  });
38315
38758
  app.get(
@@ -39581,10 +40024,10 @@ var init_app_socket = __esm({
39581
40024
  });
39582
40025
 
39583
40026
  // packages/server/src/boot-progress.ts
39584
- import { mkdirSync as mkdirSync14, readFileSync as readFileSync21, renameSync as renameSync8, rmSync as rmSync6, writeFileSync as writeFileSync11 } from "node:fs";
39585
- import { basename as basename8, dirname as dirname10, join as join31 } from "node:path";
40027
+ import { mkdirSync as mkdirSync15, readFileSync as readFileSync21, renameSync as renameSync8, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "node:fs";
40028
+ import { basename as basename8, dirname as dirname10, join as join32 } from "node:path";
39586
40029
  function bootProgressPath(stateDir) {
39587
- return join31(stateDir, BOOT_PROGRESS_NAME);
40030
+ return join32(stateDir, BOOT_PROGRESS_NAME);
39588
40031
  }
39589
40032
  function createBootProgressPublisher(stateDir, minIntervalMs = 200) {
39590
40033
  if (!stateDir) {
@@ -39604,9 +40047,9 @@ function createBootProgressPublisher(stateDir, minIntervalMs = 200) {
39604
40047
  lastAt = now;
39605
40048
  step++;
39606
40049
  try {
39607
- mkdirSync14(dirname10(path), { recursive: true, mode: 448 });
39608
- const temp = join31(dirname10(path), `.${basename8(path)}.${process.pid}.tmp`);
39609
- writeFileSync11(temp, `${JSON.stringify({ pid: process.pid, step, phase, at: new Date(now).toISOString() })}
40050
+ mkdirSync15(dirname10(path), { recursive: true, mode: 448 });
40051
+ const temp = join32(dirname10(path), `.${basename8(path)}.${process.pid}.tmp`);
40052
+ writeFileSync12(temp, `${JSON.stringify({ pid: process.pid, step, phase, at: new Date(now).toISOString() })}
39610
40053
  `, "utf8");
39611
40054
  renameSync8(temp, path);
39612
40055
  } catch {
@@ -39615,7 +40058,7 @@ function createBootProgressPublisher(stateDir, minIntervalMs = 200) {
39615
40058
  return Object.assign(write, {
39616
40059
  done: () => {
39617
40060
  try {
39618
- rmSync6(path, { force: true });
40061
+ rmSync7(path, { force: true });
39619
40062
  } catch {
39620
40063
  }
39621
40064
  }
@@ -39641,7 +40084,7 @@ __export(index_exports, {
39641
40084
  });
39642
40085
  import { createServer } from "node:http";
39643
40086
  import { readFileSync as readFileSync22, existsSync as existsSync9 } from "node:fs";
39644
- import { join as join32, resolve as resolve9, extname as extname2, normalize } from "node:path";
40087
+ import { join as join33, resolve as resolve9, extname as extname2, normalize } from "node:path";
39645
40088
  async function pipeToApp(app, req, res, port2, controller) {
39646
40089
  const url = `http://127.0.0.1:${port2}${req.url ?? "/"}`;
39647
40090
  res.on("close", () => {
@@ -39710,9 +40153,9 @@ function createShutdownSignalHandler(options) {
39710
40153
  }
39711
40154
  function serveStatic(distDir, req, res) {
39712
40155
  const rel = normalize((req.url ?? "/").split("?")[0]).replace(/^(\.\.[/\\])+/, "");
39713
- let file = join32(distDir, rel === "/" ? "index.html" : rel);
39714
- if (!file.startsWith(distDir)) file = join32(distDir, "index.html");
39715
- if (!existsSync9(file)) file = join32(distDir, "index.html");
40156
+ let file = join33(distDir, rel === "/" ? "index.html" : rel);
40157
+ if (!file.startsWith(distDir)) file = join33(distDir, "index.html");
40158
+ if (!existsSync9(file)) file = join33(distDir, "index.html");
39716
40159
  try {
39717
40160
  const body = readFileSync22(file);
39718
40161
  res.writeHead(200, { "content-type": MIME[extname2(file)] ?? "application/octet-stream" });
@@ -39762,8 +40205,9 @@ async function startServer(opts = {}) {
39762
40205
  diagnostics.push(event);
39763
40206
  if (opts.shutdownDiagnostic) opts.shutdownDiagnostic(event);
39764
40207
  else {
39765
- console.error(
39766
- `[fray-ui] shutdown ${event.phase}: ${event.message}${event.error instanceof Error ? ` \u2014 ${event.error.message}` : ""}`
40208
+ log.warn(
40209
+ "shutdown",
40210
+ `${event.phase}: ${event.message}${event.error instanceof Error ? ` \u2014 ${event.error.message}` : ""}`
39767
40211
  );
39768
40212
  }
39769
40213
  };
@@ -40010,9 +40454,9 @@ async function startServer(opts = {}) {
40010
40454
  } else {
40011
40455
  await phase("wake scheduler", () => void 0);
40012
40456
  }
40013
- statusPath = join32(ctx.project.stateDir, "server.lock");
40457
+ statusPath = join33(ctx.project.stateDir, "server.lock");
40014
40458
  const webRoot = resolve9(import.meta.dirname, "..", "..", "web");
40015
- const distDir = opts.webDistDir ? resolve9(opts.webDistDir) : join32(webRoot, "dist");
40459
+ const distDir = opts.webDistDir ? resolve9(opts.webDistDir) : join33(webRoot, "dist");
40016
40460
  startupPhase = "Vite";
40017
40461
  if (opts.dev) {
40018
40462
  try {
@@ -40024,8 +40468,9 @@ async function startServer(opts = {}) {
40024
40468
  });
40025
40469
  } catch (error) {
40026
40470
  if (opts.requireDevWeb) throw error;
40027
- console.warn(
40028
- `[fray-ui] vite dev middleware unavailable \u2014 serving API only: ${error instanceof Error ? error.message : error}`
40471
+ log.warn(
40472
+ "server",
40473
+ `vite dev middleware unavailable \u2014 serving API only: ${error instanceof Error ? error.message : error}`
40029
40474
  );
40030
40475
  }
40031
40476
  }
@@ -40055,7 +40500,7 @@ async function startServer(opts = {}) {
40055
40500
  if (vite) {
40056
40501
  vite.middlewares(req, res, () => {
40057
40502
  try {
40058
- const html = readFileSync22(join32(webRoot, "index.html"), "utf8");
40503
+ const html = readFileSync22(join33(webRoot, "index.html"), "utf8");
40059
40504
  void vite.transformIndexHtml(url, html).then((out) => {
40060
40505
  res.writeHead(200, { "content-type": "text/html" });
40061
40506
  res.end(out);
@@ -40110,8 +40555,9 @@ async function startServer(opts = {}) {
40110
40555
  port: port2,
40111
40556
  bootId: ctx.bootId
40112
40557
  }));
40113
- console.log(
40114
- `[fray-ui] server on http://127.0.0.1:${port2} (${opts.dev ? "dev" : "prod"}) \u2014 project ${ctx.project.name}`
40558
+ log.info(
40559
+ "server",
40560
+ `control plane listening on 127.0.0.1:${port2} (${opts.dev ? "dev" : "prod"}) \u2014 project ${ctx.project.name}`
40115
40561
  );
40116
40562
  startupPhase = "signal handlers";
40117
40563
  if (!opts.requestOwnerStop) {
@@ -40179,6 +40625,7 @@ var init_index = __esm({
40179
40625
  init_project_launch();
40180
40626
  init_tmux();
40181
40627
  init_boot_progress();
40628
+ init_logging();
40182
40629
  SERVER_SHUTDOWN_TIMEOUT_MS = 4e3;
40183
40630
  SERVER_FORCE_EXIT_MS = 5e3;
40184
40631
  defaultStartServerRuntime = {
@@ -40233,12 +40680,20 @@ var init_index = __esm({
40233
40680
  init_project();
40234
40681
  init_project_launch();
40235
40682
  init_shutdown();
40683
+ init_logging();
40236
40684
  import { existsSync as existsSync10 } from "node:fs";
40237
- import { join as join33 } from "node:path";
40685
+ import { join as join34 } from "node:path";
40686
+ process.on("uncaughtException", (error) => {
40687
+ log.error("dev-child", `uncaught exception: ${error instanceof Error ? error.stack ?? error.message : error}`);
40688
+ process.exit(1);
40689
+ });
40690
+ process.on("unhandledRejection", (reason) => {
40691
+ log.error("dev-child", `unhandled rejection: ${reason instanceof Error ? reason.stack ?? reason.message : reason}`);
40692
+ });
40238
40693
  var rawPort = process.env.FRAY_DEV_PORT;
40239
40694
  var port = Number(rawPort);
40240
40695
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
40241
- console.error(`[fray-ui] invalid FRAY_DEV_PORT for dev child: ${rawPort ?? "<unset>"}`);
40696
+ log.error("dev-child", `invalid FRAY_DEV_PORT: ${rawPort ?? "<unset>"}`);
40242
40697
  process.exit(1);
40243
40698
  }
40244
40699
  function notifySupervisor(message) {
@@ -40267,7 +40722,7 @@ try {
40267
40722
  ];
40268
40723
  if (!existsSync10(stableWebDist)) throw new Error("stable artifact launch is missing its verified web directory");
40269
40724
  for (const [name, directory, requiredFile] of required) {
40270
- if (!directory || !existsSync10(join33(directory, requiredFile)))
40725
+ if (!directory || !existsSync10(join34(directory, requiredFile)))
40271
40726
  throw new Error(`stable artifact launch is missing verified ${name}`);
40272
40727
  }
40273
40728
  }
@@ -40298,16 +40753,16 @@ try {
40298
40753
  process.exit(0);
40299
40754
  } catch (err) {
40300
40755
  if (err instanceof ShutdownTimeoutError) {
40301
- console.warn(`[fray-ui] dev child shutdown exceeded ${err.timeoutMs}ms; retaining ownership while the drain completes`);
40756
+ log.warn("dev-child", `shutdown exceeded ${err.timeoutMs}ms; retaining ownership while the drain completes`);
40302
40757
  try {
40303
40758
  await server.shutdownFence.whenSafe();
40304
40759
  process.exit(0);
40305
40760
  return;
40306
40761
  } catch (drainError) {
40307
- console.error(`[fray-ui] dev child late shutdown drain failed: ${drainError instanceof Error ? drainError.stack ?? drainError.message : drainError}`);
40762
+ log.error("dev-child", `late shutdown drain failed: ${drainError instanceof Error ? drainError.stack ?? drainError.message : drainError}`);
40308
40763
  }
40309
40764
  }
40310
- console.error(`[fray-ui] dev child shutdown failed: ${err instanceof Error ? err.stack ?? err.message : err}`);
40765
+ log.error("dev-child", `shutdown failed: ${err instanceof Error ? err.stack ?? err.message : err}`);
40311
40766
  process.exit(1);
40312
40767
  }
40313
40768
  };
@@ -40315,6 +40770,6 @@ try {
40315
40770
  process.on("SIGTERM", () => void shutdown());
40316
40771
  process.once("disconnect", () => void shutdown());
40317
40772
  } catch (err) {
40318
- console.error(`[fray-ui] dev child failed to start: ${err instanceof Error ? err.stack ?? err.message : err}`);
40773
+ log.error("dev-child", `failed to start: ${err instanceof Error ? err.stack ?? err.message : err}`);
40319
40774
  process.exit(1);
40320
40775
  }