frayui 0.1.2 → 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 (101) 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 +766 -329
  4. package/dist/frayui.js +1291 -193
  5. package/package.json +2 -2
  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/handoff/SKILL.md +6 -0
  13. package/web-dist/assets/{TerminalPane-DyK0ncN8.js → TerminalPane-hMf6ifuS.js} +1 -1
  14. package/web-dist/assets/{abnfDiagram-VRR7QNED-DtotTgyK.js → abnfDiagram-VRR7QNED-CQj6fwXy.js} +1 -1
  15. package/web-dist/assets/architecture-TIHT7OUA-JX8_jS4I.js +1 -0
  16. package/web-dist/assets/{architectureDiagram-ZJ3FMSHR-CVl8mUt0.js → architectureDiagram-ZJ3FMSHR-CoE5ct03.js} +1 -1
  17. package/web-dist/assets/{blockDiagram-677ZJIJ3-VUZEsYBW.js → blockDiagram-677ZJIJ3-CEudy3zd.js} +1 -1
  18. package/web-dist/assets/{c4Diagram-LMCZKHZV-Dlg18lBF.js → c4Diagram-LMCZKHZV-ChW62IOH.js} +1 -1
  19. package/web-dist/assets/channel-y3u1yiAC.js +1 -0
  20. package/web-dist/assets/{chunk-32BRIVSS-DG8gd151.js → chunk-32BRIVSS-Bh-A2le2.js} +1 -1
  21. package/web-dist/assets/{chunk-52WLFC77-DKlahFJ2.js → chunk-52WLFC77-7Gia-F6f.js} +1 -1
  22. package/web-dist/assets/{chunk-C7G6YPKG-Bk-BFDsQ.js → chunk-C7G6YPKG-C0MCv0Ym.js} +1 -1
  23. package/web-dist/assets/{chunk-EX3LRPZG-DnXqd-jJ.js → chunk-EX3LRPZG-DPRO7Ss0.js} +1 -1
  24. package/web-dist/assets/{chunk-FWX5IMBZ-ujYyBebT.js → chunk-FWX5IMBZ-Cod9MA7c.js} +2 -2
  25. package/web-dist/assets/{chunk-HOUHSVGY-Dk2XhPWQ.js → chunk-HOUHSVGY-D1oHrXQx.js} +1 -1
  26. package/web-dist/assets/{chunk-ICXQ74PX-DoseDoV4.js → chunk-ICXQ74PX-DBLMA0ZN.js} +1 -1
  27. package/web-dist/assets/{chunk-MOJQB5TN-B8dog4zz.js → chunk-MOJQB5TN-jiLvxNvx.js} +1 -1
  28. package/web-dist/assets/{chunk-OGEWGWER-D4GkHktS.js → chunk-OGEWGWER-w1RtF7G0.js} +1 -1
  29. package/web-dist/assets/{chunk-PUDLZKDR-ChEIayRe.js → chunk-PUDLZKDR-DQlFMKm5.js} +1 -1
  30. package/web-dist/assets/{chunk-Q4XR5HBZ-BtzMOr97.js → chunk-Q4XR5HBZ-CfD6vdr0.js} +1 -1
  31. package/web-dist/assets/{chunk-V7JOEXUC-DQ7c8QAd.js → chunk-V7JOEXUC-BRB1iO1P.js} +1 -1
  32. package/web-dist/assets/{chunk-VAUOI2AC-6jWzOU_J.js → chunk-VAUOI2AC-CCeJEpxW.js} +1 -1
  33. package/web-dist/assets/{chunk-VR4S4FIN-TB0TNuya.js → chunk-VR4S4FIN-DfPRj9a1.js} +1 -1
  34. package/web-dist/assets/{chunk-WYO6CB5R-CY7fL6F5.js → chunk-WYO6CB5R-BWd84n6L.js} +1 -1
  35. package/web-dist/assets/{chunk-ZGVPDNZ5-vZO6zPtA.js → chunk-ZGVPDNZ5-Bm-3TA9N.js} +1 -1
  36. package/web-dist/assets/classDiagram-OUVF2IWQ-BL8mggXA.js +1 -0
  37. package/web-dist/assets/classDiagram-v2-EOCWNBFH-BL8mggXA.js +1 -0
  38. package/web-dist/assets/{cynefin-VYW2F7L2-D4NcAp7V.js → cynefin-VYW2F7L2-BylEqg5y.js} +1 -1
  39. package/web-dist/assets/{cynefinDiagram-TSTJHNR4-hLO8f53U.js → cynefinDiagram-TSTJHNR4-CRfGS7Ou.js} +1 -1
  40. package/web-dist/assets/{dagre-VKFMJZFB-86z9AWRw.js → dagre-VKFMJZFB-CNeTu13Z.js} +1 -1
  41. package/web-dist/assets/{diagram-FQU43EPY-DXw3bj8G.js → diagram-FQU43EPY-BYZKetkJ.js} +1 -1
  42. package/web-dist/assets/{diagram-G47NLZAW-BtH3_eiQ.js → diagram-G47NLZAW-DSOzyl5U.js} +1 -1
  43. package/web-dist/assets/{diagram-NH7WQ7WH-BxWq2H6D.js → diagram-NH7WQ7WH-DFZCvfJN.js} +1 -1
  44. package/web-dist/assets/{diagram-OA4YK3LP-BoU3I6Xx.js → diagram-OA4YK3LP-m0qY8Zlg.js} +1 -1
  45. package/web-dist/assets/{diagram-WEI45ONY-CkPD-vrA.js → diagram-WEI45ONY-Cg7Qz-0T.js} +1 -1
  46. package/web-dist/assets/{ebnfDiagram-CCIWWBDH-BudQTnvf.js → ebnfDiagram-CCIWWBDH-DGvTjoXK.js} +1 -1
  47. package/web-dist/assets/{erDiagram-Q63AITRT-CRFLgkaW.js → erDiagram-Q63AITRT-BFqTK5TB.js} +1 -1
  48. package/web-dist/assets/eventmodeling-45OFAUF4-BaCHCepY.js +1 -0
  49. package/web-dist/assets/flowDiagram-23GEKE2U-Cjl6weoT.js +1 -0
  50. package/web-dist/assets/{ganttDiagram-NO4QXBWP-DFmOc45y.js → ganttDiagram-NO4QXBWP-KbR9IE0O.js} +1 -1
  51. package/web-dist/assets/{gitGraph-TEB2WS4Q-CMW6meDa.js → gitGraph-TEB2WS4Q-hCbzZO_l.js} +1 -1
  52. package/web-dist/assets/{gitGraphDiagram-IHSO6WYX-CHelbJfg.js → gitGraphDiagram-IHSO6WYX-CNPRTwQG.js} +1 -1
  53. package/web-dist/assets/index-BUY5ijyI.css +1 -0
  54. package/web-dist/assets/{index-C2Tf3Ihs.js → index-Bj6YWd3g.js} +68 -68
  55. package/web-dist/assets/{info-DKCQHKI2-DZT4W61r.js → info-DKCQHKI2-Zd_TXMfr.js} +1 -1
  56. package/web-dist/assets/{infoDiagram-FWYZ7A6U-Dj0tLhVO.js → infoDiagram-FWYZ7A6U-DQ9zmVhJ.js} +1 -1
  57. package/web-dist/assets/{ishikawaDiagram-FXEZZL3T-D825ES0c.js → ishikawaDiagram-FXEZZL3T-BFyJ5E_L.js} +1 -1
  58. package/web-dist/assets/{journeyDiagram-5HDEW3XC-D53Igq5c.js → journeyDiagram-5HDEW3XC-BgjbY7_T.js} +1 -1
  59. package/web-dist/assets/{kanban-definition-HUTT4EX6-CJ-F0vAo.js → kanban-definition-HUTT4EX6-mpUEl8xx.js} +1 -1
  60. package/web-dist/assets/{line-CuiC27jO.js → line-CM_FUYGm.js} +1 -1
  61. package/web-dist/assets/{mermaid-parser.core-rG6qAIn0.js → mermaid-parser.core-CTVz2V70.js} +3 -3
  62. package/web-dist/assets/{mermaid.core-DzNUOuDq.js → mermaid.core-BCLNmVvR.js} +3 -3
  63. package/web-dist/assets/{mindmap-definition-LN4V7U3C-DHfhQue9.js → mindmap-definition-LN4V7U3C-CgJU-6O7.js} +1 -1
  64. package/web-dist/assets/{packet-7NZHBO7P-BiUlKhVG.js → packet-7NZHBO7P-rkLFb4ZX.js} +1 -1
  65. package/web-dist/assets/{pegDiagram-2B236MQR-MQdiKZx3.js → pegDiagram-2B236MQR-Sy2oYuu1.js} +1 -1
  66. package/web-dist/assets/{pie-RZYD4A2V-dNHUvvE3.js → pie-RZYD4A2V-0yTxNtqZ.js} +1 -1
  67. package/web-dist/assets/{pieDiagram-ENE6RG2P-BwFxe1x1.js → pieDiagram-ENE6RG2P-CQ7YFWhH.js} +1 -1
  68. package/web-dist/assets/{quadrantDiagram-ABIIQ3AL-9dic-8vv.js → quadrantDiagram-ABIIQ3AL-ClCTxrOj.js} +1 -1
  69. package/web-dist/assets/{radar-I7S5WNFK-CK3dl20X.js → radar-I7S5WNFK-JUOW5mdN.js} +1 -1
  70. package/web-dist/assets/{railroad-3IZDKUUU-BuB2LkFj.js → railroad-3IZDKUUU-DJ2IS1Jg.js} +1 -1
  71. package/web-dist/assets/railroad-abnf-AHOZXSZD-CU4HK7Ek.js +1 -0
  72. package/web-dist/assets/railroad-ebnf-EBAXGLYW-CarBAd7R.js +1 -0
  73. package/web-dist/assets/railroad-peg-LSFZ7HO6-ByEhVLyD.js +1 -0
  74. package/web-dist/assets/{railroadDiagram-RFXS5EU6-CuuspS_3.js → railroadDiagram-RFXS5EU6-BNsVGeA3.js} +1 -1
  75. package/web-dist/assets/{requirementDiagram-TGXJPOKE-DHAILLdh.js → requirementDiagram-TGXJPOKE-BHa3IX0k.js} +1 -1
  76. package/web-dist/assets/{sankeyDiagram-HTMAVEWB-CztQ2t1b.js → sankeyDiagram-HTMAVEWB-CSbsLbtw.js} +1 -1
  77. package/web-dist/assets/{sequenceDiagram-DBY2YBRQ-jk56eo0l.js → sequenceDiagram-DBY2YBRQ-BgVWj1nN.js} +1 -1
  78. package/web-dist/assets/{stateDiagram-2N3HPSRC-uwqBWnrI.js → stateDiagram-2N3HPSRC-BKHYNxA3.js} +1 -1
  79. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-C-k0bbcG.js +1 -0
  80. package/web-dist/assets/{swimlanes-5IMT3BWC-BzuJ77j6.js → swimlanes-5IMT3BWC-BluloqBJ.js} +1 -1
  81. package/web-dist/assets/swimlanesDiagram-G3AALYLV-DVjLYZbp.js +8 -0
  82. package/web-dist/assets/{timeline-definition-FHXFAJF6-CVDNkzi9.js → timeline-definition-FHXFAJF6-Bj1WKNgB.js} +1 -1
  83. package/web-dist/assets/{treeView-QDETBFTQ-CdkPxaDl.js → treeView-QDETBFTQ-Dj5T24Qr.js} +1 -1
  84. package/web-dist/assets/{treemap-6X3UGDF4-lnksCHdn.js → treemap-6X3UGDF4-BPqewrXp.js} +1 -1
  85. package/web-dist/assets/{vennDiagram-L72KCM5P-B533fRM0.js → vennDiagram-L72KCM5P-BhggGhQv.js} +1 -1
  86. package/web-dist/assets/{wardley-OPB4EBWU-DwKf-2YJ.js → wardley-OPB4EBWU-Di9GWKYt.js} +1 -1
  87. package/web-dist/assets/{wardleyDiagram-EHGQE667-BDDN72B1.js → wardleyDiagram-EHGQE667-D-mtWLKf.js} +1 -1
  88. package/web-dist/assets/{xychartDiagram-FW5EYKEG-CkvHmNVt.js → xychartDiagram-FW5EYKEG-CU0us6D_.js} +1 -1
  89. package/web-dist/index.html +2 -2
  90. package/web-dist/assets/architecture-TIHT7OUA-B23bEjC4.js +0 -1
  91. package/web-dist/assets/channel-BMFwXMMq.js +0 -1
  92. package/web-dist/assets/classDiagram-OUVF2IWQ-DiWatUgG.js +0 -1
  93. package/web-dist/assets/classDiagram-v2-EOCWNBFH-DiWatUgG.js +0 -1
  94. package/web-dist/assets/eventmodeling-45OFAUF4-C7ZRnE7-.js +0 -1
  95. package/web-dist/assets/flowDiagram-23GEKE2U-DKS-kFcF.js +0 -1
  96. package/web-dist/assets/index-CeAC3wUW.css +0 -1
  97. package/web-dist/assets/railroad-abnf-AHOZXSZD-C1-MJAeY.js +0 -1
  98. package/web-dist/assets/railroad-ebnf-EBAXGLYW-crQCD9rJ.js +0 -1
  99. package/web-dist/assets/railroad-peg-LSFZ7HO6-Ba-_QGKi.js +0 -1
  100. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-DOPbAEV3.js +0 -1
  101. package/web-dist/assets/swimlanesDiagram-G3AALYLV-BRh1ca21.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(),
@@ -6582,7 +6770,37 @@ function formatGithubWakeSteer({ ref, items, omitted }) {
6582
6770
 
6583
6771
  ${lines}${more}${reviewTail}`;
6584
6772
  }
6585
- 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_REVIEW_LEAD, 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;
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 };
6802
+ }
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;
6586
6804
  var init_src = __esm({
6587
6805
  "packages/shared/src/index.ts"() {
6588
6806
  "use strict";
@@ -7216,7 +7434,13 @@ var init_src = __esm({
7216
7434
  sessionId: external_exports.string().min(1),
7217
7435
  message: external_exports.string().min(1),
7218
7436
  // Generated once before the optimistic clear so a transport replay can be idempotent.
7219
- 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()
7220
7444
  });
7221
7445
  UnqueueFollowUpInput = external_exports.object({
7222
7446
  slug: ThreadSlug,
@@ -7287,6 +7511,25 @@ ${DISPATCH_TASK_BANNER}
7287
7511
  GITHUB_DISPATCH_UI_BOUNDARY = "<!-- fray-ui:github-dispatch-ui-boundary:v1 -->";
7288
7512
  WAKE_DELIVERY_TOKEN_TAIL = /\n*<!-- fray-wake:[A-Za-z0-9_-]+ -->\s*$/;
7289
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
+ });
7290
7533
  WAKE_SCOPE = "ignore older activity you have already handled";
7291
7534
  WAKE_REVIEW_LEAD = "A review's body is often empty because its substance is inline comments. Read them, one call each:";
7292
7535
  WAKE_REF = String.raw`[A-Za-z0-9][\w.-]*\/[A-Za-z0-9][\w.-]*#\d+`;
@@ -7296,6 +7539,8 @@ ${DISPATCH_TASK_BANNER}
7296
7539
  WAKE_MULTI_HEAD = new RegExp(
7297
7540
  String.raw`^(👤|🤖) (\d+) new GitHub items on (${WAKE_REF})\. Read exactly these — ` + WAKE_SCOPE + String.raw` — and continue:$`
7298
7541
  );
7542
+ WAKE_ITEM = /^- (👤|🤖) (.+?) from @(\S+?)(?: at (\S+?))?(?:: (\S+))?$/;
7543
+ WAKE_MORE = /^- …and (\d+) more not listed — check .+ for the rest$/;
7299
7544
  GithubStatus = external_exports.object({
7300
7545
  installed: external_exports.boolean(),
7301
7546
  inRepo: external_exports.boolean(),
@@ -7585,11 +7830,20 @@ ${DISPATCH_TASK_BANNER}
7585
7830
  // non-reasoning messages and on any reasoning block whose timing couldn't be derived.
7586
7831
  durationMs: external_exports.number().nonnegative().optional(),
7587
7832
  // A turn-BOUNDARY marker: this `kind:"event"` line was emitted at the position an EXTERNAL wake — a
7588
- // background task/shell completion `<task-notification>` re-invoked the agent and started a fresh
7589
- // turn. The client renders it as a centered divider rule carrying the cause label, so two consecutive
7590
- // assistant turns (each with its own trailing signal) no longer paint as one seamless bubble. Additive
7591
- // + optional: an old client ignores it and shows the plain quiet event line (graceful degrade).
7592
- 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(),
7593
7847
  // Block-ordered content for an assistant turn (see TranscriptPart). Defaults to [] so a pre-restart
7594
7848
  // server (which ships only text/tools) parses; the client renders `parts` when non-empty and falls
7595
7849
  // back to the legacy tools-then-text layout when it's empty. `text`/`tools` stay populated for that
@@ -7618,6 +7872,17 @@ ${DISPATCH_TASK_BANNER}
7618
7872
  // old client ignores it and shows the plain bubble (the previous behavior), and an old server simply
7619
7873
  // never sets it.
7620
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(),
7621
7886
  // A SUB-AGENT (or peer session) wrote this user turn, not the human — the same defect class `wake`
7622
7887
  // above corrects. Claude Code's agent-to-agent channel (a background child calling
7623
7888
  // `SendMessage({to:"main"})`) delivers UPWARD into the parent's queue like any follow-up, so the
@@ -10010,15 +10275,15 @@ var init_settings = __esm({
10010
10275
  });
10011
10276
 
10012
10277
  // packages/server/src/discover.ts
10013
- import { readdirSync, statSync as statSync4, openSync as openSync4, readSync, closeSync as closeSync4 } from "node:fs";
10014
- 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";
10015
10280
  function sentinelFor(sessionId) {
10016
10281
  return `threads/${sessionId}/scratch.md`;
10017
10282
  }
10018
10283
  function readHead(path) {
10019
10284
  let fd;
10020
10285
  try {
10021
- fd = openSync4(path, "r");
10286
+ fd = openSync5(path, "r");
10022
10287
  const buf = Buffer.allocUnsafe(HEAD_BYTES);
10023
10288
  const n = readSync(fd, buf, 0, HEAD_BYTES, 0);
10024
10289
  return buf.toString("utf8", 0, n);
@@ -10039,7 +10304,7 @@ function discoverTranscriptId(logDir, sessionId, opts = {}) {
10039
10304
  const sentinel = sentinelFor(sessionId);
10040
10305
  let names;
10041
10306
  try {
10042
- names = readdirSync(logDir);
10307
+ names = readdirSync2(logDir);
10043
10308
  } catch {
10044
10309
  return void 0;
10045
10310
  }
@@ -10048,10 +10313,10 @@ function discoverTranscriptId(logDir, sessionId, opts = {}) {
10048
10313
  if (name.startsWith(".") || !name.endsWith(".jsonl")) continue;
10049
10314
  const id = name.slice(0, -".jsonl".length);
10050
10315
  if (!id || id === sessionId || exclude?.has(id)) continue;
10051
- const path = join5(logDir, name);
10316
+ const path = join6(logDir, name);
10052
10317
  let mtime;
10053
10318
  try {
10054
- mtime = statSync4(path).mtimeMs;
10319
+ mtime = statSync5(path).mtimeMs;
10055
10320
  } catch {
10056
10321
  continue;
10057
10322
  }
@@ -10423,7 +10688,7 @@ function spawnWithRunner(slug, cmd, cwd, env, options, runner) {
10423
10688
  invalidateLiveness();
10424
10689
  return identity;
10425
10690
  } catch {
10426
- 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"})`);
10427
10692
  throw new TmuxSpawnError(stage, identity);
10428
10693
  }
10429
10694
  }
@@ -10993,6 +11258,7 @@ var init_tmux = __esm({
10993
11258
  "use strict";
10994
11259
  init_src();
10995
11260
  init_tmux_socket();
11261
+ init_logging();
10996
11262
  init_tmux_socket();
10997
11263
  execFileAsync = promisify(execFile);
10998
11264
  socket = "fray";
@@ -11316,6 +11582,28 @@ A \` \`\`\`question \` block IS the handback \u2014 do not also emit a \`done\`/
11316
11582
  as your next user message, possibly as terse as "1: A, 2: B".`;
11317
11583
  STOP_CRITERION = `## The stop criterion
11318
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
+
11319
11607
  **Your default is to DECIDE.** A reversible call costs minutes to redo; a round-trip to the human costs
11320
11608
  hours \u2014 they may not read your question for hours, and the whole effort sits idle until they do. If it
11321
11609
  is derivable from the code, the conventions, or ordinary engineering judgment, it is yours: make it,
@@ -11369,6 +11657,12 @@ rate-limited to once every two minutes so it cannot create a tight stop loop. Re
11369
11657
  \`stop_hook\` to deregister it before genuinely finishing. This is a rest-time nudge, not a timer and
11370
11658
  cannot wake a turn that already stopped.
11371
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.
11372
11666
  - **It is your compaction-survival mechanism, so use it deliberately.** A long effort WILL be
11373
11667
  compacted, and compaction drops the REASONING first \u2014 the plan, the alternatives you ruled out, why
11374
11668
  the human chose what they chose. A summary preserves what you did, not why. So write that here AS
@@ -11408,6 +11702,13 @@ rate-limited to once every two minutes so it cannot create a tight stop loop. Re
11408
11702
  \`stop_hook\` to deregister it before genuinely finishing. This is a rest-time nudge, not a timer and
11409
11703
  cannot wake a turn that already stopped.
11410
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
+
11411
11712
  **It is your compaction-survival mechanism, so use it deliberately.** A long effort WILL be compacted,
11412
11713
  and compaction drops the REASONING first \u2014 the plan, the alternatives you ruled out, why the human
11413
11714
  chose what they chose. A summary preserves what you did, not why. So write that here AS YOU GO rather
@@ -11735,10 +12036,10 @@ var init_detached_daemons = __esm({
11735
12036
  });
11736
12037
 
11737
12038
  // packages/server/src/backend/claude-broker-diagnostics.ts
11738
- import { appendFileSync, mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync4, statSync as statSync5 } from "node:fs";
11739
- 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";
11740
12041
  function claudeBrokerDiagnosticLogPath(stateDir, sessionId) {
11741
- return join6(stateDir, "claude-broker", `${sessionId}.diagnostics.log`);
12042
+ return join7(stateDir, "claude-broker", `${sessionId}.diagnostics.log`);
11742
12043
  }
11743
12044
  function readClaudeBrokerExit(stateDir, sessionId) {
11744
12045
  let newest = null;
@@ -11782,14 +12083,14 @@ var init_claude_broker_diagnostics = __esm({
11782
12083
  // packages/server/src/backend/claude-broker-host.ts
11783
12084
  import { spawn as spawn2 } from "node:child_process";
11784
12085
  import { createHash as createHash4, randomUUID as randomUUID8 } from "node:crypto";
11785
- import { accessSync, constants as fsConstants, mkdirSync as mkdirSync6, readFileSync as readFileSync6, readdirSync as readdirSync2, unlinkSync } from "node:fs";
11786
- 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";
11787
12088
  function resolveClaudeExecutableAbsolute(bin, env = process.env) {
11788
12089
  const candidate = bin && bin.length > 0 ? bin : "claude";
11789
12090
  if (isAbsolute4(candidate)) return candidate;
11790
12091
  for (const dir of (env.PATH ?? "").split(delimiter)) {
11791
12092
  if (!dir) continue;
11792
- const full = join7(dir, candidate);
12093
+ const full = join8(dir, candidate);
11793
12094
  try {
11794
12095
  accessSync(full, fsConstants.X_OK);
11795
12096
  return full;
@@ -11801,11 +12102,11 @@ function resolveClaudeExecutableAbsolute(bin, env = process.env) {
11801
12102
  function claudeBrokerSocketPath(stateDir, sessionId) {
11802
12103
  const key = createHash4("sha256").update(stateDir).update("\0").update(sessionId).digest("hex").slice(0, 16);
11803
12104
  if (process.platform === "win32") return `\\\\.\\pipe\\fray-claude-${key}`;
11804
- return join7(process.env.TMPDIR ?? "/tmp", `fray-claude-${key}.sock`);
12105
+ return join8(process.env.TMPDIR ?? "/tmp", `fray-claude-${key}.sock`);
11805
12106
  }
11806
12107
  function claudeBrokerRecordPath(stateDir, sessionId) {
11807
12108
  const key = createHash4("sha256").update(sessionId).digest("hex").slice(0, 16);
11808
- return join7(stateDir, "claude-broker", `${key}.json`);
12109
+ return join8(stateDir, "claude-broker", `${key}.json`);
11809
12110
  }
11810
12111
  function pidAlive(pid) {
11811
12112
  try {
@@ -11834,17 +12135,17 @@ function liveBrokerRecord(recordPath2) {
11834
12135
  return null;
11835
12136
  }
11836
12137
  function liveBrokerRecords(stateDir) {
11837
- const dir = join7(stateDir, "claude-broker");
12138
+ const dir = join8(stateDir, "claude-broker");
11838
12139
  let names;
11839
12140
  try {
11840
- names = readdirSync2(dir);
12141
+ names = readdirSync3(dir);
11841
12142
  } catch {
11842
12143
  return [];
11843
12144
  }
11844
12145
  const out = [];
11845
12146
  for (const name of names) {
11846
12147
  if (!name.endsWith(".json")) continue;
11847
- const record = liveBrokerRecord(join7(dir, name));
12148
+ const record = liveBrokerRecord(join8(dir, name));
11848
12149
  if (record) out.push(record);
11849
12150
  }
11850
12151
  return out;
@@ -11852,7 +12153,7 @@ function liveBrokerRecords(stateDir) {
11852
12153
  function forkBroker(options) {
11853
12154
  const socketPath = claudeBrokerSocketPath(options.stateDir, options.sessionId);
11854
12155
  const recordPath2 = claudeBrokerRecordPath(options.stateDir, options.sessionId);
11855
- mkdirSync6(dirname4(recordPath2), { recursive: true });
12156
+ mkdirSync7(dirname4(recordPath2), { recursive: true });
11856
12157
  const config = {
11857
12158
  socketPath,
11858
12159
  cwd: options.cwd,
@@ -12071,6 +12372,17 @@ var init_claude_broker_client = __esm({
12071
12372
  });
12072
12373
 
12073
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
+ }
12074
12386
  function utf8Bytes(value) {
12075
12387
  return encoder2.encode(value).byteLength;
12076
12388
  }
@@ -12151,8 +12463,8 @@ function validateInputMessage(value) {
12151
12463
  if (utf8Bytes(value.text) > CLAUDE_AGENT_SDK_MAX_INPUT_BYTES) {
12152
12464
  throw new ClaudeAgentSdkProtocolError(`input.text exceeds ${CLAUDE_AGENT_SDK_MAX_INPUT_BYTES} bytes`);
12153
12465
  }
12154
- const text = safeText(value.text, "input.text", CLAUDE_AGENT_SDK_MAX_INPUT_BYTES);
12155
- 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");
12156
12468
  const parentToolUseId = boundedOptionalId(value.parentToolUseId, "input.parentToolUseId");
12157
12469
  return parentToolUseId === void 0 ? { id, text } : { id, text, parentToolUseId };
12158
12470
  }
@@ -12182,7 +12494,7 @@ function validatePermissionDecision(value) {
12182
12494
  }
12183
12495
  throw new ClaudeAgentSdkProtocolError("permission decision behavior is unsupported");
12184
12496
  }
12185
- 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;
12186
12498
  var init_claude_agent_sdk_protocol = __esm({
12187
12499
  "packages/server/src/backend/claude-agent-sdk-protocol.ts"() {
12188
12500
  "use strict";
@@ -12194,6 +12506,7 @@ var init_claude_agent_sdk_protocol = __esm({
12194
12506
  UNSAFE_TEXT2 = /[\p{Cf}\p{Cs}\p{Zl}\p{Zp}\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu;
12195
12507
  ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/;
12196
12508
  RESERVED_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
12509
+ DELIVERABLE_CONTROL_CODES = /* @__PURE__ */ new Set([9, 10, 13]);
12197
12510
  CLAUDE_BROKER_CAPABILITY_SUBAGENT_STEER = "subagent-steer-v1";
12198
12511
  CLAUDE_BROKER_CAPABILITY_CANCEL_INPUT = "cancel-input-v1";
12199
12512
  CLAUDE_BROKER_CAPABILITY_STOP_TASK = "stop-task-v1";
@@ -12353,28 +12666,35 @@ var init_credential_redaction = __esm({
12353
12666
  });
12354
12667
 
12355
12668
  // packages/server/src/backend/claude-permission-interactions.ts
12669
+ import { homedir as homedir4 } from "node:os";
12356
12670
  function clip(text, max) {
12357
12671
  return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
12358
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
+ }
12359
12688
  function buildClaudePermissionInteraction(request, owner) {
12360
12689
  const tool = request.toolName || "tool";
12361
- let inputSummary = "";
12362
- try {
12363
- inputSummary = clip(redactCredentialSyntax(JSON.stringify(request.input ?? {})), 3800);
12364
- } catch {
12365
- inputSummary = "(uninspectable input)";
12366
- }
12367
- 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) : "";
12368
12693
  const message = clip(
12369
- `A ${tool} tool call needs your approval before it runs.` + (request.description ? `
12370
-
12371
- ${clip(redactCredentialSyntax(request.description), 1e3)}` : "") + (inputSummary ? `
12372
-
12373
- Input: ${inputSummary}` : "") + (request.blockedPath ? `
12374
-
12375
- Path: ${clip(request.blockedPath, 500)}` : ""),
12376
- 7900
12694
+ [description, request.blockedPath ? `Blocked path: ${clip(request.blockedPath, 500)}` : ""].filter(Boolean).join("\n\n"),
12695
+ 2e3
12377
12696
  );
12697
+ const preview = inputPreview(request.input, request.description);
12378
12698
  const parsed = InteractionRequest.safeParse({
12379
12699
  protocolVersion: INTERACTION_PROTOCOL_VERSION,
12380
12700
  contentFormat: "plain-text",
@@ -12394,12 +12714,14 @@ Path: ${clip(request.blockedPath, 500)}` : ""),
12394
12714
  providerRequestId: clip(request.requestId, 500),
12395
12715
  allowedDecisions: ALLOWED_DECISIONS,
12396
12716
  payload: {
12397
- title,
12398
- message,
12399
12717
  kind: "permission-approval",
12400
- permission: clip(request.requestId, 500),
12401
- resourceLabel: clip(tool, 2e3),
12402
- 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) }
12403
12725
  },
12404
12726
  expiresAt: null
12405
12727
  });
@@ -12553,7 +12875,7 @@ function claudeQuestionDecisionFor(spec, decisionId, values) {
12553
12875
  message: "Fray could not return the operator's answer in a form this tool accepts. Ask the question in your final message instead."
12554
12876
  };
12555
12877
  }
12556
- 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;
12557
12879
  var init_claude_permission_interactions = __esm({
12558
12880
  "packages/server/src/backend/claude-permission-interactions.ts"() {
12559
12881
  "use strict";
@@ -12568,6 +12890,9 @@ var init_claude_permission_interactions = __esm({
12568
12890
  { id: CLAUDE_PERMISSION_DECISIONS.allow, semantic: "approve", label: "Grant for turn", description: "Allow this tool call to run once." },
12569
12891
  { id: CLAUDE_PERMISSION_DECISIONS.deny, semantic: "deny", label: "Deny", description: "Block this tool call." }
12570
12892
  ];
12893
+ SUBJECT_KEYS = ["command", "file_path", "notebook_path", "path", "url", "pattern", "query", "prompt"];
12894
+ PREVIEW_MAX_CHARS = 6e3;
12895
+ PREVIEW_MAX_LINES = 200;
12571
12896
  CLAUDE_ASK_USER_QUESTION_TOOL = "AskUserQuestion";
12572
12897
  CLAUDE_QUESTION_DECISIONS = { answer: "answer", decline: "decline" };
12573
12898
  QUESTION_DECISIONS = [
@@ -12594,6 +12919,20 @@ function claudeBrokerBridgeEnabled(env = process.env) {
12594
12919
  function inputIdFor(deliveryId) {
12595
12920
  return deliveryId && UUID_SHAPE.test(deliveryId) ? deliveryId : randomUUID9();
12596
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
+ }
12597
12936
  function createClaudeAgentBrokerBridge(deps) {
12598
12937
  const sessions = /* @__PURE__ */ new Map();
12599
12938
  const executablePath = resolveClaudeExecutableAbsolute(deps.executablePath, deps.env);
@@ -12713,11 +13052,13 @@ function createClaudeAgentBrokerBridge(deps) {
12713
13052
  killBroker(deps.stateDir, prior.sessionId);
12714
13053
  sessions.delete(input.threadSlug);
12715
13054
  }
13055
+ const message = validatedInput({ id: randomUUID9(), text: input.prompt });
12716
13056
  const session = await attach3(input.threadSlug, input.sessionId, input.cwd, input.permissionMode ?? "default", { appendSystemPrompt: input.appendSystemPrompt, model: input.model, effort: input.effort });
12717
- session.client.sendInput({ id: randomUUID9(), text: input.prompt });
13057
+ session.client.sendInput(message);
12718
13058
  return { binding: { threadSlug: input.threadSlug, sessionId: input.sessionId, cwd: input.cwd, generation: session.generation, state: "active" } };
12719
13059
  },
12720
13060
  async followUp(input) {
13061
+ const message = validatedInput({ id: inputIdFor(input.deliveryId), text: input.text });
12721
13062
  let held = current(input.threadSlug, input.sessionId);
12722
13063
  if (held && !holdsLiveDaemon(held)) {
12723
13064
  held.client.close();
@@ -12737,7 +13078,7 @@ function createClaudeAgentBrokerBridge(deps) {
12737
13078
  input.permissionMode ?? "default",
12738
13079
  { resume: true, appendSystemPrompt: input.appendSystemPrompt, model: input.model, effort: input.effort }
12739
13080
  );
12740
- session.client.sendInput({ id: inputIdFor(input.deliveryId), text: input.text });
13081
+ session.client.sendInput(message);
12741
13082
  },
12742
13083
  async cancelFollowUp(input) {
12743
13084
  const held = current(input.threadSlug, input.sessionId);
@@ -12859,15 +13200,15 @@ var init_claude_agent_broker_bridge = __esm({
12859
13200
  });
12860
13201
 
12861
13202
  // packages/server/src/backend/claude-quota.ts
12862
- import { join as join8 } from "node:path";
12863
- 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";
12864
13205
  import { createHash as createHash5 } from "node:crypto";
12865
13206
  import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
12866
13207
  import { execFile as execFile2 } from "node:child_process";
12867
13208
  import { promisify as promisify2 } from "node:util";
12868
13209
  function claudeConfigDir() {
12869
13210
  const override = process.env.CLAUDE_CONFIG_DIR;
12870
- return override && override.trim() ? override : join8(homedir3(), ".claude");
13211
+ return override && override.trim() ? override : join9(homedir5(), ".claude");
12871
13212
  }
12872
13213
  function tokenFromCredentialsJson(raw2) {
12873
13214
  let doc;
@@ -12896,7 +13237,7 @@ async function readKeychainToken() {
12896
13237
  }
12897
13238
  async function readAccessToken(configDir) {
12898
13239
  try {
12899
- const fromFile = tokenFromCredentialsJson(await readFile(join8(configDir, ".credentials.json"), "utf8"));
13240
+ const fromFile = tokenFromCredentialsJson(await readFile(join9(configDir, ".credentials.json"), "utf8"));
12900
13241
  if (fromFile) return fromFile;
12901
13242
  } catch {
12902
13243
  }
@@ -12965,8 +13306,8 @@ function parseClaudeUsage(body, planType) {
12965
13306
  function cachePaths(cacheDir, configDir) {
12966
13307
  const profile = createHash5("sha256").update(configDir).digest("hex").slice(0, 12);
12967
13308
  return {
12968
- data: join8(cacheDir, `claude-${profile}.json`),
12969
- lock: join8(cacheDir, `claude-${profile}.lock`)
13309
+ data: join9(cacheDir, `claude-${profile}.json`),
13310
+ lock: join9(cacheDir, `claude-${profile}.lock`)
12970
13311
  };
12971
13312
  }
12972
13313
  async function readShared(path) {
@@ -13158,7 +13499,7 @@ function refreshSharedInBackground(paths, claudeBin, deps, now) {
13158
13499
  async function refreshClaudeQuotaInBackground(claudeBin = "claude", deps = {}) {
13159
13500
  const now = (deps.now ?? Date.now)();
13160
13501
  const configDir = claudeConfigDir();
13161
- const cacheDir = deps.cacheDir ?? join8(homedir3(), ".fray", "quota-cache");
13502
+ const cacheDir = deps.cacheDir ?? join9(homedir5(), ".fray", "quota-cache");
13162
13503
  try {
13163
13504
  await mkdir(cacheDir, { recursive: true, mode: 448 });
13164
13505
  } catch {
@@ -13169,7 +13510,7 @@ async function refreshClaudeQuotaInBackground(claudeBin = "claude", deps = {}) {
13169
13510
  async function readClaudeQuota(claudeBin = "claude", deps = {}, options = {}) {
13170
13511
  const now = (deps.now ?? Date.now)();
13171
13512
  const configDir = claudeConfigDir();
13172
- const cacheDir = deps.cacheDir ?? join8(homedir3(), ".fray", "quota-cache");
13513
+ const cacheDir = deps.cacheDir ?? join9(homedir5(), ".fray", "quota-cache");
13173
13514
  const paths = cachePaths(cacheDir, configDir);
13174
13515
  try {
13175
13516
  await mkdir(cacheDir, { recursive: true, mode: 448 });
@@ -13257,19 +13598,19 @@ var init_claude_quota = __esm({
13257
13598
  });
13258
13599
 
13259
13600
  // packages/server/src/backend/auth-status.ts
13260
- import { join as join9 } from "node:path";
13261
- 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";
13262
13603
  import { readFileSync as readFileSync7 } from "node:fs";
13263
13604
  import { execFile as execFile3 } from "node:child_process";
13264
13605
  import { promisify as promisify3 } from "node:util";
13265
13606
  function claudeConfigDir2() {
13266
13607
  const override = process.env.CLAUDE_CONFIG_DIR;
13267
- return override && override.trim() ? override : join9(homedir4(), ".claude");
13608
+ return override && override.trim() ? override : join10(homedir6(), ".claude");
13268
13609
  }
13269
13610
  function claudeFileState(configDir) {
13270
13611
  let raw2;
13271
13612
  try {
13272
- raw2 = readFileSync7(join9(configDir, ".credentials.json"), "utf8");
13613
+ raw2 = readFileSync7(join10(configDir, ".credentials.json"), "utf8");
13273
13614
  } catch (err) {
13274
13615
  return err.code === "ENOENT" ? "absent" : "error";
13275
13616
  }
@@ -13302,7 +13643,7 @@ function readCodexAuthState(codexHome = defaultCodexHome()) {
13302
13643
  if (process.env.OPENAI_API_KEY || process.env.CODEX_API_KEY || process.env.CODEX_ACCESS_TOKEN) return "authed";
13303
13644
  let raw2;
13304
13645
  try {
13305
- raw2 = readFileSync7(join9(codexHome, "auth.json"), "utf8");
13646
+ raw2 = readFileSync7(join10(codexHome, "auth.json"), "utf8");
13306
13647
  } catch (err) {
13307
13648
  return err.code === "ENOENT" ? "signed-out" : "unknown";
13308
13649
  }
@@ -13392,7 +13733,7 @@ var init_auth_status = __esm({
13392
13733
  // packages/server/src/fray.ts
13393
13734
  import { execFile as execFile4 } from "node:child_process";
13394
13735
  import { lstatSync, realpathSync as realpathSync4 } from "node:fs";
13395
- 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";
13396
13737
  import { promisify as promisify4 } from "node:util";
13397
13738
  function frayScriptsDir() {
13398
13739
  if (process.env.FRAY_SCRIPTS_DIR) return process.env.FRAY_SCRIPTS_DIR;
@@ -13401,7 +13742,7 @@ function frayScriptsDir() {
13401
13742
  function directFrayRoot(projectDir) {
13402
13743
  try {
13403
13744
  const projectRoot = realpathSync4(projectDir);
13404
- const path = join10(projectRoot, ".fray");
13745
+ const path = join11(projectRoot, ".fray");
13405
13746
  const stat2 = lstatSync(path);
13406
13747
  if (!stat2.isDirectory() || stat2.isSymbolicLink()) return null;
13407
13748
  const real = realpathSync4(path);
@@ -13415,7 +13756,7 @@ function frayDirExists(projectDir) {
13415
13756
  }
13416
13757
  async function readBoard(projectDir, scriptsDir = frayScriptsDir()) {
13417
13758
  if (!directFrayRoot(projectDir)) throw new Error("unsafe or missing .fray directory");
13418
- const { stdout } = await execFileP("node", [join10(scriptsDir, "index.mjs"), "--json"], {
13759
+ const { stdout } = await execFileP("node", [join11(scriptsDir, "index.mjs"), "--json"], {
13419
13760
  cwd: projectDir,
13420
13761
  env: { ...process.env, CLAUDE_PROJECT_DIR: projectDir },
13421
13762
  maxBuffer: 32 * 1024 * 1024
@@ -13440,7 +13781,7 @@ async function readBoard(projectDir, scriptsDir = frayScriptsDir()) {
13440
13781
  };
13441
13782
  }
13442
13783
  async function runThreadUpdate(projectDir, slug, args, scriptsDir = frayScriptsDir()) {
13443
- await execFileP("node", [join10(scriptsDir, "thread-update.mjs"), slug, ...args], {
13784
+ await execFileP("node", [join11(scriptsDir, "thread-update.mjs"), slug, ...args], {
13444
13785
  cwd: projectDir,
13445
13786
  env: { ...process.env, CLAUDE_PROJECT_DIR: projectDir }
13446
13787
  });
@@ -13455,12 +13796,12 @@ var init_fray = __esm({
13455
13796
  });
13456
13797
 
13457
13798
  // packages/server/src/session-files.ts
13458
- import { lstatSync as lstatSync2, rmSync as rmSync4 } from "node:fs";
13799
+ import { lstatSync as lstatSync2, rmSync as rmSync5 } from "node:fs";
13459
13800
  import { tmpdir as tmpdir2 } from "node:os";
13460
- import { join as join11 } from "node:path";
13801
+ import { join as join12 } from "node:path";
13461
13802
  function systemPromptPath(sessionId) {
13462
13803
  if (!SESSION_ID_RE.test(sessionId)) throw new Error("invalid session id");
13463
- return join11(SYSTEM_PROMPT_DIR, `${sessionId}.md`);
13804
+ return join12(SYSTEM_PROMPT_DIR, `${sessionId}.md`);
13464
13805
  }
13465
13806
  function isDirectDirectory(path) {
13466
13807
  try {
@@ -13480,9 +13821,9 @@ function pathAbsent(path) {
13480
13821
  }
13481
13822
  function unlinkDirectChild(parent, filename) {
13482
13823
  if (!isDirectDirectory(parent)) return pathAbsent(parent);
13483
- const child = join11(parent, filename);
13824
+ const child = join12(parent, filename);
13484
13825
  try {
13485
- rmSync4(child, { force: true });
13826
+ rmSync5(child, { force: true });
13486
13827
  } catch {
13487
13828
  return false;
13488
13829
  }
@@ -13490,14 +13831,14 @@ function unlinkDirectChild(parent, filename) {
13490
13831
  }
13491
13832
  function cleanupAdoptionSessionFiles(projectDir, sessionId) {
13492
13833
  if (!SESSION_ID_RE.test(sessionId)) return false;
13493
- const frayDir = join11(projectDir, ".fray");
13834
+ const frayDir = join12(projectDir, ".fray");
13494
13835
  let clean = true;
13495
13836
  if (isDirectDirectory(frayDir)) {
13496
- const threads = join11(frayDir, "threads");
13837
+ const threads = join12(frayDir, "threads");
13497
13838
  if (isDirectDirectory(threads)) {
13498
- const child = join11(threads, sessionId);
13839
+ const child = join12(threads, sessionId);
13499
13840
  try {
13500
- rmSync4(child, { recursive: true, force: true });
13841
+ rmSync5(child, { recursive: true, force: true });
13501
13842
  } catch {
13502
13843
  clean = false;
13503
13844
  }
@@ -13510,7 +13851,7 @@ var SYSTEM_PROMPT_DIR, SESSION_ID_RE;
13510
13851
  var init_session_files = __esm({
13511
13852
  "packages/server/src/session-files.ts"() {
13512
13853
  "use strict";
13513
- SYSTEM_PROMPT_DIR = join11(tmpdir2(), "fray-sysprompts");
13854
+ SYSTEM_PROMPT_DIR = join12(tmpdir2(), "fray-sysprompts");
13514
13855
  SESSION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,199}$/;
13515
13856
  }
13516
13857
  });
@@ -13747,8 +14088,8 @@ var init_adoption_recovery = __esm({
13747
14088
  });
13748
14089
 
13749
14090
  // packages/server/src/dispatch.ts
13750
- 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";
13751
- 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";
13752
14093
  import { fileURLToPath as fileURLToPath2 } from "node:url";
13753
14094
  import { createHash as createHash6, randomUUID as randomUUID11 } from "node:crypto";
13754
14095
  function fallbackTitle(prompt) {
@@ -13768,7 +14109,7 @@ function fallbackTitle(prompt) {
13768
14109
  }
13769
14110
  function resolveSlug(frayDir, base, taken) {
13770
14111
  base = ThreadSlug.parse(base);
13771
- const isTaken = (slug) => existsSync3(join12(frayDir, `${slug}.md`)) || (taken?.(slug) ?? false);
14112
+ const isTaken = (slug) => existsSync3(join13(frayDir, `${slug}.md`)) || (taken?.(slug) ?? false);
13772
14113
  if (!isTaken(base)) return base;
13773
14114
  for (let n = 2; ; n++) {
13774
14115
  const suffix = `-${n}`;
@@ -13785,12 +14126,12 @@ function resolveLegacyThreadFile(projectDir, value) {
13785
14126
  if (!parsed.success) return null;
13786
14127
  try {
13787
14128
  const projectRoot = realpathSync5(projectDir);
13788
- const frayPath = join12(projectRoot, ".fray");
14129
+ const frayPath = join13(projectRoot, ".fray");
13789
14130
  const frayStat = lstatSync3(frayPath);
13790
14131
  if (!frayStat.isDirectory() || frayStat.isSymbolicLink()) return null;
13791
14132
  const realFray = realpathSync5(frayPath);
13792
14133
  if (dirname6(realFray) !== projectRoot || basename4(realFray) !== ".fray") return null;
13793
- const path = join12(realFray, `${parsed.data}.md`);
14134
+ const path = join13(realFray, `${parsed.data}.md`);
13794
14135
  const before = lstatSync3(path);
13795
14136
  if (!before.isFile() || before.isSymbolicLink()) return null;
13796
14137
  const realPath = realpathSync5(path);
@@ -13798,7 +14139,7 @@ function resolveLegacyThreadFile(projectDir, value) {
13798
14139
  let contents;
13799
14140
  let openedBefore;
13800
14141
  let openedAfter;
13801
- const fd = openSync5(path, constants.O_RDONLY | constants.O_NOFOLLOW);
14142
+ const fd = openSync6(path, constants.O_RDONLY | constants.O_NOFOLLOW);
13802
14143
  try {
13803
14144
  openedBefore = fstatSync(fd);
13804
14145
  contents = readFileSync8(fd);
@@ -13834,9 +14175,9 @@ function boardAuthorizesAdoption(board, slug) {
13834
14175
  return !board.errorItems.some((item) => item.file === `${slug}.md`);
13835
14176
  }
13836
14177
  function ensureSafeDirectDirectory(parent, name) {
13837
- const path = join12(parent, name);
14178
+ const path = join13(parent, name);
13838
14179
  try {
13839
- mkdirSync7(path);
14180
+ mkdirSync8(path);
13840
14181
  } catch (error) {
13841
14182
  const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
13842
14183
  if (code !== "EEXIST") throw error;
@@ -13907,14 +14248,14 @@ function writeScratchpad(projectDir, sessionId, title, kind = "claude") {
13907
14248
  const threadsDir = ensureSafeDirectDirectory(frayDir, "threads");
13908
14249
  const dir = ensureSafeDirectDirectory(threadsDir, sessionId);
13909
14250
  const rel = scratchpadRelPath(sessionId);
13910
- const path = join12(dir, "scratch.md");
13911
- const tmp = join12(dir, ".scratch.tmp");
14251
+ const path = join13(dir, "scratch.md");
14252
+ const tmp = join13(dir, ".scratch.tmp");
13912
14253
  try {
13913
- writeFileSync4(tmp, scratchpadContent(title, kind), { flag: "wx", mode: 384 });
14254
+ writeFileSync5(tmp, scratchpadContent(title, kind), { flag: "wx", mode: 384 });
13914
14255
  if (existsSync3(path)) throw new Error("scratchpad already exists");
13915
14256
  renameSync5(tmp, path);
13916
14257
  } catch (error) {
13917
- rmSync5(tmp, { force: true });
14258
+ rmSync6(tmp, { force: true });
13918
14259
  throw error;
13919
14260
  }
13920
14261
  return rel;
@@ -13932,8 +14273,9 @@ function codexScratchpadHookConfig(hookScript, sessionId) {
13932
14273
  }
13933
14274
  ]
13934
14275
  });
13935
- const bashBackgroundHook = join12(dirname6(hookScript), "bash-background.mjs");
13936
- 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");
13937
14279
  return {
13938
14280
  bypass_hook_trust: true,
13939
14281
  hooks: {
@@ -13948,12 +14290,19 @@ function codexScratchpadHookConfig(hookScript, sessionId) {
13948
14290
  }]
13949
14291
  }],
13950
14292
  // An optional scratchpad-frontmatter reminder can keep a worker from forgetting owned work when
13951
- // 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.
13952
14295
  Stop: [{
13953
- hooks: [{
13954
- type: "command",
13955
- command: `node ${JSON.stringify(scratchpadStopHook)} --session=${JSON.stringify(sessionId)}`
13956
- }]
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
+ ]
13957
14306
  }],
13958
14307
  // Native Codex children inherit the root scratchpad mandate even with `fork_turns:"none"`.
13959
14308
  // Constrain it structurally at child start: shared writes, but only merge-style scoped edits.
@@ -13966,10 +14315,10 @@ function codexScratchpadHookConfig(hookScript, sessionId) {
13966
14315
  }
13967
14316
  function scratchpadHookScript() {
13968
14317
  const plugin = workerPluginDir();
13969
- return plugin ? join12(plugin, "hooks", "scratchpad.mjs") : void 0;
14318
+ return plugin ? join13(plugin, "hooks", "scratchpad.mjs") : void 0;
13970
14319
  }
13971
14320
  function composePrompt(sessionId, prompt, kind = "claude") {
13972
- 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.`;
13973
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.";
13974
14323
  return `${scratch}${handoff}
13975
14324
 
@@ -13977,16 +14326,16 @@ function composePrompt(sessionId, prompt, kind = "claude") {
13977
14326
  ${DISPATCH_TASK_BANNER_MARKER}${prompt}`;
13978
14327
  }
13979
14328
  function scratchpadOrientation(sessionId, planPath, kind = "claude") {
13980
- 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.`;
13981
14330
  const lines = [scratch];
13982
14331
  if (planPath) lines.push(`PLAN: ${planPath} \u2014 the durable plan artifact this thread works from; read it FIRST.`);
13983
14332
  return lines.join("\n");
13984
14333
  }
13985
14334
  function frayConfigBlock(projectDir) {
13986
- const path = join12(projectDir, "FRAY.md");
14335
+ const path = join13(projectDir, "FRAY.md");
13987
14336
  let body;
13988
14337
  try {
13989
- const st = statSync6(path);
14338
+ const st = statSync7(path);
13990
14339
  if (!st.isFile() || st.size > FRAY_MD_MAX_BYTES) return "";
13991
14340
  body = readFileSync8(path, "utf8").trim();
13992
14341
  } catch {
@@ -14002,7 +14351,7 @@ ${clipped}`;
14002
14351
  }
14003
14352
  function validPlanPath(projectDir, planPath) {
14004
14353
  if (!planPath || !PLAN_PATH_RE.test(planPath)) return null;
14005
- return existsSync3(join12(projectDir, planPath)) ? planPath : null;
14354
+ return existsSync3(join13(projectDir, planPath)) ? planPath : null;
14006
14355
  }
14007
14356
  function workerPermissionMode(m) {
14008
14357
  return m === "plan" ? "auto" : m;
@@ -14014,15 +14363,15 @@ function effectivePermissionMode(kind, mode) {
14014
14363
  }
14015
14364
  function systemPromptFlags(sessionId, system) {
14016
14365
  if (!system) return [];
14017
- mkdirSync7(SYSTEM_PROMPT_DIR, { recursive: true });
14366
+ mkdirSync8(SYSTEM_PROMPT_DIR, { recursive: true });
14018
14367
  const path = systemPromptPath(sessionId);
14019
- writeFileSync4(path, system);
14368
+ writeFileSync5(path, system);
14020
14369
  return ["--append-system-prompt-file", path];
14021
14370
  }
14022
14371
  function resolveFrayMcp(stateDir, moduleUrl = import.meta.url, env = process.env) {
14023
14372
  const pluginDir = resolveWorkerPluginDir(moduleUrl, env);
14024
14373
  if (!pluginDir) return void 0;
14025
- const scriptPath = join12(pluginDir, "bin", FRAY_MCP.script);
14374
+ const scriptPath = join13(pluginDir, "bin", FRAY_MCP.script);
14026
14375
  if (!existsSync3(scriptPath)) return void 0;
14027
14376
  return { scriptPath, stateDir };
14028
14377
  }
@@ -14060,12 +14409,12 @@ function buildClaudeCommand(opts) {
14060
14409
  }
14061
14410
  function resolveWorkerPluginDir(moduleUrl = import.meta.url, env = process.env) {
14062
14411
  const override = env.FRAY_WORKER_PLUGIN_DIR;
14063
- if (override && existsSync3(join12(override, ".claude-plugin", "plugin.json")))
14412
+ if (override && existsSync3(join13(override, ".claude-plugin", "plugin.json")))
14064
14413
  return override;
14065
14414
  let current = dirname6(fileURLToPath2(moduleUrl));
14066
14415
  for (; ; ) {
14067
- const candidate = join12(current, "cc-worker");
14068
- 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;
14069
14418
  const parent = dirname6(current);
14070
14419
  if (parent === current) return void 0;
14071
14420
  current = parent;
@@ -14109,7 +14458,7 @@ function createDispatcher(deps) {
14109
14458
  const killPane2 = deps.killPane ?? killPane;
14110
14459
  const killSession2 = deps.killSession ?? killSession;
14111
14460
  const readBoardSource = deps.readBoard ?? readBoard;
14112
- const frayDir = join12(deps.project.dir, ".fray");
14461
+ const frayDir = join13(deps.project.dir, ".fray");
14113
14462
  const adoptionRuntime = deps.adoptionRuntime ?? {
14114
14463
  lookupAdoptionPane,
14115
14464
  findAdoptionPane,
@@ -14149,14 +14498,14 @@ function createDispatcher(deps) {
14149
14498
  }
14150
14499
  function writePrewrites(built) {
14151
14500
  for (const file of built.prewrite) {
14152
- if (file.mode === void 0) writeFileSync4(file.path, file.contents);
14153
- 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 });
14154
14503
  }
14155
14504
  }
14156
14505
  function cleanupPrewrites(built) {
14157
14506
  for (const path of new Set(built.prewrite.map((file) => file.path))) {
14158
14507
  try {
14159
- rmSync5(path, { force: true });
14508
+ rmSync6(path, { force: true });
14160
14509
  } catch {
14161
14510
  }
14162
14511
  }
@@ -14164,7 +14513,7 @@ function createDispatcher(deps) {
14164
14513
  function cleanupDispatchFiles(scratchRel, built, sessionId) {
14165
14514
  cleanupPrewrites(built);
14166
14515
  try {
14167
- rmSync5(join12(deps.project.dir, scratchRel), { force: true });
14516
+ rmSync6(join13(deps.project.dir, scratchRel), { force: true });
14168
14517
  } catch {
14169
14518
  }
14170
14519
  cleanupAdoptionSessionFiles(deps.project.dir, sessionId);
@@ -14952,10 +15301,10 @@ var init_permission_controller = __esm({
14952
15301
  });
14953
15302
 
14954
15303
  // packages/server/src/backend/codex-models.ts
14955
- import { join as join13 } from "node:path";
15304
+ import { join as join14 } from "node:path";
14956
15305
  import { readFileSync as readFileSync9 } from "node:fs";
14957
15306
  function cachePath(codexHome) {
14958
- return join13(codexHome, "models_cache.json");
15307
+ return join14(codexHome, "models_cache.json");
14959
15308
  }
14960
15309
  function toCodexModel(raw2) {
14961
15310
  if (!raw2 || typeof raw2 !== "object") return void 0;
@@ -15717,7 +16066,7 @@ var init_delivery_ledger = __esm({
15717
16066
  });
15718
16067
 
15719
16068
  // packages/server/src/codex-subagents.ts
15720
- 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";
15721
16070
  function createCodexSubAgentTracker(deps) {
15722
16071
  const findRollouts = deps.findRollouts ?? ((ids) => findRolloutsByIds(ids, deps.codexHome));
15723
16072
  const readAppended = deps.readAppended ?? defaultReadAppended;
@@ -15900,7 +16249,7 @@ function agentLabel(path) {
15900
16249
  function defaultReadAppended(path, offset) {
15901
16250
  let size;
15902
16251
  try {
15903
- size = statSync7(path).size;
16252
+ size = statSync8(path).size;
15904
16253
  } catch {
15905
16254
  return void 0;
15906
16255
  }
@@ -15908,7 +16257,7 @@ function defaultReadAppended(path, offset) {
15908
16257
  const from = restarted ? 0 : offset;
15909
16258
  if (size <= from) return { text: "", offset: from, restarted };
15910
16259
  try {
15911
- const fd = openSync6(path, "r");
16260
+ const fd = openSync7(path, "r");
15912
16261
  try {
15913
16262
  const buf = Buffer.allocUnsafe(size - from);
15914
16263
  const read = readSync2(fd, buf, 0, buf.length, from);
@@ -16011,15 +16360,15 @@ var init_completion_relay = __esm({
16011
16360
 
16012
16361
  // packages/server/src/tail-cache.ts
16013
16362
  import { createHash as createHash7 } from "node:crypto";
16014
- import { closeSync as closeSync7, openSync as openSync7, readFileSync as readFileSync10, readSync as readSync3, statSync as statSync8 } from "node:fs";
16015
- 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";
16016
16365
  function foldSchemaDigest(dir = import.meta.dirname) {
16017
16366
  if (foldSchemaMemo) return foldSchemaMemo;
16018
16367
  const hash = createHash7("sha256").update("fray-tail-state-v1\0");
16019
16368
  let read = 0;
16020
16369
  for (const name of FOLD_SOURCES) {
16021
16370
  try {
16022
- hash.update(name).update("\0").update(readFileSync10(join14(dir, name)));
16371
+ hash.update(name).update("\0").update(readFileSync10(join15(dir, name)));
16023
16372
  read++;
16024
16373
  } catch {
16025
16374
  hash.update(name).update("\0missing\0");
@@ -16049,9 +16398,9 @@ function measureFence(path, offset) {
16049
16398
  if (!Number.isSafeInteger(offset) || offset <= 0) return null;
16050
16399
  let fd;
16051
16400
  try {
16052
- const stat2 = statSync8(path);
16401
+ const stat2 = statSync9(path);
16053
16402
  if (stat2.size < offset) return null;
16054
- fd = openSync7(path, "r");
16403
+ fd = openSync8(path, "r");
16055
16404
  const window = Math.min(SAMPLE_BYTES, offset);
16056
16405
  const head = Buffer.allocUnsafe(window);
16057
16406
  const headRead = readSync3(fd, head, 0, window, 0);
@@ -16215,9 +16564,9 @@ var init_tail_cache = __esm({
16215
16564
  });
16216
16565
 
16217
16566
  // packages/server/src/tailer.ts
16218
- 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";
16219
- import { join as join15 } from "node:path";
16220
- 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";
16221
16570
  function activityMinute(at) {
16222
16571
  if (!at) return "";
16223
16572
  const ms = Date.parse(at);
@@ -16471,7 +16820,7 @@ function launchTaskId(text) {
16471
16820
  function readDescendantSidecars(sessionDir, mtimeMs) {
16472
16821
  let names;
16473
16822
  try {
16474
- names = readdirSync3(join15(sessionDir, "subagents"));
16823
+ names = readdirSync4(join16(sessionDir, "subagents"));
16475
16824
  } catch {
16476
16825
  return [];
16477
16826
  }
@@ -16482,7 +16831,7 @@ function readDescendantSidecars(sessionDir, mtimeMs) {
16482
16831
  if (!agentId) continue;
16483
16832
  let parsed;
16484
16833
  try {
16485
- parsed = JSON.parse(readFileSync11(join15(sessionDir, "subagents", name), "utf8"));
16834
+ parsed = JSON.parse(readFileSync11(join16(sessionDir, "subagents", name), "utf8"));
16486
16835
  } catch {
16487
16836
  continue;
16488
16837
  }
@@ -16496,7 +16845,7 @@ function readDescendantSidecars(sessionDir, mtimeMs) {
16496
16845
  agentType: text(meta.agentType),
16497
16846
  parentAgentId: text(meta.parentAgentId),
16498
16847
  spawnDepth: typeof meta.spawnDepth === "number" && Number.isFinite(meta.spawnDepth) ? meta.spawnDepth : void 0,
16499
- spawnedAtMs: mtimeMs(join15(sessionDir, "subagents", name))
16848
+ spawnedAtMs: mtimeMs(join16(sessionDir, "subagents", name))
16500
16849
  });
16501
16850
  }
16502
16851
  return out;
@@ -16932,13 +17281,13 @@ function isPermMarker(v) {
16932
17281
  }
16933
17282
  function defaultMtimeMs(path) {
16934
17283
  try {
16935
- return statSync9(path).mtimeMs;
17284
+ return statSync10(path).mtimeMs;
16936
17285
  } catch {
16937
17286
  return void 0;
16938
17287
  }
16939
17288
  }
16940
17289
  function defaultLogDir(project) {
16941
- return join15(homedir5(), ".claude", "projects", project.cwdSlug);
17290
+ return join16(homedir7(), ".claude", "projects", project.cwdSlug);
16942
17291
  }
16943
17292
  function defaultReadPermMarker(project) {
16944
17293
  if (!project.stateDir) return () => void 0;
@@ -17087,7 +17436,7 @@ function createTailer(deps) {
17087
17436
  return captured.kind === "captured" ? captured.text : "";
17088
17437
  }
17089
17438
  const defaultBackend = {
17090
- transcriptPath: (sessionId) => join15(logDir, `${sessionId}.jsonl`),
17439
+ transcriptPath: (sessionId) => join16(logDir, `${sessionId}.jsonl`),
17091
17440
  foldLine: (state, line) => {
17092
17441
  const rec = parseLine(line);
17093
17442
  if (rec) applyRecord(state, rec);
@@ -17189,7 +17538,7 @@ ${ask}`;
17189
17538
  return state.path.replace(/\.jsonl$/, "");
17190
17539
  }
17191
17540
  function descendantSidecars(state) {
17192
- const at = mtimeMs(join15(sessionDirOf(state), "subagents"));
17541
+ const at = mtimeMs(join16(sessionDirOf(state), "subagents"));
17193
17542
  const cached = descendantIndex.get(state.slug);
17194
17543
  if (cached && cached.at === at) return cached.all;
17195
17544
  const all = readDescendantSidecars(sessionDirOf(state), mtimeMs);
@@ -17203,7 +17552,7 @@ ${ask}`;
17203
17552
  return descendantIndex.get(state.slug)?.byToolUse.get(id);
17204
17553
  }
17205
17554
  function descendantTranscript(state, meta) {
17206
- return join15(sessionDirOf(state), "subagents", `agent-${meta.agentId}.jsonl`);
17555
+ return join16(sessionDirOf(state), "subagents", `agent-${meta.agentId}.jsonl`);
17207
17556
  }
17208
17557
  function descendantState(state, meta) {
17209
17558
  const task = deps.runtimeTasks?.(state.sessionId).find((entry) => entry.taskId === meta.agentId);
@@ -17428,7 +17777,7 @@ ${ask}`;
17428
17777
  function scanForeign(nowMs) {
17429
17778
  let names;
17430
17779
  try {
17431
- names = readdirSync3(logDir);
17780
+ names = readdirSync4(logDir);
17432
17781
  } catch {
17433
17782
  return [];
17434
17783
  }
@@ -17443,10 +17792,10 @@ ${ask}`;
17443
17792
  if (name.startsWith(".") || !name.endsWith(".jsonl")) continue;
17444
17793
  const id = name.slice(0, -".jsonl".length);
17445
17794
  if (!id || registered.has(id)) continue;
17446
- const path = join15(logDir, name);
17795
+ const path = join16(logDir, name);
17447
17796
  let mtime;
17448
17797
  try {
17449
- mtime = statSync9(path).mtimeMs;
17798
+ mtime = statSync10(path).mtimeMs;
17450
17799
  } catch {
17451
17800
  continue;
17452
17801
  }
@@ -17636,7 +17985,7 @@ ${ask}`;
17636
17985
  function consume(state, backend, onLine) {
17637
17986
  let size;
17638
17987
  try {
17639
- size = statSync9(state.path).size;
17988
+ size = statSync10(state.path).size;
17640
17989
  } catch {
17641
17990
  return;
17642
17991
  }
@@ -17647,7 +17996,7 @@ ${ask}`;
17647
17996
  if (size <= state.offset) return;
17648
17997
  let chunk = "";
17649
17998
  try {
17650
- const fd = openSync8(state.path, "r");
17999
+ const fd = openSync9(state.path, "r");
17651
18000
  try {
17652
18001
  const buf = Buffer.allocUnsafe(size - state.offset);
17653
18002
  const read = readSync4(fd, buf, 0, buf.length, state.offset);
@@ -17687,13 +18036,14 @@ ${ask}`;
17687
18036
  const authFailure = row.backend !== "codex" && isClaudeAuthErrorText(pane);
17688
18037
  if (authFailure) state.authFault = "authentication_rejected";
17689
18038
  const detail = authFailure ? "(claude authentication failure \u2014 pane content redacted; sign in and retry)" : pane.trim() || "(pane empty / unavailable)";
17690
- console.error(
17691
- `[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:
17692
18042
  ${detail.slice(0, 4e3)}`
17693
18043
  );
17694
18044
  try {
17695
- mkdirSync8(STALL_LOG_DIR, { recursive: true });
17696
- 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}
17697
18047
  captured_at: ${new Date(now()).toISOString()}
17698
18048
 
17699
18049
  ${detail}
@@ -17705,7 +18055,7 @@ ${detail}
17705
18055
  if (state.offset > 0) return true;
17706
18056
  let size = 0;
17707
18057
  try {
17708
- size = statSync9(state.path).size;
18058
+ size = statSync10(state.path).size;
17709
18059
  } catch {
17710
18060
  size = 0;
17711
18061
  }
@@ -17732,7 +18082,7 @@ ${detail}
17732
18082
  committed = false;
17733
18083
  }
17734
18084
  if (!committed) return false;
17735
- state.path = join15(logDir, `${found}.jsonl`);
18085
+ state.path = join16(logDir, `${found}.jsonl`);
17736
18086
  state.offset = 0;
17737
18087
  state.partial = "";
17738
18088
  state.primed = false;
@@ -17760,7 +18110,7 @@ ${detail}
17760
18110
  let state = states.get(row.slug);
17761
18111
  const runtimeGeneration = row.runtime_generation ?? 0;
17762
18112
  if (!state || state.sessionId !== row.session_id || state.nativeSessionId !== nativeId || state.runtimeGeneration !== runtimeGeneration) {
17763
- const path = backend.transcriptPath(nativeId) ?? join15(logDir, `${nativeId}.jsonl`);
18113
+ const path = backend.transcriptPath(nativeId) ?? join16(logDir, `${nativeId}.jsonl`);
17764
18114
  state = newTailState(row.slug, row.session_id, path, false, nativeId, runtimeGeneration);
17765
18115
  hydrateFromCache(state, row, nativeId);
17766
18116
  states.set(row.slug, state);
@@ -17999,8 +18349,9 @@ ${detail}
17999
18349
  if (elapsed > POLL_MS3) {
18000
18350
  overBudgetTicks++;
18001
18351
  if (overBudgetTicks === 1 || overBudgetTicks % 30 === 0) {
18002
- console.error(
18003
- `[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})`
18004
18355
  );
18005
18356
  }
18006
18357
  }
@@ -18010,7 +18361,7 @@ ${detail}
18010
18361
  function reportTickFailure(error) {
18011
18362
  tickFailures++;
18012
18363
  if (tickFailures === 1 || tickFailures % 50 === 0) {
18013
- 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)}`);
18014
18365
  }
18015
18366
  }
18016
18367
  function scheduleTick() {
@@ -18130,6 +18481,7 @@ var init_tailer = __esm({
18130
18481
  init_codex_subagents();
18131
18482
  init_completion_relay();
18132
18483
  init_tail_cache();
18484
+ init_logging();
18133
18485
  IDLE_BACKSTOP_MS = 5e3;
18134
18486
  RUNTIME_CHASE_MAX = 20;
18135
18487
  POLL_MS3 = 1e3;
@@ -18143,7 +18495,7 @@ var init_tailer = __esm({
18143
18495
  CACHE_FLUSH_MS = 3e4;
18144
18496
  PRIME_PROGRESS_EVERY = 20;
18145
18497
  DISCOVER_RETRY_MS = 15e3;
18146
- STALL_LOG_DIR = join15(tmpdir3(), "fray-worker-logs");
18498
+ STALL_LOG_DIR = join16(tmpdir3(), "fray-worker-logs");
18147
18499
  PERM_YES_OPTION = /(^|\n)\s*(❯\s*)?1\.\s+Yes\b/;
18148
18500
  PERM_QUESTION = /\b(?:Do you want|Would you like)\b/;
18149
18501
  PERM_FOOTER = /\bEsc to (cancel|reject)\b/;
@@ -18203,14 +18555,14 @@ var init_tailer = __esm({
18203
18555
  });
18204
18556
 
18205
18557
  // packages/server/src/backend/codex.ts
18206
- import { join as join16 } from "node:path";
18207
- import { homedir as homedir6 } from "node:os";
18208
- 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";
18209
18561
  function defaultCodexHome() {
18210
- 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");
18211
18563
  }
18212
18564
  function sessionsDir(codexHome) {
18213
- return join16(codexHome, "sessions");
18565
+ return join17(codexHome, "sessions");
18214
18566
  }
18215
18567
  function codexSandbox(mode) {
18216
18568
  switch (mode) {
@@ -18627,23 +18979,23 @@ function collectRollouts(dir, out, budget) {
18627
18979
  const files = entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).sort(descByName);
18628
18980
  for (const d of dirs) {
18629
18981
  if (budget.n <= 0) return;
18630
- collectRollouts(join16(dir, d.name), out, budget);
18982
+ collectRollouts(join17(dir, d.name), out, budget);
18631
18983
  }
18632
18984
  for (const f of files) {
18633
18985
  if (budget.n <= 0) return;
18634
18986
  let mtimeMs;
18635
18987
  try {
18636
- mtimeMs = statSync10(join16(dir, f.name)).mtimeMs;
18988
+ mtimeMs = statSync11(join17(dir, f.name)).mtimeMs;
18637
18989
  } catch {
18638
18990
  continue;
18639
18991
  }
18640
- out.push({ path: join16(dir, f.name), mtimeMs });
18992
+ out.push({ path: join17(dir, f.name), mtimeMs });
18641
18993
  budget.n--;
18642
18994
  }
18643
18995
  }
18644
18996
  function safeReaddir(dir) {
18645
18997
  try {
18646
- return readdirSync4(dir, { withFileTypes: true });
18998
+ return readdirSync5(dir, { withFileTypes: true });
18647
18999
  } catch {
18648
19000
  return [];
18649
19001
  }
@@ -22491,23 +22843,64 @@ var init_wrapper = __esm({
22491
22843
  }
22492
22844
  });
22493
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
+
22494
22887
  // packages/server/src/backend/codex-app-server-native.ts
22495
22888
  import { spawn as spawn3 } from "node:child_process";
22496
22889
  import { connect } from "node:net";
22497
22890
  import { createHash as createHash8, randomUUID as randomUUID12 } from "node:crypto";
22498
- import { existsSync as existsSync4, mkdirSync as mkdirSync9, readFileSync as readFileSync12, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "node:fs";
22499
- 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";
22500
22893
  import { PassThrough, Writable } from "node:stream";
22501
22894
  import { StringDecoder } from "node:string_decoder";
22502
22895
  function nativeDir(stateDir) {
22503
- return join17(stateDir, "codex-app-server-native");
22896
+ return join18(stateDir, "codex-app-server-native");
22504
22897
  }
22505
22898
  function nativeRecordPath(stateDir, projectId) {
22506
- return join17(nativeDir(stateDir), `${projectId}.json`);
22899
+ return join18(nativeDir(stateDir), `${projectId}.json`);
22507
22900
  }
22508
22901
  function nativeListenSocketPath(stateDir, projectId) {
22509
22902
  const key = createHash8("sha256").update(stateDir).update("\0").update(projectId).digest("hex").slice(0, 16);
22510
- return join17(process.env.TMPDIR ?? "/tmp", `fray-codex-native-${key}.sock`);
22903
+ return join18(process.env.TMPDIR ?? "/tmp", `fray-codex-native-${key}.sock`);
22511
22904
  }
22512
22905
  function pidAlive2(pid) {
22513
22906
  try {
@@ -22666,7 +23059,7 @@ function attach(record, timeoutMs) {
22666
23059
  }
22667
23060
  async function startListener(options) {
22668
23061
  const { stateDir, projectId } = options;
22669
- mkdirSync9(nativeDir(stateDir), { recursive: true });
23062
+ mkdirSync10(nativeDir(stateDir), { recursive: true });
22670
23063
  const socketPath = nativeListenSocketPath(stateDir, projectId);
22671
23064
  if (existsSync4(socketPath) && !await socketAccepting(socketPath)) {
22672
23065
  try {
@@ -22675,7 +23068,7 @@ async function startListener(options) {
22675
23068
  }
22676
23069
  }
22677
23070
  const generation = randomUUID12();
22678
- const child = spawn3(options.codexBin, ["app-server", "--listen", `unix://${socketPath}`], {
23071
+ const child = spawn3(options.codexBin, codexAppServerArgv(["--listen", `unix://${socketPath}`], options.frayMcp), {
22679
23072
  cwd: options.cwd,
22680
23073
  env: options.env,
22681
23074
  detached: true,
@@ -22689,7 +23082,7 @@ async function startListener(options) {
22689
23082
  socketPath,
22690
23083
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
22691
23084
  };
22692
- writeFileSync6(nativeRecordPath(stateDir, projectId), JSON.stringify(record));
23085
+ writeFileSync7(nativeRecordPath(stateDir, projectId), JSON.stringify(record));
22693
23086
  const deadline = Date.now() + (options.timeoutMs ?? 3e4);
22694
23087
  for (; ; ) {
22695
23088
  if (existsSync4(socketPath) && await socketAccepting(socketPath)) return record;
@@ -22703,6 +23096,8 @@ var init_codex_app_server_native = __esm({
22703
23096
  "packages/server/src/backend/codex-app-server-native.ts"() {
22704
23097
  "use strict";
22705
23098
  init_wrapper();
23099
+ init_codex_mcp();
23100
+ init_logging();
22706
23101
  PRESUMED_LOSSY_REJOIN = 1;
22707
23102
  nativeListenCodexAppServerHost = async (options) => {
22708
23103
  const timeoutMs = options.timeoutMs ?? 3e4;
@@ -22743,8 +23138,8 @@ var init_codex_app_server_native = __esm({
22743
23138
  droppedWhileDetached: PRESUMED_LOSSY_REJOIN
22744
23139
  };
22745
23140
  }
22746
- 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`);
22747
- 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"] });
22748
23143
  return { process: child, generation: randomUUID12(), reattached: false, daemonPid: process.pid, droppedWhileDetached: 0 };
22749
23144
  }
22750
23145
  };
@@ -22755,20 +23150,20 @@ var init_codex_app_server_native = __esm({
22755
23150
  import { spawn as spawn4 } from "node:child_process";
22756
23151
  import { createConnection } from "node:net";
22757
23152
  import { createHash as createHash9, randomUUID as randomUUID13 } from "node:crypto";
22758
- import { existsSync as existsSync5, mkdirSync as mkdirSync10, readFileSync as readFileSync13, unlinkSync as unlinkSync3 } from "node:fs";
22759
- 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";
22760
23155
  import { PassThrough as PassThrough2, Writable as Writable2 } from "node:stream";
22761
23156
  import { StringDecoder as StringDecoder2 } from "node:string_decoder";
22762
23157
  function daemonDir(stateDir) {
22763
- return join18(stateDir, "codex-app-server");
23158
+ return join19(stateDir, "codex-app-server");
22764
23159
  }
22765
23160
  function recordPath(stateDir, projectId) {
22766
- return join18(daemonDir(stateDir), `${projectId}.json`);
23161
+ return join19(daemonDir(stateDir), `${projectId}.json`);
22767
23162
  }
22768
23163
  function codexAppServerSocketPath(stateDir, projectId) {
22769
23164
  const key = createHash9("sha256").update(stateDir).update("\0").update(projectId).digest("hex").slice(0, 16);
22770
23165
  if (process.platform === "win32") return `\\\\.\\pipe\\fray-codex-${key}`;
22771
- return join18(process.env.TMPDIR ?? "/tmp", `fray-codex-${key}.sock`);
23166
+ return join19(process.env.TMPDIR ?? "/tmp", `fray-codex-${key}.sock`);
22772
23167
  }
22773
23168
  function pidAlive3(pid) {
22774
23169
  try {
@@ -22852,7 +23247,7 @@ async function stopCodexAppServerDaemon(stateDir, projectId, timeoutMs = 1e4) {
22852
23247
  }
22853
23248
  function forkDaemon(options) {
22854
23249
  const { stateDir, projectId } = options;
22855
- mkdirSync10(daemonDir(stateDir), { recursive: true });
23250
+ mkdirSync11(daemonDir(stateDir), { recursive: true });
22856
23251
  const record = recordPath(stateDir, projectId);
22857
23252
  try {
22858
23253
  unlinkSync3(record);
@@ -22872,6 +23267,10 @@ function forkDaemon(options) {
22872
23267
  generation,
22873
23268
  clientInfo: options.clientInfo,
22874
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),
22875
23274
  ...options.reachabilityCheckMs === void 0 ? {} : { reachabilityCheckMs: options.reachabilityCheckMs }
22876
23275
  });
22877
23276
  const child = spawn4(process.execPath, [options.daemonEntry ?? daemonEntry()], {
@@ -22982,7 +23381,7 @@ function attach2(record, timeoutMs) {
22982
23381
  });
22983
23382
  }
22984
23383
  function inProcessCodexAppServer(options) {
22985
- const child = spawn4(options.codexBin, ["app-server", "--stdio"], {
23384
+ const child = spawn4(options.codexBin, codexAppServerArgv(["--stdio"], options.frayMcp), {
22986
23385
  cwd: options.cwd,
22987
23386
  env: options.env,
22988
23387
  stdio: ["pipe", "pipe", "pipe"]
@@ -22991,7 +23390,7 @@ function inProcessCodexAppServer(options) {
22991
23390
  }
22992
23391
  function directChildHost(spawnChild, generationId = randomUUID13) {
22993
23392
  return async (options) => ({
22994
- 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 }),
22995
23394
  generation: generationId(),
22996
23395
  reattached: false,
22997
23396
  daemonPid: process.pid,
@@ -23004,6 +23403,8 @@ var init_codex_app_server_host = __esm({
23004
23403
  "use strict";
23005
23404
  init_detached_daemons();
23006
23405
  init_codex_app_server_native();
23406
+ init_codex_mcp();
23407
+ init_logging();
23007
23408
  daemonEntry = () => resolveDetachedDaemonEntry(import.meta.url, "codex-app-server-daemon");
23008
23409
  daemonCodexAppServerHost = async (options) => {
23009
23410
  const existing = liveDaemonRecord(options.stateDir, options.projectId);
@@ -23033,7 +23434,7 @@ var init_codex_app_server_host = __esm({
23033
23434
  const attached = await attach2(record, timeoutMs);
23034
23435
  return { ...attached, generation: record.generation, reattached: false, daemonPid: record.daemonPid };
23035
23436
  } catch (error) {
23036
- 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`);
23037
23438
  return inProcessCodexAppServer(options);
23038
23439
  }
23039
23440
  };
@@ -23597,6 +23998,7 @@ var init_codex_app_server = __esm({
23597
23998
  init_credential_redaction();
23598
23999
  init_codex_app_server_host();
23599
24000
  init_codex_app_server_native();
24001
+ init_logging();
23600
24002
  CODEX_APP_SERVER_PROVIDER = "codex-app-server";
23601
24003
  CODEX_NATIVE_LISTEN_FLAG = "FRAY_CODEX_NATIVE_LISTEN";
23602
24004
  CODEX_APP_SERVER_SUPPORTED_VERSION = "0.146.0";
@@ -24922,7 +25324,8 @@ var init_codex_app_server = __esm({
24922
25324
  // environment; no value is copied into argv, SQLite, diagnostics, or logs.
24923
25325
  env: codexAppServerEnvironment(),
24924
25326
  clientInfo: CLIENT_INFO,
24925
- capabilities: CLIENT_CAPABILITIES
25327
+ capabilities: CLIENT_CAPABILITIES,
25328
+ frayMcp: this.options.frayMcp
24926
25329
  });
24927
25330
  const child = attachment.process;
24928
25331
  let connection;
@@ -24961,7 +25364,7 @@ var init_codex_app_server = __esm({
24961
25364
  });
24962
25365
  if (version !== aheadVersionWarned) {
24963
25366
  aheadVersionWarned = version;
24964
- console.warn(`[fray-ui] ${verdict.message}`);
25367
+ log.warn("codex", verdict.message);
24965
25368
  }
24966
25369
  }
24967
25370
  handshaking = false;
@@ -25832,17 +26235,17 @@ var init_codex_app_server = __esm({
25832
26235
  });
25833
26236
 
25834
26237
  // packages/server/src/backend/codex-quota.ts
25835
- import { join as join19 } from "node:path";
25836
- 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";
25837
26240
  import { spawn as spawn5 } from "node:child_process";
25838
26241
  function sessionsDir2(codexHome) {
25839
- return join19(codexHome, "sessions");
26242
+ return join20(codexHome, "sessions");
25840
26243
  }
25841
26244
  function newestRollouts(dir, out, budget) {
25842
26245
  if (budget.n <= 0) return;
25843
26246
  let entries;
25844
26247
  try {
25845
- entries = readdirSync5(dir, { withFileTypes: true });
26248
+ entries = readdirSync6(dir, { withFileTypes: true });
25846
26249
  } catch {
25847
26250
  return;
25848
26251
  }
@@ -25850,18 +26253,18 @@ function newestRollouts(dir, out, budget) {
25850
26253
  const files = entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).map((e) => e.name).sort(descByName2);
25851
26254
  for (const d of dirs) {
25852
26255
  if (budget.n <= 0) return;
25853
- newestRollouts(join19(dir, d), out, budget);
26256
+ newestRollouts(join20(dir, d), out, budget);
25854
26257
  }
25855
26258
  for (const f of files) {
25856
26259
  if (budget.n <= 0) return;
25857
- out.push(join19(dir, f));
26260
+ out.push(join20(dir, f));
25858
26261
  budget.n--;
25859
26262
  }
25860
26263
  }
25861
26264
  function readTail(path) {
25862
26265
  let fd;
25863
26266
  try {
25864
- fd = openSync9(path, "r");
26267
+ fd = openSync10(path, "r");
25865
26268
  const size = fstatSync2(fd).size;
25866
26269
  const start = Math.max(0, size - TAIL_BYTES);
25867
26270
  const len = size - start;
@@ -25926,7 +26329,7 @@ function readCodexQuotaFromRollouts(codexHome = defaultCodexHome()) {
25926
26329
  newestRollouts(sessionsDir2(codexHome), rollouts, { n: 8 });
25927
26330
  const byMtime = rollouts.map((path) => {
25928
26331
  try {
25929
- return { path, mtimeMs: statSync11(path).mtimeMs };
26332
+ return { path, mtimeMs: statSync12(path).mtimeMs };
25930
26333
  } catch {
25931
26334
  return void 0;
25932
26335
  }
@@ -26102,7 +26505,7 @@ async function readQuota(opts = {}) {
26102
26505
  if (fx && process.env.NODE_ENV !== "production") {
26103
26506
  const snap = await fixture(fx);
26104
26507
  if (snap) {
26105
- 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)`);
26106
26509
  return snap;
26107
26510
  }
26108
26511
  }
@@ -26117,6 +26520,7 @@ var init_quota = __esm({
26117
26520
  "use strict";
26118
26521
  init_codex_quota();
26119
26522
  init_claude_quota();
26523
+ init_logging();
26120
26524
  }
26121
26525
  });
26122
26526
 
@@ -26126,20 +26530,20 @@ import {
26126
26530
  constants as constants2,
26127
26531
  fstatSync as fstatSync3,
26128
26532
  lstatSync as lstatSync4,
26129
- openSync as openSync10,
26533
+ openSync as openSync11,
26130
26534
  readFileSync as readFileSync14,
26131
- readdirSync as readdirSync6,
26535
+ readdirSync as readdirSync7,
26132
26536
  realpathSync as realpathSync6,
26133
26537
  unlinkSync as unlinkSync4
26134
26538
  } from "node:fs";
26135
26539
  import { createHash as createHash11 } from "node:crypto";
26136
- 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";
26137
26541
  function sameStat(a, b) {
26138
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;
26139
26543
  }
26140
26544
  function directDirectory(parentRealPath, name) {
26141
26545
  try {
26142
- const path = join20(parentRealPath, name);
26546
+ const path = join21(parentRealPath, name);
26143
26547
  const stat2 = lstatSync4(path);
26144
26548
  if (!stat2.isDirectory() || stat2.isSymbolicLink()) return null;
26145
26549
  const realPath = realpathSync6(path);
@@ -26177,7 +26581,7 @@ function resolvePlanFile(projectDir, value, hooks = {}) {
26177
26581
  const directoryBefore = planDirectory(projectDir);
26178
26582
  if (!directoryBefore) return null;
26179
26583
  hooks.afterDirectoryCheck?.();
26180
- const path = join20(directoryBefore.plans.realPath, filename);
26584
+ const path = join21(directoryBefore.plans.realPath, filename);
26181
26585
  const before = lstatSync4(path);
26182
26586
  if (!before.isFile() || before.isSymbolicLink()) return null;
26183
26587
  const realPath = realpathSync6(path);
@@ -26186,7 +26590,7 @@ function resolvePlanFile(projectDir, value, hooks = {}) {
26186
26590
  let contents;
26187
26591
  let openedBefore;
26188
26592
  let openedAfter;
26189
- const fd = openSync10(path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
26593
+ const fd = openSync11(path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
26190
26594
  try {
26191
26595
  openedBefore = fstatSync3(fd);
26192
26596
  contents = readFileSync14(fd);
@@ -26231,7 +26635,7 @@ function listPlanFiles(projectDir, hooks = {}) {
26231
26635
  if (!directoryBefore) return [];
26232
26636
  try {
26233
26637
  hooks.afterDirectoryCheck?.();
26234
- const names = readdirSync6(directoryBefore.plans.realPath).sort();
26638
+ const names = readdirSync7(directoryBefore.plans.realPath).sort();
26235
26639
  const files = names.flatMap((filename) => {
26236
26640
  const relativePath = `.fray/plans/${filename}`;
26237
26641
  if (!filenameFromPlanPath(relativePath)) return [];
@@ -26256,7 +26660,7 @@ import {
26256
26660
  existsSync as existsSync6,
26257
26661
  watch as fsWatch
26258
26662
  } from "node:fs";
26259
- import { join as join21 } from "node:path";
26663
+ import { join as join22 } from "node:path";
26260
26664
  import watcher from "@parcel/watcher";
26261
26665
  function appServerTurnStalled(liveness, lastActivityAt, nowMs) {
26262
26666
  if (!liveness) return false;
@@ -26357,7 +26761,7 @@ function deriveAwaitingBackground(row, tele, runtime, hasActionableInteraction =
26357
26761
  }
26358
26762
  function scratchpadPathIfExists(projectDir, sessionId) {
26359
26763
  const rel = scratchpadRelPath(sessionId);
26360
- return existsSync6(join21(projectDir, rel)) ? rel : void 0;
26764
+ return existsSync6(join22(projectDir, rel)) ? rel : void 0;
26361
26765
  }
26362
26766
  function resolveSessionProfile(row, tele) {
26363
26767
  const persistedModel = row.model?.trim() || void 0;
@@ -26716,7 +27120,7 @@ function createBoard(project, storage, bus, tailer, bootId, deps = {}) {
26716
27120
  if (parcelSub || stopped) return Promise.resolve();
26717
27121
  if (watchSetup) return watchSetup;
26718
27122
  const setup = (async () => {
26719
- const next = await subscribe(join21(project.dir, ".fray"), () => scheduleRebuild());
27123
+ const next = await subscribe(join22(project.dir, ".fray"), () => scheduleRebuild());
26720
27124
  if (stopped) {
26721
27125
  await next.unsubscribe();
26722
27126
  return;
@@ -27514,7 +27918,7 @@ function createScheduler(deps) {
27514
27918
  const autoResumeOnLimit = deps.autoResumeOnLimit ?? (() => true);
27515
27919
  const fetchPr = deps.fetchPr ?? defaultFetchPr;
27516
27920
  const fetchGithubReview = deps.fetchGithubReview ?? createGithubReviewFetcher({ now });
27517
- const log = deps.log ?? ((m) => console.log(`[fray-ui] ${m}`));
27921
+ const log2 = deps.log ?? ((m) => log.info("scheduler", m));
27518
27922
  const tickMs = deps.tickMs ?? 1e4;
27519
27923
  const pollMs = deps.pollMs ?? 6e4;
27520
27924
  const deliveryLeaseMs = Math.max(1, deps.deliveryLeaseMs ?? 3e4);
@@ -27744,14 +28148,14 @@ function createScheduler(deps) {
27744
28148
  return;
27745
28149
  }
27746
28150
  const suppressed = prior?.suppressed ? ` (${prior.suppressed} identical repeats suppressed)` : "";
27747
- 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}`);
27748
28152
  reviewFailures.set(key, { signature, loggedAt: at, suppressed: 0 });
27749
28153
  }
27750
28154
  function recordReviewSuccess(key, slug) {
27751
28155
  const prior = reviewFailures.get(key);
27752
28156
  if (!prior) return;
27753
28157
  const suppressed = prior.suppressed ? `; ${prior.suppressed} identical repeats were suppressed` : "";
27754
- log(`waker: GitHub review check recovered for ${key} (${slug})${suppressed}`);
28158
+ log2(`waker: GitHub review check recovered for ${key} (${slug})${suppressed}`);
27755
28159
  reviewFailures.delete(key);
27756
28160
  }
27757
28161
  async function evalThread(slug, sessionId, fence, nowMs, fenceAt) {
@@ -27799,9 +28203,9 @@ function createScheduler(deps) {
27799
28203
  try {
27800
28204
  const s = await fetchPr(ref);
27801
28205
  if (s) st.prCache.set(k, s);
27802
- 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`);
27803
28207
  } catch (err) {
27804
- 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)}`);
27805
28209
  }
27806
28210
  }),
27807
28211
  ...[...reviewRefs].map(async ([k, ref]) => {
@@ -27846,7 +28250,7 @@ function createScheduler(deps) {
27846
28250
  }, nowMs).delivery;
27847
28251
  st.fired = true;
27848
28252
  if (isPrWatchHint(h.kind)) deps.storage.setSnoozedUntil(slug, null);
27849
- log(`waker: queued ${slug} \u2014 ${verdict.reason}`);
28253
+ log2(`waker: queued ${slug} \u2014 ${verdict.reason}`);
27850
28254
  checkpoint("after-enqueue", item);
27851
28255
  return;
27852
28256
  }
@@ -27897,7 +28301,7 @@ function createScheduler(deps) {
27897
28301
  try {
27898
28302
  quota = await deps.readQuota();
27899
28303
  } catch (err) {
27900
- 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)}`);
27901
28305
  }
27902
28306
  }
27903
28307
  for (const c of candidates) {
@@ -27914,7 +28318,7 @@ function createScheduler(deps) {
27914
28318
  message: limitResumeSteer(c.fault.window),
27915
28319
  reason: `${c.fault.window} usage limit reset (interrupted ${c.fault.at})`
27916
28320
  }, nowMs).delivery;
27917
- log(`waker: queued ${c.slug} \u2014 ${item.reason}`);
28321
+ log2(`waker: queued ${c.slug} \u2014 ${item.reason}`);
27918
28322
  checkpoint("after-enqueue", item);
27919
28323
  }
27920
28324
  }
@@ -27937,7 +28341,7 @@ function createScheduler(deps) {
27937
28341
  message: relayMessage(report),
27938
28342
  reason: `sub-agent report never reached the model (${report.summary ?? report.taskId})`
27939
28343
  }, nowMs).delivery;
27940
- log(`waker: queued ${row.slug} \u2014 ${item.reason}`);
28344
+ log2(`waker: queued ${row.slug} \u2014 ${item.reason}`);
27941
28345
  checkpoint("after-enqueue", item);
27942
28346
  }
27943
28347
  }
@@ -27958,7 +28362,7 @@ function createScheduler(deps) {
27958
28362
  message: armed.prompt,
27959
28363
  reason: `snooze elapsed (${armed.until})`
27960
28364
  }, nowMs).delivery;
27961
- log(`waker: queued ${row.slug} \u2014 ${item.reason}`);
28365
+ log2(`waker: queued ${row.slug} \u2014 ${item.reason}`);
27962
28366
  checkpoint("after-enqueue", item);
27963
28367
  }
27964
28368
  }
@@ -27993,7 +28397,7 @@ function createScheduler(deps) {
27993
28397
  );
27994
28398
  if (recovered?.state === "exhausted") {
27995
28399
  settleSnooze(item);
27996
- 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"}`);
27997
28401
  }
27998
28402
  }
27999
28403
  }
@@ -28033,7 +28437,7 @@ function createScheduler(deps) {
28033
28437
  if (error?.terminalDelivery === true) {
28034
28438
  outbox.supersede(item.id, failedAt, message);
28035
28439
  settleSnooze(item);
28036
- log(`waker: delivery ABANDONED for ${item.slug} (terminal, no retry): ${message}`);
28440
+ log2(`waker: delivery ABANDONED for ${item.slug} (terminal, no retry): ${message}`);
28037
28441
  continue;
28038
28442
  }
28039
28443
  outbox.deferFailure(
@@ -28043,14 +28447,14 @@ function createScheduler(deps) {
28043
28447
  failedAt + Math.max(deliveryLeaseMs, retryDelay(item.attempts)),
28044
28448
  message
28045
28449
  );
28046
- 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}`);
28047
28451
  continue;
28048
28452
  }
28049
28453
  const retried = item.attempts > 1 ? ` (on attempt ${item.attempts})` : "";
28050
- log(`waker: delivered ${item.slug} \u2014 ${item.reason}${retried}`);
28454
+ log2(`waker: delivered ${item.slug} \u2014 ${item.reason}${retried}`);
28051
28455
  checkpoint("after-delivery", item);
28052
28456
  if (!outbox.acknowledge(item.id, deliveryOwner, now())) {
28053
- 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`);
28054
28458
  continue;
28055
28459
  }
28056
28460
  settleSnooze(item);
@@ -28076,7 +28480,7 @@ function createScheduler(deps) {
28076
28480
  await evalThread(row.slug, row.session_id, fence, nowMs, fenceAt);
28077
28481
  } catch (err) {
28078
28482
  if (err instanceof InjectedSchedulerCrash) throw err;
28079
- 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)}`);
28080
28484
  }
28081
28485
  }));
28082
28486
  const crashed = candidateResults.find((result) => result.status === "rejected");
@@ -28093,19 +28497,19 @@ function createScheduler(deps) {
28093
28497
  await evalLimits(now());
28094
28498
  } catch (err) {
28095
28499
  if (err instanceof InjectedSchedulerCrash) throw err;
28096
- 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)}`);
28097
28501
  }
28098
28502
  try {
28099
28503
  evalSnoozes(now());
28100
28504
  } catch (err) {
28101
28505
  if (err instanceof InjectedSchedulerCrash) throw err;
28102
- 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)}`);
28103
28507
  }
28104
28508
  try {
28105
28509
  repairDroppedReports(now());
28106
28510
  } catch (err) {
28107
28511
  if (err instanceof InjectedSchedulerCrash) throw err;
28108
- 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)}`);
28109
28513
  }
28110
28514
  reconcileOutbox(now());
28111
28515
  await deliverDue();
@@ -28129,9 +28533,9 @@ function createScheduler(deps) {
28129
28533
  start() {
28130
28534
  if (timer) return;
28131
28535
  if (stopped) throw new ProducerStoppedError("wake scheduler");
28132
- 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)}`));
28133
28537
  timer = setInterval(() => {
28134
- 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)}`));
28135
28539
  }, tickMs);
28136
28540
  timer.unref?.();
28137
28541
  },
@@ -28156,6 +28560,7 @@ var init_scheduler = __esm({
28156
28560
  init_completion_relay();
28157
28561
  init_github_review();
28158
28562
  init_github_review();
28563
+ init_logging();
28159
28564
  execFileAsync5 = promisify6(execFile6);
28160
28565
  PR_REF_RE = /(?:https?:\/\/github\.com\/)?([A-Za-z0-9][\w.-]*)\/([A-Za-z0-9][\w.-]*?)(?:\/pull\/|\/pulls\/|#)(\d+)/;
28161
28566
  LIMIT_FENCE_PREFIX = "limit";
@@ -28175,7 +28580,7 @@ var init_scheduler = __esm({
28175
28580
  });
28176
28581
 
28177
28582
  // packages/server/src/resume.ts
28178
- import { writeFileSync as writeFileSync7 } from "node:fs";
28583
+ import { writeFileSync as writeFileSync8 } from "node:fs";
28179
28584
  import { randomUUID as randomUUID16 } from "node:crypto";
28180
28585
  import { setTimeout as delay2 } from "node:timers/promises";
28181
28586
  function permissionModeForRow(row, settings) {
@@ -28216,7 +28621,7 @@ function spawnPinnedSession(deps, tx, row, permissionMode, message, options, lau
28216
28621
  env: claudeWorkerEnvironment(),
28217
28622
  prewrite: []
28218
28623
  };
28219
- for (const f of built.prewrite) writeFileSync7(f.path, f.contents);
28624
+ for (const f of built.prewrite) writeFileSync8(f.path, f.contents);
28220
28625
  return tx.spawn(row.slug, built.argv, deps.project.dir, {
28221
28626
  ...built.env,
28222
28627
  FRAY_UI_THREAD: row.slug,
@@ -29235,7 +29640,7 @@ var init_resume = __esm({
29235
29640
  });
29236
29641
 
29237
29642
  // packages/server/src/backend/claude.ts
29238
- import { join as join22 } from "node:path";
29643
+ import { join as join23 } from "node:path";
29239
29644
  function toolResultText2(content) {
29240
29645
  if (typeof content === "string") return content;
29241
29646
  if (!Array.isArray(content)) return "";
@@ -29324,7 +29729,7 @@ function createClaudeBackend(opts) {
29324
29729
  return { argv, env: claudeWorkerEnvironment(), prewrite: [] };
29325
29730
  },
29326
29731
  transcriptPath(sessionId) {
29327
- return join22(opts.logDir, `${sessionId}.jsonl`);
29732
+ return join23(opts.logDir, `${sessionId}.jsonl`);
29328
29733
  },
29329
29734
  parseLine(line) {
29330
29735
  return parseClaudeLine(line);
@@ -30295,27 +30700,27 @@ var init_profile_controller = __esm({
30295
30700
  });
30296
30701
 
30297
30702
  // packages/server/src/backend/codex-app-server-diagnostics.ts
30298
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync11, renameSync as renameSync6, statSync as statSync12 } from "node:fs";
30299
- 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";
30300
30705
  function codexDiagnosticLogPath(stateDir, projectId) {
30301
- return join23(stateDir, "codex-app-server", `${projectId}.diagnostics.log`);
30706
+ return join24(stateDir, "codex-app-server", `${projectId}.diagnostics.log`);
30302
30707
  }
30303
30708
  function createCodexDiagnosticSink(stateDir, projectId, now = () => /* @__PURE__ */ new Date()) {
30304
- const dir = join23(stateDir, "codex-app-server");
30709
+ const dir = join24(stateDir, "codex-app-server");
30305
30710
  const path = codexDiagnosticLogPath(stateDir, projectId);
30306
30711
  let ensured = false;
30307
30712
  return (event) => {
30308
30713
  if (event.event === "stderr") return;
30309
30714
  try {
30310
30715
  if (!ensured) {
30311
- mkdirSync11(dir, { recursive: true });
30716
+ mkdirSync12(dir, { recursive: true });
30312
30717
  ensured = true;
30313
30718
  }
30314
30719
  try {
30315
- if (statSync12(path).size > MAX_BYTES2) renameSync6(path, `${path}.1`);
30720
+ if (statSync13(path).size > MAX_BYTES2) renameSync6(path, `${path}.1`);
30316
30721
  } catch {
30317
30722
  }
30318
- appendFileSync2(path, `${JSON.stringify({ at: now().toISOString(), ...event })}
30723
+ appendFileSync3(path, `${JSON.stringify({ at: now().toISOString(), ...event })}
30319
30724
  `);
30320
30725
  } catch {
30321
30726
  }
@@ -30331,15 +30736,15 @@ var init_codex_app_server_diagnostics = __esm({
30331
30736
 
30332
30737
  // packages/server/src/backend/stale-socket-sweep.ts
30333
30738
  import { execFile as execFile8 } from "node:child_process";
30334
- import { readdirSync as readdirSync7, unlinkSync as unlinkSync5 } from "node:fs";
30739
+ import { readdirSync as readdirSync8, unlinkSync as unlinkSync5 } from "node:fs";
30335
30740
  import { connect as connectSocket } from "node:net";
30336
- import { join as join24 } from "node:path";
30741
+ import { join as join25 } from "node:path";
30337
30742
  function sweepStaleSockets(options, deps = {}) {
30338
30743
  if (process.platform === "win32") return;
30339
30744
  const keep = new Set(options.keep ?? []);
30340
30745
  let candidates;
30341
30746
  try {
30342
- 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));
30343
30748
  } catch {
30344
30749
  return;
30345
30750
  }
@@ -30652,7 +31057,7 @@ var init_orphan_reaper = __esm({
30652
31057
  });
30653
31058
 
30654
31059
  // packages/server/src/context.ts
30655
- import { join as join25 } from "node:path";
31060
+ import { join as join26 } from "node:path";
30656
31061
  import { randomUUID as randomUUID17 } from "node:crypto";
30657
31062
  function reconcileSessions(storage) {
30658
31063
  for (const row of storage.allSessions()) {
@@ -30805,7 +31210,7 @@ function createContextUnchecked(opts, resources) {
30805
31210
  projectId: project.id,
30806
31211
  projectDir: project.dir
30807
31212
  }, project.tmuxSocketManaged !== false);
30808
- const dbPath = join25(project.stateDir, "ui.db");
31213
+ const dbPath = join26(project.stateDir, "ui.db");
30809
31214
  const storage = createStorage(dbPath);
30810
31215
  resources.storage = storage;
30811
31216
  const bus = new Bus();
@@ -30868,7 +31273,7 @@ function createContextUnchecked(opts, resources) {
30868
31273
  contextUnsubscribers.push(() => clearInterval(quotaRefreshTimer));
30869
31274
  }
30870
31275
  if (!process.env.FRAY_ORPHAN_REAPER_OFF) {
30871
- contextUnsubscribers.push(startOrphanReaper({ log: (m) => console.log(`[fray-ui] ${m}`) }));
31276
+ contextUnsubscribers.push(startOrphanReaper({ log: (m) => log.info("reaper", m) }));
30872
31277
  }
30873
31278
  opts.startup?.afterPhase?.("orphan reaper");
30874
31279
  reconcileSessions(storage);
@@ -30883,6 +31288,9 @@ function createContextUnchecked(opts, resources) {
30883
31288
  // The detached app-server daemon's socket + record live under the project state dir, so a
30884
31289
  // later fray generation can find the app-server this one left running.
30885
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),
30886
31294
  dbPath,
30887
31295
  interactions: storage.interactions,
30888
31296
  codexBin: opts.codexBin,
@@ -30952,7 +31360,7 @@ function createContextUnchecked(opts, resources) {
30952
31360
  // per death rather than a card nobody asked for.
30953
31361
  onDiagnostic: (slug, _sessionId, diagnostic) => {
30954
31362
  if (diagnostic.kind !== "lifecycle" || diagnostic.phase !== "crashed") return;
30955
- 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"}`);
30956
31364
  }
30957
31365
  }) : void 0;
30958
31366
  resources.claudeBroker = claudeBroker;
@@ -31174,6 +31582,7 @@ var init_context = __esm({
31174
31582
  init_adoption_recovery();
31175
31583
  init_orphan_reaper();
31176
31584
  init_shutdown();
31585
+ init_logging();
31177
31586
  CONTEXT_STARTUP_CLEANUP_TIMEOUT_MS = 4e3;
31178
31587
  QUOTA_REFRESH_INTERVAL_MS = 2 * 6e4;
31179
31588
  ContextStartupError = class extends Error {
@@ -33827,7 +34236,7 @@ var init_server = __esm({
33827
34236
  });
33828
34237
 
33829
34238
  // packages/server/src/repair.ts
33830
- 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";
33831
34240
  import { basename as basename6, dirname as dirname9, resolve as resolve7 } from "node:path";
33832
34241
  function quoteValue(v) {
33833
34242
  if (/^[\w./#:+-]+$/.test(v)) return v;
@@ -33858,7 +34267,7 @@ function repairThreadFile(frayDir, file) {
33858
34267
  "---",
33859
34268
  ""
33860
34269
  ].join("\n");
33861
- writeFileSync8(abs, frontmatter + content);
34270
+ writeFileSync9(abs, frontmatter + content);
33862
34271
  return { slug };
33863
34272
  }
33864
34273
  var RepairError, REPAIR_STATUS_TEXT;
@@ -34009,11 +34418,11 @@ var init_bash_background = __esm({
34009
34418
  });
34010
34419
 
34011
34420
  // packages/server/src/transcript.ts
34012
- 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";
34013
34422
  import { createHash as createHash13 } from "node:crypto";
34014
34423
  import { StringDecoder as StringDecoder4 } from "node:string_decoder";
34015
- import { join as join26 } from "node:path";
34016
- 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";
34017
34426
  function isInjectedNoise(text) {
34018
34427
  const t = text.trimStart();
34019
34428
  return NOISE_PREFIXES.some((p) => t.startsWith(p));
@@ -34028,7 +34437,7 @@ function compactionLabel(preTokens, postTokens) {
34028
34437
  return shrank ? `Context compacted \u2014 ${formatTokens(preTokens)} \u2192 ${formatTokens(postTokens)} tokens` : "Context compacted";
34029
34438
  }
34030
34439
  function compactionMessage(sourceId, at, preTokens, postTokens) {
34031
- 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 };
34032
34441
  }
34033
34442
  function normalizeNewlines(s) {
34034
34443
  return stripDeliveryMarkers(s).replace(/\r\n?/g, "\n");
@@ -34071,7 +34480,9 @@ function userProjection(text, first) {
34071
34480
  const peer = parseAgentMessage(text);
34072
34481
  if (peer) return { displayText: peer.body, peerFrom: peer.from };
34073
34482
  const displayText2 = userDisplayText(text, first);
34074
- 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 } : {} };
34075
34486
  }
34076
34487
  function pushTextPart(m, text) {
34077
34488
  const last = m.parts[m.parts.length - 1];
@@ -34134,6 +34545,7 @@ function createTranscriptFold(identityPrefix = "claude") {
34134
34545
  const backgroundShells = /* @__PURE__ */ new Map();
34135
34546
  const backgroundTaskIds = /* @__PURE__ */ new Map();
34136
34547
  const childDispatchIds = /* @__PURE__ */ new Map();
34548
+ const dispatchLabels = /* @__PURE__ */ new Map();
34137
34549
  let prevTs;
34138
34550
  let thinkingMsgId = null;
34139
34551
  const queuedPending = [];
@@ -34247,14 +34659,13 @@ function createTranscriptFold(identityPrefix = "claude") {
34247
34659
  if (entry) {
34248
34660
  resolveQueued(prompt);
34249
34661
  if (dispatchId && entry.message.peerFrom) entry.message.peerDispatchId = dispatchId;
34250
- const dispatched = dispatchId ? agentDispatches.get(dispatchId) : void 0;
34251
- const described = dispatched?.call.detail?.trim();
34662
+ const described = dispatchId ? dispatchLabels.get(dispatchId) : void 0;
34252
34663
  if (described && entry.message.peerFrom) entry.message.peerFrom = described;
34253
34664
  if (thisTs) prevTs = thisTs;
34254
34665
  lastAssistantId = null;
34255
34666
  } else {
34256
34667
  const parsed = prompt.trim() ? parseAgentMessage(prompt) : void 0;
34257
- const describedHere = (dispatchId ? agentDispatches.get(dispatchId) : void 0)?.call.detail?.trim();
34668
+ const describedHere = dispatchId ? dispatchLabels.get(dispatchId) : void 0;
34258
34669
  const from = describedHere || str(peerOrigin.from) || str(peerOrigin.name) || parsed?.from || "";
34259
34670
  const body = parsed?.body ?? (typeof peerOrigin.body === "string" ? peerOrigin.body : "");
34260
34671
  if (from && body.trim()) {
@@ -34351,9 +34762,10 @@ ${block.text}` : block.text;
34351
34762
  pushToolPart(m, call);
34352
34763
  m.tools.push(call);
34353
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());
34354
34766
  if (call.sendTo && call.sendTo !== "main") {
34355
34767
  const dispatchId = childDispatchIds.get(call.sendTo);
34356
- const described = dispatchId ? agentDispatches.get(dispatchId)?.call.detail?.trim() : void 0;
34768
+ const described = dispatchId ? dispatchLabels.get(dispatchId) : void 0;
34357
34769
  if (dispatchId) call.sendDispatchId = dispatchId;
34358
34770
  if (described) call.sendTargetLabel = described;
34359
34771
  }
@@ -34503,18 +34915,18 @@ function looksLikeImage(buf, ext) {
34503
34915
  }
34504
34916
  function pruneScreenshotCache() {
34505
34917
  try {
34506
- const entries = readdirSync8(SCREENSHOT_CACHE_DIR).filter((n) => !n.startsWith("."));
34918
+ const entries = readdirSync9(SCREENSHOT_CACHE_DIR).filter((n) => !n.startsWith("."));
34507
34919
  if (entries.length <= SCREENSHOT_CACHE_MAX) return;
34508
34920
  const byMtime = entries.map((n) => {
34509
34921
  try {
34510
- return { n, m: statSync13(join26(SCREENSHOT_CACHE_DIR, n)).mtimeMs };
34922
+ return { n, m: statSync14(join27(SCREENSHOT_CACHE_DIR, n)).mtimeMs };
34511
34923
  } catch {
34512
34924
  return { n, m: 0 };
34513
34925
  }
34514
34926
  }).sort((a, b) => b.m - a.m);
34515
34927
  for (const { n } of byMtime.slice(SCREENSHOT_CACHE_MAX)) {
34516
34928
  try {
34517
- unlinkSync6(join26(SCREENSHOT_CACHE_DIR, n));
34929
+ unlinkSync6(join27(SCREENSHOT_CACHE_DIR, n));
34518
34930
  } catch {
34519
34931
  }
34520
34932
  }
@@ -34544,15 +34956,15 @@ function persistBase64Image(mediaType, data, idKey) {
34544
34956
  const ext = IMAGE_MEDIA_EXT[typeof mediaType === "string" ? mediaType.toLowerCase() : ""];
34545
34957
  if (!ext || !data) return void 0;
34546
34958
  const name = createHash13("sha256").update(idKey).digest("hex").slice(0, 32);
34547
- const path = join26(SCREENSHOT_CACHE_DIR, `${name}.${ext}`);
34959
+ const path = join27(SCREENSHOT_CACHE_DIR, `${name}.${ext}`);
34548
34960
  try {
34549
34961
  if (existsSync8(path)) return path;
34550
34962
  if (data.length > SCREENSHOT_MAX_BASE64) return void 0;
34551
34963
  const buf = Buffer.from(data, "base64");
34552
34964
  if (buf.length === 0 || !looksLikeImage(buf, ext)) return void 0;
34553
- mkdirSync12(SCREENSHOT_CACHE_DIR, { recursive: true });
34554
- const tmp = join26(SCREENSHOT_CACHE_DIR, `.${name}.${process.pid}.${screenshotTmpSeq++}.tmp`);
34555
- 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);
34556
34968
  renameSync7(tmp, path);
34557
34969
  pruneScreenshotCache();
34558
34970
  return path;
@@ -34566,15 +34978,15 @@ function persistSentFile(srcPath, idKey) {
34566
34978
  const outExt = ext === "jpeg" ? "jpg" : ext;
34567
34979
  try {
34568
34980
  const name = createHash13("sha256").update(idKey).digest("hex").slice(0, 32);
34569
- const dest = join26(SCREENSHOT_CACHE_DIR, `${name}.${outExt}`);
34981
+ const dest = join27(SCREENSHOT_CACHE_DIR, `${name}.${outExt}`);
34570
34982
  if (existsSync8(dest)) return dest;
34571
- const size = statSync13(srcPath).size;
34983
+ const size = statSync14(srcPath).size;
34572
34984
  if (size === 0 || size > SENT_IMAGE_MAX_BYTES) return void 0;
34573
34985
  const buf = readFileSync17(srcPath);
34574
34986
  if (!looksLikeImage(buf, outExt)) return void 0;
34575
- mkdirSync12(SCREENSHOT_CACHE_DIR, { recursive: true });
34576
- const tmp = join26(SCREENSHOT_CACHE_DIR, `.${name}.${process.pid}.${screenshotTmpSeq++}.tmp`);
34577
- 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);
34578
34990
  renameSync7(tmp, dest);
34579
34991
  pruneScreenshotCache();
34580
34992
  return dest;
@@ -34874,7 +35286,7 @@ function completionEvents(rec, dispatches, backgroundShells, backgroundTaskIds)
34874
35286
  const elapsedMs2 = elapsedBetween(shell.at, rec.timestamp);
34875
35287
  shell.call.status = status === "completed" ? "completed" : status === "killed" ? "cancelled" : "failed";
34876
35288
  if (elapsedMs2 !== void 0) shell.call.durationMs = elapsedMs2;
34877
- 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 });
34878
35290
  continue;
34879
35291
  }
34880
35292
  dispatches.delete(id);
@@ -34922,11 +35334,11 @@ function retainedFoldEntry(path, identityPrefix, fileId, size) {
34922
35334
  return { entry, hit };
34923
35335
  }
34924
35336
  function readTranscript(project, sessionId) {
34925
- const path = join26(homedir7(), ".claude", "projects", project.cwdSlug, `${sessionId}.jsonl`);
35337
+ const path = join27(homedir9(), ".claude", "projects", project.cwdSlug, `${sessionId}.jsonl`);
34926
35338
  const identityPrefix = `claude:${sessionId}`;
34927
35339
  let fd;
34928
35340
  try {
34929
- fd = openSync11(path, "r");
35341
+ fd = openSync12(path, "r");
34930
35342
  const st = fstatSync4(fd);
34931
35343
  const size = st.size;
34932
35344
  const fileId = `${st.dev}:${st.ino}:${Math.trunc(st.birthtimeMs)}`;
@@ -34970,7 +35382,7 @@ function verifyIncrementalParse(path, identityPrefix, incremental) {
34970
35382
  }
34971
35383
  }
34972
35384
  function logDirOf(project) {
34973
- return join26(homedir7(), ".claude", "projects", project.cwdSlug);
35385
+ return join27(homedir9(), ".claude", "projects", project.cwdSlug);
34974
35386
  }
34975
35387
  function projectCodexTranscript(raw2, identityPrefix = "codex") {
34976
35388
  const out = [];
@@ -35876,7 +36288,7 @@ function sourceForThread(project, storage, slug, backendFor) {
35876
36288
  if (row) {
35877
36289
  const backend = row.backend === "codex" ? "codex" : "claude";
35878
36290
  const nativeId = backend === "codex" ? row.agent_session_id ?? row.session_id : row.transcript_id ?? row.session_id;
35879
- 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`);
35880
36292
  if (!path) return void 0;
35881
36293
  return {
35882
36294
  slug,
@@ -35894,7 +36306,7 @@ function sourceForThread(project, storage, slug, backendFor) {
35894
36306
  nativeId: slug,
35895
36307
  backend: "claude",
35896
36308
  runtimeGeneration: 0,
35897
- path: join26(logDirOf(project), `${slug}.jsonl`)
36309
+ path: join27(logDirOf(project), `${slug}.jsonl`)
35898
36310
  };
35899
36311
  }
35900
36312
  function discoveredClaudeSource(project, storage, slug, expectedNativeId) {
@@ -35915,13 +36327,13 @@ function discoveredClaudeSource(project, storage, slug, expectedNativeId) {
35915
36327
  nativeId,
35916
36328
  backend: "claude",
35917
36329
  runtimeGeneration: row.runtime_generation ?? 0,
35918
- path: join26(logDirOf(project), `${nativeId}.jsonl`)
36330
+ path: join27(logDirOf(project), `${nativeId}.jsonl`)
35919
36331
  };
35920
36332
  }
35921
36333
  function fixedSnapshot(source) {
35922
36334
  let fd;
35923
36335
  try {
35924
- fd = openSync11(source.path, "r");
36336
+ fd = openSync12(source.path, "r");
35925
36337
  const before = fstatSync4(fd);
35926
36338
  if (!Number.isSafeInteger(before.size) || before.size < 0) throw new Error("transcript is too large to page safely");
35927
36339
  const bytes = Buffer.allocUnsafe(before.size);
@@ -36220,7 +36632,7 @@ var init_transcript = __esm({
36220
36632
  "image/gif": "gif",
36221
36633
  "image/webp": "webp"
36222
36634
  };
36223
- SCREENSHOT_CACHE_DIR = join26(tmpdir4(), "fray-tool-images");
36635
+ SCREENSHOT_CACHE_DIR = join27(tmpdir4(), "fray-tool-images");
36224
36636
  SCREENSHOT_CACHE_MAX = 200;
36225
36637
  SCREENSHOT_MAX_BASE64 = 32e6;
36226
36638
  screenshotTmpSeq = 0;
@@ -36270,9 +36682,9 @@ var init_open_external = __esm({
36270
36682
 
36271
36683
  // packages/server/src/local-file.ts
36272
36684
  import { spawn as spawn7 } from "node:child_process";
36273
- import { realpathSync as realpathSync7, statSync as statSync14 } from "node:fs";
36274
- import { homedir as homedir8 } from "node:os";
36275
- 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";
36276
36688
  function isUnder(real, root) {
36277
36689
  let rootReal;
36278
36690
  try {
@@ -36292,17 +36704,17 @@ function resolveLocalFile(rawPath, roots) {
36292
36704
  }
36293
36705
  if (!roots.some((root) => isUnder(real, root))) throw new Error("Local file is outside Fray's trusted roots");
36294
36706
  try {
36295
- 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");
36296
36708
  } catch (error) {
36297
36709
  if (error instanceof Error && error.message === "Local path is not a regular file") throw error;
36298
36710
  throw new Error("Local file was not found");
36299
36711
  }
36300
36712
  return real;
36301
36713
  }
36302
- function resolveOpenableFile(raw2, projectDir, roots, home = homedir8()) {
36714
+ function resolveOpenableFile(raw2, projectDir, roots, home = homedir10()) {
36303
36715
  const trimmed = raw2.trim().replace(/:\d+(?::\d+)?$/, "");
36304
36716
  if (!trimmed) return null;
36305
- 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);
36306
36718
  try {
36307
36719
  return resolveLocalFile(abs, roots);
36308
36720
  } catch {
@@ -36646,11 +37058,11 @@ var init_external_terminal = __esm({
36646
37058
  });
36647
37059
 
36648
37060
  // packages/server/src/background-shell-output.ts
36649
- 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";
36650
37062
  function readBackgroundShellOutput(path, maxBytes = OUTPUT_TAIL_BYTES) {
36651
37063
  let fd;
36652
37064
  try {
36653
- fd = openSync12(path, "r");
37065
+ fd = openSync13(path, "r");
36654
37066
  const size = fstatSync5(fd).size;
36655
37067
  const length = Math.min(size, maxBytes);
36656
37068
  const offset = Math.max(0, size - length);
@@ -36680,7 +37092,7 @@ var init_background_shell_output = __esm({
36680
37092
 
36681
37093
  // packages/server/src/router.ts
36682
37094
  import { readFileSync as readFileSync18 } from "node:fs";
36683
- import { join as join28 } from "node:path";
37095
+ import { join as join29 } from "node:path";
36684
37096
  function validateGithubDispatchProfile(input) {
36685
37097
  validateThreadProfile(input.backend, input.model, input.effort);
36686
37098
  }
@@ -36821,7 +37233,7 @@ async function stopAndForgetRegisteredRuntime(storage, row, runtime = tmux_expor
36821
37233
  return forgotten;
36822
37234
  }
36823
37235
  function createRouter(ctx) {
36824
- const frayDir = join28(ctx.project.dir, ".fray");
37236
+ const frayDir = join29(ctx.project.dir, ".fray");
36825
37237
  const openRoots = openableFileRoots(ctx.project);
36826
37238
  const claudeRename = createClaudeRenameController({ storage: ctx.storage, tailer: ctx.tailer, board: ctx.board });
36827
37239
  function isAutoTitledSession(slug) {
@@ -37159,6 +37571,14 @@ function createRouter(ctx) {
37159
37571
  if (hasPendingPermissionChange(row)) {
37160
37572
  throw new Error("Wait for the current permission change to finish before sending a follow-up");
37161
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
+ }
37162
37582
  if (row?.backend === "codex") {
37163
37583
  const bridge = ctx.codexAppServer;
37164
37584
  if (!bridge) throw new Error("Codex app-server is unavailable; cannot deliver this follow-up");
@@ -37220,7 +37640,12 @@ function createRouter(ctx) {
37220
37640
  // needs the same treatment: while the process is still latched on its own 429, delivering
37221
37641
  // into it does nothing at all. Restart it instead — otherwise the button is a no-op and
37222
37642
  // reads as fray having ignored the click.
37223
- 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(
37224
37649
  ctx.tailer.get(input.slug)?.limitFault,
37225
37650
  Date.now(),
37226
37651
  (ctx.tailer.get(input.slug)?.subAgents ?? []).some((agent) => agent.state === "running")
@@ -37567,7 +37992,7 @@ function createRouter(ctx) {
37567
37992
  const row = ctx.storage.getSession(input.slug);
37568
37993
  if (!row) return { markdown: "" };
37569
37994
  try {
37570
- 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") };
37571
37996
  } catch {
37572
37997
  return { markdown: "" };
37573
37998
  }
@@ -38054,7 +38479,7 @@ var init_local_origin = __esm({
38054
38479
  });
38055
38480
 
38056
38481
  // packages/server/src/local-image.ts
38057
- 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";
38058
38483
  import { extname, isAbsolute as isAbsolute6 } from "node:path";
38059
38484
  function resolveLocalImage(rawPath) {
38060
38485
  if (!rawPath || !isAbsolute6(rawPath)) return { status: 400 };
@@ -38067,7 +38492,7 @@ function resolveLocalImage(rawPath) {
38067
38492
  return { status: 404 };
38068
38493
  }
38069
38494
  try {
38070
- if (!statSync15(real).isFile()) return { status: 404 };
38495
+ if (!statSync16(real).isFile()) return { status: 404 };
38071
38496
  return { status: 200, contentType, body: readFileSync19(real) };
38072
38497
  } catch {
38073
38498
  return { status: 404 };
@@ -38088,11 +38513,11 @@ var init_local_image = __esm({
38088
38513
  });
38089
38514
 
38090
38515
  // packages/server/src/local-visualization.ts
38091
- import { readdirSync as readdirSync9, readFileSync as readFileSync20, realpathSync as realpathSync9, statSync as statSync16 } from "node:fs";
38092
- 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";
38093
38518
  function children(path, pattern) {
38094
38519
  try {
38095
- 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();
38096
38521
  } catch {
38097
38522
  return [];
38098
38523
  }
@@ -38101,7 +38526,7 @@ function isUnder2(path, root) {
38101
38526
  return path === root || path.startsWith(root.endsWith(sep3) ? root : root + sep3);
38102
38527
  }
38103
38528
  function resolveFragment(projectDir, sessionId, file) {
38104
- const base = join29(projectDir, ".codex", "visualizations");
38529
+ const base = join30(projectDir, ".codex", "visualizations");
38105
38530
  let projectReal;
38106
38531
  let baseReal;
38107
38532
  try {
@@ -38112,15 +38537,15 @@ function resolveFragment(projectDir, sessionId, file) {
38112
38537
  }
38113
38538
  if (!isUnder2(baseReal, projectReal)) return null;
38114
38539
  for (const year of children(base, YEAR_PART)) {
38115
- const yearDir = join29(base, year);
38540
+ const yearDir = join30(base, year);
38116
38541
  for (const month of children(yearDir, DATE_PART)) {
38117
- const monthDir = join29(yearDir, month);
38542
+ const monthDir = join30(yearDir, month);
38118
38543
  for (const day of children(monthDir, DATE_PART)) {
38119
- const sessionRoot = join29(baseReal, year, month, day, sessionId);
38120
- const candidate = join29(monthDir, day, sessionId, file);
38544
+ const sessionRoot = join30(baseReal, year, month, day, sessionId);
38545
+ const candidate = join30(monthDir, day, sessionId, file);
38121
38546
  try {
38122
38547
  const real = realpathSync9(candidate);
38123
- if (isUnder2(real, sessionRoot) && statSync16(real).isFile()) return real;
38548
+ if (isUnder2(real, sessionRoot) && statSync17(real).isFile()) return real;
38124
38549
  } catch {
38125
38550
  }
38126
38551
  }
@@ -38201,7 +38626,7 @@ function resolveLocalVisualization(projectDir, sessionId, file) {
38201
38626
  if (!path) return { status: 404 };
38202
38627
  let fragment;
38203
38628
  try {
38204
- const size = statSync16(path).size;
38629
+ const size = statSync17(path).size;
38205
38630
  if (size > MAX_FRAGMENT_BYTES) return { status: 413 };
38206
38631
  fragment = readFileSync20(path, "utf8");
38207
38632
  } catch {
@@ -38245,8 +38670,8 @@ var init_local_visualization = __esm({
38245
38670
  });
38246
38671
 
38247
38672
  // packages/server/src/app.ts
38248
- import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync10 } from "node:fs";
38249
- 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";
38250
38675
  import { randomUUID as randomUUID18 } from "node:crypto";
38251
38676
  function createApp(ctx, options = {}) {
38252
38677
  const app = new Hono2();
@@ -38323,11 +38748,11 @@ function createApp(ctx, options = {}) {
38323
38748
  if (typeof body.data !== "string" || body.data.length > ATTACHMENT_MAX_BASE64_CHARS) return c.json({ error: "bad payload" }, 400);
38324
38749
  const ext = `.${attachmentExtension(name)}`;
38325
38750
  const buf = Buffer.from(body.data, "base64");
38326
- const dir = join30(ctx.project.stateDir, "attachments");
38327
- mkdirSync13(dir, { recursive: true });
38751
+ const dir = join31(ctx.project.stateDir, "attachments");
38752
+ mkdirSync14(dir, { recursive: true });
38328
38753
  const base = name.replace(/\.[^.]+$/, "").replace(/[^a-zA-Z0-9_-]+/g, "-").slice(0, 40) || "file";
38329
- const path = join30(dir, `${Date.now()}-${randomUUID18().slice(0, 8)}-${base}${ext}`);
38330
- writeFileSync10(path, buf);
38754
+ const path = join31(dir, `${Date.now()}-${randomUUID18().slice(0, 8)}-${base}${ext}`);
38755
+ writeFileSync11(path, buf);
38331
38756
  return c.json({ path });
38332
38757
  });
38333
38758
  app.get(
@@ -39599,10 +40024,10 @@ var init_app_socket = __esm({
39599
40024
  });
39600
40025
 
39601
40026
  // packages/server/src/boot-progress.ts
39602
- import { mkdirSync as mkdirSync14, readFileSync as readFileSync21, renameSync as renameSync8, rmSync as rmSync6, writeFileSync as writeFileSync11 } from "node:fs";
39603
- 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";
39604
40029
  function bootProgressPath(stateDir) {
39605
- return join31(stateDir, BOOT_PROGRESS_NAME);
40030
+ return join32(stateDir, BOOT_PROGRESS_NAME);
39606
40031
  }
39607
40032
  function createBootProgressPublisher(stateDir, minIntervalMs = 200) {
39608
40033
  if (!stateDir) {
@@ -39622,9 +40047,9 @@ function createBootProgressPublisher(stateDir, minIntervalMs = 200) {
39622
40047
  lastAt = now;
39623
40048
  step++;
39624
40049
  try {
39625
- mkdirSync14(dirname10(path), { recursive: true, mode: 448 });
39626
- const temp = join31(dirname10(path), `.${basename8(path)}.${process.pid}.tmp`);
39627
- 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() })}
39628
40053
  `, "utf8");
39629
40054
  renameSync8(temp, path);
39630
40055
  } catch {
@@ -39633,7 +40058,7 @@ function createBootProgressPublisher(stateDir, minIntervalMs = 200) {
39633
40058
  return Object.assign(write, {
39634
40059
  done: () => {
39635
40060
  try {
39636
- rmSync6(path, { force: true });
40061
+ rmSync7(path, { force: true });
39637
40062
  } catch {
39638
40063
  }
39639
40064
  }
@@ -39659,7 +40084,7 @@ __export(index_exports, {
39659
40084
  });
39660
40085
  import { createServer } from "node:http";
39661
40086
  import { readFileSync as readFileSync22, existsSync as existsSync9 } from "node:fs";
39662
- 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";
39663
40088
  async function pipeToApp(app, req, res, port2, controller) {
39664
40089
  const url = `http://127.0.0.1:${port2}${req.url ?? "/"}`;
39665
40090
  res.on("close", () => {
@@ -39728,9 +40153,9 @@ function createShutdownSignalHandler(options) {
39728
40153
  }
39729
40154
  function serveStatic(distDir, req, res) {
39730
40155
  const rel = normalize((req.url ?? "/").split("?")[0]).replace(/^(\.\.[/\\])+/, "");
39731
- let file = join32(distDir, rel === "/" ? "index.html" : rel);
39732
- if (!file.startsWith(distDir)) file = join32(distDir, "index.html");
39733
- 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");
39734
40159
  try {
39735
40160
  const body = readFileSync22(file);
39736
40161
  res.writeHead(200, { "content-type": MIME[extname2(file)] ?? "application/octet-stream" });
@@ -39780,8 +40205,9 @@ async function startServer(opts = {}) {
39780
40205
  diagnostics.push(event);
39781
40206
  if (opts.shutdownDiagnostic) opts.shutdownDiagnostic(event);
39782
40207
  else {
39783
- console.error(
39784
- `[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}` : ""}`
39785
40211
  );
39786
40212
  }
39787
40213
  };
@@ -40028,9 +40454,9 @@ async function startServer(opts = {}) {
40028
40454
  } else {
40029
40455
  await phase("wake scheduler", () => void 0);
40030
40456
  }
40031
- statusPath = join32(ctx.project.stateDir, "server.lock");
40457
+ statusPath = join33(ctx.project.stateDir, "server.lock");
40032
40458
  const webRoot = resolve9(import.meta.dirname, "..", "..", "web");
40033
- const distDir = opts.webDistDir ? resolve9(opts.webDistDir) : join32(webRoot, "dist");
40459
+ const distDir = opts.webDistDir ? resolve9(opts.webDistDir) : join33(webRoot, "dist");
40034
40460
  startupPhase = "Vite";
40035
40461
  if (opts.dev) {
40036
40462
  try {
@@ -40042,8 +40468,9 @@ async function startServer(opts = {}) {
40042
40468
  });
40043
40469
  } catch (error) {
40044
40470
  if (opts.requireDevWeb) throw error;
40045
- console.warn(
40046
- `[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}`
40047
40474
  );
40048
40475
  }
40049
40476
  }
@@ -40073,7 +40500,7 @@ async function startServer(opts = {}) {
40073
40500
  if (vite) {
40074
40501
  vite.middlewares(req, res, () => {
40075
40502
  try {
40076
- const html = readFileSync22(join32(webRoot, "index.html"), "utf8");
40503
+ const html = readFileSync22(join33(webRoot, "index.html"), "utf8");
40077
40504
  void vite.transformIndexHtml(url, html).then((out) => {
40078
40505
  res.writeHead(200, { "content-type": "text/html" });
40079
40506
  res.end(out);
@@ -40128,8 +40555,9 @@ async function startServer(opts = {}) {
40128
40555
  port: port2,
40129
40556
  bootId: ctx.bootId
40130
40557
  }));
40131
- console.log(
40132
- `[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}`
40133
40561
  );
40134
40562
  startupPhase = "signal handlers";
40135
40563
  if (!opts.requestOwnerStop) {
@@ -40197,6 +40625,7 @@ var init_index = __esm({
40197
40625
  init_project_launch();
40198
40626
  init_tmux();
40199
40627
  init_boot_progress();
40628
+ init_logging();
40200
40629
  SERVER_SHUTDOWN_TIMEOUT_MS = 4e3;
40201
40630
  SERVER_FORCE_EXIT_MS = 5e3;
40202
40631
  defaultStartServerRuntime = {
@@ -40251,12 +40680,20 @@ var init_index = __esm({
40251
40680
  init_project();
40252
40681
  init_project_launch();
40253
40682
  init_shutdown();
40683
+ init_logging();
40254
40684
  import { existsSync as existsSync10 } from "node:fs";
40255
- 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
+ });
40256
40693
  var rawPort = process.env.FRAY_DEV_PORT;
40257
40694
  var port = Number(rawPort);
40258
40695
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
40259
- console.error(`[fray-ui] invalid FRAY_DEV_PORT for dev child: ${rawPort ?? "<unset>"}`);
40696
+ log.error("dev-child", `invalid FRAY_DEV_PORT: ${rawPort ?? "<unset>"}`);
40260
40697
  process.exit(1);
40261
40698
  }
40262
40699
  function notifySupervisor(message) {
@@ -40285,7 +40722,7 @@ try {
40285
40722
  ];
40286
40723
  if (!existsSync10(stableWebDist)) throw new Error("stable artifact launch is missing its verified web directory");
40287
40724
  for (const [name, directory, requiredFile] of required) {
40288
- if (!directory || !existsSync10(join33(directory, requiredFile)))
40725
+ if (!directory || !existsSync10(join34(directory, requiredFile)))
40289
40726
  throw new Error(`stable artifact launch is missing verified ${name}`);
40290
40727
  }
40291
40728
  }
@@ -40316,16 +40753,16 @@ try {
40316
40753
  process.exit(0);
40317
40754
  } catch (err) {
40318
40755
  if (err instanceof ShutdownTimeoutError) {
40319
- 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`);
40320
40757
  try {
40321
40758
  await server.shutdownFence.whenSafe();
40322
40759
  process.exit(0);
40323
40760
  return;
40324
40761
  } catch (drainError) {
40325
- 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}`);
40326
40763
  }
40327
40764
  }
40328
- 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}`);
40329
40766
  process.exit(1);
40330
40767
  }
40331
40768
  };
@@ -40333,6 +40770,6 @@ try {
40333
40770
  process.on("SIGTERM", () => void shutdown());
40334
40771
  process.once("disconnect", () => void shutdown());
40335
40772
  } catch (err) {
40336
- 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}`);
40337
40774
  process.exit(1);
40338
40775
  }