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