frizz 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -4
- package/dist/claude-agent-broker.js +9 -4
- package/dist/dev-child.js +1868 -529
- package/dist/frizz.js +814 -234
- package/package.json +5 -3
- package/runtime/cc-worker/DECISIONS.md +17 -0
- package/runtime/cc-worker/bin/frizz-mcp.mjs +159 -174
- package/runtime/cc-worker/hooks/session-seed.mjs +1 -1
- package/web-dist/assets/{TerminalPane-CTdetDJJ.js → TerminalPane-WcpgmCA-.js} +1 -1
- package/web-dist/assets/{abnfDiagram-VRR7QNED-BDKrjCMs.js → abnfDiagram-VRR7QNED-Caecklbz.js} +1 -1
- package/web-dist/assets/architecture-TIHT7OUA-BTy2nvYL.js +1 -0
- package/web-dist/assets/{architectureDiagram-ZJ3FMSHR-BJebpUUM.js → architectureDiagram-ZJ3FMSHR-BEk0fg8C.js} +1 -1
- package/web-dist/assets/{blockDiagram-677ZJIJ3-BNbuk25k.js → blockDiagram-677ZJIJ3-B4yq3xiy.js} +1 -1
- package/web-dist/assets/{c4Diagram-LMCZKHZV-lCfyotdU.js → c4Diagram-LMCZKHZV-DaA5NBkz.js} +1 -1
- package/web-dist/assets/channel-WghlT4VS.js +1 -0
- package/web-dist/assets/{chunk-32BRIVSS-DP72SEkr.js → chunk-32BRIVSS-2XsSs8F_.js} +1 -1
- package/web-dist/assets/{chunk-52WLFC77-DxO3gN-p.js → chunk-52WLFC77-DWJfECP3.js} +1 -1
- package/web-dist/assets/{chunk-C7G6YPKG-BXFx9Vlr.js → chunk-C7G6YPKG-Bz07eaGt.js} +1 -1
- package/web-dist/assets/{chunk-EX3LRPZG-CR3sHpPf.js → chunk-EX3LRPZG-D78kZEMM.js} +1 -1
- package/web-dist/assets/{chunk-FWX5IMBZ-CJ8L__oG.js → chunk-FWX5IMBZ-CeCa17CW.js} +2 -2
- package/web-dist/assets/{chunk-HOUHSVGY-BCl3JSWr.js → chunk-HOUHSVGY-9H4GFuBa.js} +1 -1
- package/web-dist/assets/{chunk-ICXQ74PX-CZxFMci1.js → chunk-ICXQ74PX--Aa17r4X.js} +1 -1
- package/web-dist/assets/{chunk-MOJQB5TN-Ck3_47dB.js → chunk-MOJQB5TN-C3vXhsPV.js} +1 -1
- package/web-dist/assets/{chunk-OGEWGWER-CVXjES-F.js → chunk-OGEWGWER-G4kZRBkX.js} +1 -1
- package/web-dist/assets/{chunk-PUDLZKDR-JjrVgaR7.js → chunk-PUDLZKDR-DWd5kvyM.js} +1 -1
- package/web-dist/assets/{chunk-Q4XR5HBZ-C5YMRAQ0.js → chunk-Q4XR5HBZ-u55p39Up.js} +1 -1
- package/web-dist/assets/{chunk-V7JOEXUC-DK0mfUHU.js → chunk-V7JOEXUC-BU4YBmV4.js} +1 -1
- package/web-dist/assets/{chunk-VAUOI2AC-BKBntpid.js → chunk-VAUOI2AC-VXcmT20q.js} +1 -1
- package/web-dist/assets/{chunk-VR4S4FIN-zgE1dG6U.js → chunk-VR4S4FIN-CSjEXLOC.js} +1 -1
- package/web-dist/assets/{chunk-WYO6CB5R-CZhh1IBq.js → chunk-WYO6CB5R-4WGk2dpL.js} +1 -1
- package/web-dist/assets/{chunk-ZGVPDNZ5-CrNjIEem.js → chunk-ZGVPDNZ5-D5Xl8ZV6.js} +1 -1
- package/web-dist/assets/classDiagram-OUVF2IWQ-D21-Dy4r.js +1 -0
- package/web-dist/assets/classDiagram-v2-EOCWNBFH-D21-Dy4r.js +1 -0
- package/web-dist/assets/{cynefin-VYW2F7L2-Ca_BPfTG.js → cynefin-VYW2F7L2-O6wB49wK.js} +1 -1
- package/web-dist/assets/{cynefinDiagram-TSTJHNR4-CsINQf6g.js → cynefinDiagram-TSTJHNR4-pd6ZogNC.js} +1 -1
- package/web-dist/assets/{dagre-VKFMJZFB-CE0DYMFy.js → dagre-VKFMJZFB-D8W1S6fw.js} +1 -1
- package/web-dist/assets/{diagram-FQU43EPY-BqTduEYE.js → diagram-FQU43EPY-Bm1NOkHd.js} +1 -1
- package/web-dist/assets/{diagram-G47NLZAW-Cho718s6.js → diagram-G47NLZAW-CjwnXTIc.js} +1 -1
- package/web-dist/assets/{diagram-NH7WQ7WH-D0Z8t9UC.js → diagram-NH7WQ7WH-DH1jZ_mu.js} +1 -1
- package/web-dist/assets/{diagram-OA4YK3LP-CBacyeDx.js → diagram-OA4YK3LP-B-s969Q_.js} +1 -1
- package/web-dist/assets/{diagram-WEI45ONY-CozpXa4j.js → diagram-WEI45ONY-BU3lv6cL.js} +1 -1
- package/web-dist/assets/{ebnfDiagram-CCIWWBDH-ba4NbQQo.js → ebnfDiagram-CCIWWBDH-FsAeyqcC.js} +1 -1
- package/web-dist/assets/{erDiagram-Q63AITRT-B3BciOYa.js → erDiagram-Q63AITRT-BRvg5Y9d.js} +1 -1
- package/web-dist/assets/eventmodeling-45OFAUF4-RhX2WA7g.js +1 -0
- package/web-dist/assets/flowDiagram-23GEKE2U-C7N14tdV.js +1 -0
- package/web-dist/assets/{ganttDiagram-NO4QXBWP-lPmrRit4.js → ganttDiagram-NO4QXBWP-eATKNh9U.js} +1 -1
- package/web-dist/assets/{gitGraph-TEB2WS4Q-BjyclOq0.js → gitGraph-TEB2WS4Q-CNYqsua8.js} +1 -1
- package/web-dist/assets/{gitGraphDiagram-IHSO6WYX-pzeb4Yrw.js → gitGraphDiagram-IHSO6WYX-BUxgluf8.js} +1 -1
- package/web-dist/assets/index-BFuReKr0.js +368 -0
- package/web-dist/assets/index-BH9U3Zlg.css +1 -0
- package/web-dist/assets/{info-DKCQHKI2-Bwycegvf.js → info-DKCQHKI2-HsVLwo1c.js} +1 -1
- package/web-dist/assets/{infoDiagram-FWYZ7A6U-BCvRGj_5.js → infoDiagram-FWYZ7A6U-D7U65xDp.js} +1 -1
- package/web-dist/assets/{ishikawaDiagram-FXEZZL3T-Ofw1RMj3.js → ishikawaDiagram-FXEZZL3T-BsfIYXjX.js} +1 -1
- package/web-dist/assets/{journeyDiagram-5HDEW3XC-C5ROwFio.js → journeyDiagram-5HDEW3XC-CpkREKol.js} +1 -1
- package/web-dist/assets/{kanban-definition-HUTT4EX6-YLPLkpeT.js → kanban-definition-HUTT4EX6-2M3KW6Gw.js} +1 -1
- package/web-dist/assets/{line-KtkNqRgI.js → line-Cs7zWs24.js} +1 -1
- package/web-dist/assets/{mermaid-parser.core-D_FfqBe7.js → mermaid-parser.core-Dw-IECV5.js} +3 -3
- package/web-dist/assets/{mermaid.core-Ffv8anVf.js → mermaid.core-C1lrddjT.js} +3 -3
- package/web-dist/assets/{mindmap-definition-LN4V7U3C-B2jj4vfm.js → mindmap-definition-LN4V7U3C-gVS5On1g.js} +1 -1
- package/web-dist/assets/{packet-7NZHBO7P-BjmWHwra.js → packet-7NZHBO7P-Tti1-UWA.js} +1 -1
- package/web-dist/assets/{pegDiagram-2B236MQR-Dq3iJDyq.js → pegDiagram-2B236MQR-DE5in93G.js} +1 -1
- package/web-dist/assets/{pie-RZYD4A2V-jBbH1lv9.js → pie-RZYD4A2V-DfLNkvHl.js} +1 -1
- package/web-dist/assets/{pieDiagram-ENE6RG2P-C3ETW9lq.js → pieDiagram-ENE6RG2P-DcqSqJqb.js} +1 -1
- package/web-dist/assets/{quadrantDiagram-ABIIQ3AL-DItSmme7.js → quadrantDiagram-ABIIQ3AL-DR-nRPgN.js} +1 -1
- package/web-dist/assets/{radar-I7S5WNFK-6ey6crgP.js → radar-I7S5WNFK-DGLDBfPd.js} +1 -1
- package/web-dist/assets/{railroad-3IZDKUUU-Cii-Mn0E.js → railroad-3IZDKUUU-BaiGEHlV.js} +1 -1
- package/web-dist/assets/railroad-abnf-AHOZXSZD-BCeoIOWA.js +1 -0
- package/web-dist/assets/railroad-ebnf-EBAXGLYW-lZpXJLD8.js +1 -0
- package/web-dist/assets/railroad-peg-LSFZ7HO6-CaUBrHTL.js +1 -0
- package/web-dist/assets/{railroadDiagram-RFXS5EU6-M363ils_.js → railroadDiagram-RFXS5EU6-82aEev5_.js} +1 -1
- package/web-dist/assets/{requirementDiagram-TGXJPOKE-CTs2_V6T.js → requirementDiagram-TGXJPOKE-Bs8opBRj.js} +1 -1
- package/web-dist/assets/{sankeyDiagram-HTMAVEWB-QTLLcDPD.js → sankeyDiagram-HTMAVEWB-gOb3FIr8.js} +1 -1
- package/web-dist/assets/{sequenceDiagram-DBY2YBRQ-Bxw9Tr6e.js → sequenceDiagram-DBY2YBRQ-6DvjtRHR.js} +1 -1
- package/web-dist/assets/{stateDiagram-2N3HPSRC-BecB6roG.js → stateDiagram-2N3HPSRC-Cg2LWdUf.js} +1 -1
- package/web-dist/assets/stateDiagram-v2-6OUMAXLB-Digf9CWZ.js +1 -0
- package/web-dist/assets/{swimlanes-5IMT3BWC-COiYgS0w.js → swimlanes-5IMT3BWC-BDpPdYEl.js} +1 -1
- package/web-dist/assets/swimlanesDiagram-G3AALYLV-DEZRDWHA.js +8 -0
- package/web-dist/assets/{timeline-definition-FHXFAJF6-Ci22coeH.js → timeline-definition-FHXFAJF6-BSjfeutv.js} +1 -1
- package/web-dist/assets/{treeView-QDETBFTQ-4DW35czh.js → treeView-QDETBFTQ-DwgPy6V3.js} +1 -1
- package/web-dist/assets/{treemap-6X3UGDF4-Dq_-Z6-Y.js → treemap-6X3UGDF4-7eiifTKW.js} +1 -1
- package/web-dist/assets/{vennDiagram-L72KCM5P-CHQSIEYq.js → vennDiagram-L72KCM5P-9u4AhcnV.js} +1 -1
- package/web-dist/assets/{wardley-OPB4EBWU-Bwlh7HCY.js → wardley-OPB4EBWU-DqNb5x9U.js} +1 -1
- package/web-dist/assets/{wardleyDiagram-EHGQE667-DctjuPYn.js → wardleyDiagram-EHGQE667-DlJ7h3vg.js} +1 -1
- package/web-dist/assets/{xychartDiagram-FW5EYKEG-ZoIGosv8.js → xychartDiagram-FW5EYKEG-B_9ukg2S.js} +1 -1
- package/web-dist/index.html +2 -2
- package/web-dist/assets/architecture-TIHT7OUA-ChUMo004.js +0 -1
- package/web-dist/assets/channel-pr7r6raB.js +0 -1
- package/web-dist/assets/classDiagram-OUVF2IWQ-CvGbPMn_.js +0 -1
- package/web-dist/assets/classDiagram-v2-EOCWNBFH-CvGbPMn_.js +0 -1
- package/web-dist/assets/eventmodeling-45OFAUF4-DKmyo-jd.js +0 -1
- package/web-dist/assets/flowDiagram-23GEKE2U-DluCBvT4.js +0 -1
- package/web-dist/assets/index-BQtjYMpV.css +0 -1
- package/web-dist/assets/index-CLW1Q49U.js +0 -360
- package/web-dist/assets/railroad-abnf-AHOZXSZD-U_vb4BrX.js +0 -1
- package/web-dist/assets/railroad-ebnf-EBAXGLYW-BIHG7gNU.js +0 -1
- package/web-dist/assets/railroad-peg-LSFZ7HO6-Cpd9r-tB.js +0 -1
- package/web-dist/assets/stateDiagram-v2-6OUMAXLB-JXw9T96l.js +0 -1
- package/web-dist/assets/swimlanesDiagram-G3AALYLV--1Wv7FQQ.js +0 -8
package/dist/dev-child.js
CHANGED
|
@@ -5541,13 +5541,27 @@ function isAllowedAttachmentName(name) {
|
|
|
5541
5541
|
function isDirectSubAgent(agent) {
|
|
5542
5542
|
return (agent.depth ?? 1) === 1;
|
|
5543
5543
|
}
|
|
5544
|
-
function
|
|
5545
|
-
|
|
5546
|
-
|
|
5544
|
+
function retiredAwaitingKindsIn(body) {
|
|
5545
|
+
if (!body || !RETIRED_LINE_RE.test(body)) return [];
|
|
5546
|
+
const found = [];
|
|
5547
|
+
for (const line of body.split("\n")) {
|
|
5548
|
+
const m = /^\s*([a-z-]+):\s*\S/i.exec(line);
|
|
5549
|
+
const kind = m?.[1].toLowerCase();
|
|
5550
|
+
if (!kind) continue;
|
|
5551
|
+
const hit = RETIRED_AWAITING_KINDS.find((k) => k === kind);
|
|
5552
|
+
if (hit && !found.includes(hit)) found.push(hit);
|
|
5553
|
+
}
|
|
5554
|
+
return found;
|
|
5547
5555
|
}
|
|
5548
|
-
function
|
|
5549
|
-
|
|
5550
|
-
|
|
5556
|
+
function isAwaitingItemKind(kind) {
|
|
5557
|
+
return AWAITING_ITEM_KINDS.includes(kind);
|
|
5558
|
+
}
|
|
5559
|
+
function parseAwaitingDuration(value) {
|
|
5560
|
+
const m = AWAITING_DURATION_RE.exec(value.trim());
|
|
5561
|
+
if (!m) return null;
|
|
5562
|
+
const ms = Number(m[1]) * DURATION_UNIT_MS[m[2]];
|
|
5563
|
+
if (!Number.isFinite(ms) || ms <= 0) return null;
|
|
5564
|
+
return Math.min(ms, AWAITING_FOR_MAX_MS);
|
|
5551
5565
|
}
|
|
5552
5566
|
function saysAllDone(text) {
|
|
5553
5567
|
if (typeof text !== "string") return false;
|
|
@@ -5557,10 +5571,11 @@ function saysAllDone(text) {
|
|
|
5557
5571
|
}
|
|
5558
5572
|
return false;
|
|
5559
5573
|
}
|
|
5560
|
-
function restPromptMessage(prompt) {
|
|
5574
|
+
function restPromptMessage(prompt, opts = {}) {
|
|
5575
|
+
const note = opts.overQuestion ? `${OVER_QUESTION_NOTE} ${OPT_OUT_NOTE}` : OPT_OUT_NOTE;
|
|
5561
5576
|
return `${prompt.trim()}
|
|
5562
5577
|
|
|
5563
|
-
(Goal \u2014 sent each time you come to rest. ${
|
|
5578
|
+
(Goal \u2014 sent each time you come to rest. ${note})`;
|
|
5564
5579
|
}
|
|
5565
5580
|
function compactionPromptMessage(prompt) {
|
|
5566
5581
|
return `${prompt.trim()}
|
|
@@ -5580,11 +5595,55 @@ function formatIntervalLabel(seconds) {
|
|
|
5580
5595
|
const hours = minutes / 60;
|
|
5581
5596
|
return `${Number.isInteger(hours) ? hours : hours.toFixed(1)} hr`;
|
|
5582
5597
|
}
|
|
5583
|
-
function
|
|
5584
|
-
const
|
|
5585
|
-
|
|
5598
|
+
function prWatchWakeMessage(input) {
|
|
5599
|
+
const lines = [];
|
|
5600
|
+
if (input.merged || input.closed) {
|
|
5601
|
+
lines.push(`\u23F0 ${input.target} was ${input.merged ? "MERGED" : "CLOSED"}.`, "");
|
|
5602
|
+
lines.push("(This watcher is spent \u2014 there is nothing further to report on a finished PR.)");
|
|
5603
|
+
return lines.join("\n");
|
|
5604
|
+
}
|
|
5605
|
+
if (input.checks) {
|
|
5606
|
+
const c = input.checks;
|
|
5607
|
+
lines.push(
|
|
5608
|
+
c.verdict === "passing" ? `\u2705 CI PASSED on ${input.target} \u2014 ${c.passed} check${c.passed === 1 ? "" : "s"} green.` : `\u274C CI FAILED on ${input.target}${c.failing.length ? `: ${c.failing.join(", ")}` : ""}.`
|
|
5609
|
+
);
|
|
5610
|
+
}
|
|
5611
|
+
if (input.review) {
|
|
5612
|
+
if (lines.length) lines.push("");
|
|
5613
|
+
lines.push(input.review);
|
|
5614
|
+
}
|
|
5615
|
+
lines.push("", "(Registered PR watcher \u2014 STILL ARMED. It reports again on the next CI change, review or comment. Drop it with `mcp__frizz__watch_pr` when it stops mattering.)");
|
|
5616
|
+
return lines.join("\n");
|
|
5617
|
+
}
|
|
5618
|
+
function shellDoneMessage(shell) {
|
|
5619
|
+
const what = shell.taskId ? `\`${shell.taskId}\` \u2014 ${shell.label}` : shell.label;
|
|
5620
|
+
const verb = shell.status === "failed" ? "FAILED" : shell.status === "killed" ? "was STOPPED" : "finished";
|
|
5621
|
+
return `\u23F0 Your background shell ${verb}: ${what}.
|
|
5586
5622
|
|
|
5587
|
-
(
|
|
5623
|
+
(Frizz sends this because it finished after you came to rest, where your runtime's own completion notification does not reach you. Read its output if you still need it.)`;
|
|
5624
|
+
}
|
|
5625
|
+
function liveOpsLines(ops) {
|
|
5626
|
+
const lines = [];
|
|
5627
|
+
const section = (heading, kind, items) => {
|
|
5628
|
+
if (!items.length) return;
|
|
5629
|
+
lines.push("", heading);
|
|
5630
|
+
for (const i of items) lines.push(`- \`${kind}: ${i.id ?? "?"}\` \u2014 ${i.label}`);
|
|
5631
|
+
};
|
|
5632
|
+
section("Background shells still running:", "shell", ops?.shells ?? []);
|
|
5633
|
+
section("Sub-agents still running (they re-invoke you on their own, so parking on one is optional):", "agent", ops?.subAgents ?? []);
|
|
5634
|
+
section("Timers you have armed:", "timer", ops?.timers ?? []);
|
|
5635
|
+
section("Pull requests you registered:", "pr", ops?.prs ?? []);
|
|
5636
|
+
return lines;
|
|
5637
|
+
}
|
|
5638
|
+
function signoffNudgeMessage(ops) {
|
|
5639
|
+
const lines = liveOpsLines(ops);
|
|
5640
|
+
if (lines.length) {
|
|
5641
|
+
lines.push("", "An ```awaiting fence takes one such line per thing you are ACTUALLY waiting on, plus a");
|
|
5642
|
+
lines.push("required `for:` duration (`30s`/`15m`/`2h`/`3d`) and a one-line `reason:`. Frizz checks every");
|
|
5643
|
+
lines.push("id: name something that is not running and you are bumped rather than parked.");
|
|
5644
|
+
}
|
|
5645
|
+
return lines.length === 0 ? SIGNOFF_NUDGE_MESSAGE : `${SIGNOFF_NUDGE_MESSAGE}
|
|
5646
|
+
${lines.join("\n")}`;
|
|
5588
5647
|
}
|
|
5589
5648
|
function timerPromptMessage(prompt, fireAt) {
|
|
5590
5649
|
return `${prompt.trim()}
|
|
@@ -5692,7 +5751,7 @@ function parseGithubWakeSteer(text) {
|
|
|
5692
5751
|
function frizzRoute(path) {
|
|
5693
5752
|
return `${FRIZZ_ROUTE_PREFIX}${path.startsWith("/") ? path : `/${path}`}`;
|
|
5694
5753
|
}
|
|
5695
|
-
var ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrizzStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, NativeInputRequired, AwaitingHint,
|
|
5754
|
+
var ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrizzStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, NativeInputRequired, AwaitingHint, RETIRED_AWAITING_KINDS, RETIRED_LINE_RE, RETIRED_AWAITING_REPLACEMENT, AWAITING_ITEM_KINDS, AWAITING_DURATION_RE, DURATION_UNIT_MS, AWAITING_FOR_MAX_MS, SnoozeUntil, SNOOZE_PROMPT_MAX, SnoozePrompt, RECURRING_PROMPT_MAX, RECURRING_MIN_INTERVAL_SECONDS, RECURRING_MAX_INTERVAL_SECONDS, RecurringPromptText, RecurringIntervalSeconds, ThreadRecurringPrompt, ALLDONE_SENTINEL, OPT_OUT_NOTE, OVER_QUESTION_NOTE, TIMER_PROMPT_MAX, TIMER_MAX_DELAY_SECONDS, TIMER_MAX_ARMED, TimerPromptText, SIGNOFF_NUDGE_MARKER, SIGNOFF_NUDGE_MESSAGE, ThreadWatchKind, GithubChecksState, GithubMergeState, GithubWatchStatus, ThreadWatchView, PR_WATCH_DEFAULT_FOR_MS, PR_WATCH_MAX_ARMED, PrWatchView, AddOwnPrWatchInput, AddOwnPrWatchResult, DropOwnPrWatchInput, DropOwnPrWatchResult, ListOwnPrWatchesInput, OwnPrWatchesResult, ThreadTimerView, ThreadFence, PlanView, LimitWindow, LimitPause, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AccountEmails, AuthSnapshot, AccountLogoutInput, AccountLogoutResult, AccountLoginStartInput, AccountLoginStartResult, AccountLoginStatusInput, AccountLoginStatusResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, FollowUpInput, UnqueueFollowUpInput, UnqueueFollowUpResult, DeliverQueuedNowInput, DeliverQueuedNowResult, SetThreadSnoozeInput, SetThreadRecurringPromptInput, SetOwnThreadRecurringPromptInput, SetOwnThreadRecurringPromptResult, GetOwnThreadRecurringPromptInput, OwnThreadRecurringPromptResult, SetOwnThreadTimerInput, CancelOwnThreadTimerInput, ListOwnThreadTimersInput, OwnThreadTimersResult, ThreadActivityItem, ListOwnThreadActivityInput, OwnThreadActivityResult, SetOwnThreadTimerResult, CancelOwnThreadTimerResult, SetOwnThreadStopHookInput, SetOwnThreadHeartbeatInput, ThreadPluginReloadResult, 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, WAKE_DELIVERY_TOKEN_LINE, AGENT_MESSAGE_WRAPPER, GithubWakeItem, GithubWakeSteer, WAKE_SCOPE, WAKE_REVIEW_LEAD, WAKE_BACKLOG_TAIL, WAKE_REF, WAKE_SINGLE, WAKE_MULTI_HEAD, WAKE_ITEM, WAKE_MORE, PR_WATCH_FINISHED, PR_WATCH_CI_PASSED, PR_WATCH_CI_FAILED, GithubStatus, GithubRefCard, GithubRefPreviewInput, GithubRefPreviewResult, GithubItem, GithubListInput, GithubListResult, GithubBatchInput, GithubBatchResult, BoardMeta, ServerEvent, TranscriptEdit, TranscriptTodo, TranscriptToolCall, TranscriptPart, TranscriptMessage, TranscriptPageCursor, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketClientMsg, ProjectCard, PROJECT_ICON_EXTENSIONS, PROJECT_ICON_MAX_BASE64_CHARS, DirectoryPickResult, ThreadLocation, FRIZZ_ROUTE_PREFIX, DEFAULT_PORT, threadIdentityName;
|
|
5696
5755
|
var init_src = __esm({
|
|
5697
5756
|
"packages/shared/src/index.ts"() {
|
|
5698
5757
|
"use strict";
|
|
@@ -5904,7 +5963,15 @@ var init_src = __esm({
|
|
|
5904
5963
|
command: external_exports.string().optional(),
|
|
5905
5964
|
// ISO8601 of the shell output file's last write — "last active 6 min ago" for a quiet-but-live
|
|
5906
5965
|
// watcher. Optional (see SubAgentView.lastActivityAt).
|
|
5907
|
-
lastActivityAt: external_exports.string().optional()
|
|
5966
|
+
lastActivityAt: external_exports.string().optional(),
|
|
5967
|
+
// The PROVIDER's session-wide background-task handle (`bzvtnt3ig`), as distinct from `id`, which is
|
|
5968
|
+
// the launch tool_use id. Both name the same shell and neither is a substitute for the other:
|
|
5969
|
+
// `id` is what the two copies of a row reconcile on, and this is the handle the runtime hands the
|
|
5970
|
+
// MODEL — "Command running in background with ID: bzvtnt3ig" is the only id a worker ever sees, so
|
|
5971
|
+
// it is the one it registers a `shell` watcher against. Matching on `id`/`label` alone meant every
|
|
5972
|
+
// such watcher was unfireable (scheduler.evalWatchers, 2026-08-14). Absent for a CODEX row, whose
|
|
5973
|
+
// single `processId` IS its `id`, and for a Claude row between its tool_use and its launch ack.
|
|
5974
|
+
taskId: external_exports.string().optional()
|
|
5908
5975
|
});
|
|
5909
5976
|
CompletionHoldOp = external_exports.object({
|
|
5910
5977
|
label: external_exports.string(),
|
|
@@ -5941,10 +6008,22 @@ var init_src = __esm({
|
|
|
5941
6008
|
title: external_exports.string().max(120)
|
|
5942
6009
|
});
|
|
5943
6010
|
AwaitingHint = external_exports.object({
|
|
5944
|
-
kind: external_exports.enum(["
|
|
6011
|
+
kind: external_exports.enum(["shell", "agent", "timer", "pr", "for", "reason"]),
|
|
5945
6012
|
value: external_exports.string()
|
|
5946
6013
|
});
|
|
5947
|
-
|
|
6014
|
+
RETIRED_AWAITING_KINDS = ["watch", "pr-watch", "human", "ci", "session"];
|
|
6015
|
+
RETIRED_LINE_RE = new RegExp(`^\\s*(${RETIRED_AWAITING_KINDS.join("|")}):\\s*\\S`, "im");
|
|
6016
|
+
RETIRED_AWAITING_REPLACEMENT = {
|
|
6017
|
+
"watch": "`shell: <the id your runtime gave you>` (or `agent: <id>`) \u2014 the same id, on the current line kind",
|
|
6018
|
+
"pr-watch": "register the PR with `mcp__frizz__watch_pr`, then name it `pr: owner/repo#123`",
|
|
6019
|
+
"human": "there is no human gate any more \u2014 if you need a person, ask a ```question instead of parking",
|
|
6020
|
+
"ci": "CI is not a wait of its own: register the PR with `mcp__frizz__watch_pr` and you are woken when its checks settle",
|
|
6021
|
+
"session": "there is no cross-session wait \u2014 name the sub-agent you dispatched with `agent: <id>`"
|
|
6022
|
+
};
|
|
6023
|
+
AWAITING_ITEM_KINDS = ["shell", "agent", "timer", "pr"];
|
|
6024
|
+
AWAITING_DURATION_RE = /^(\d{1,5})(s|m|h|d)$/;
|
|
6025
|
+
DURATION_UNIT_MS = { s: 1e3, m: 6e4, h: 36e5, d: 864e5 };
|
|
6026
|
+
AWAITING_FOR_MAX_MS = 24 * 60 * 60 * 1e3;
|
|
5948
6027
|
SnoozeUntil = external_exports.string().regex(
|
|
5949
6028
|
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/,
|
|
5950
6029
|
"Snooze time must be an ISO-8601 UTC instant"
|
|
@@ -5958,13 +6037,6 @@ var init_src = __esm({
|
|
|
5958
6037
|
RECURRING_MIN_INTERVAL_SECONDS = 60;
|
|
5959
6038
|
RECURRING_MAX_INTERVAL_SECONDS = 24 * 60 * 60;
|
|
5960
6039
|
RecurringPromptText = external_exports.string().trim().min(1).max(RECURRING_PROMPT_MAX);
|
|
5961
|
-
DEFAULT_GOAL_TRIGGERS = {
|
|
5962
|
-
stopHook: true,
|
|
5963
|
-
heartbeat: false,
|
|
5964
|
-
postCompaction: false,
|
|
5965
|
-
pauseOnQuestions: true
|
|
5966
|
-
};
|
|
5967
|
-
DEFAULT_RECURRING_PROMPT = "If further work towards the original task/goal remains, keep going. If there are open questions that require human input, ask them with question fences.";
|
|
5968
6040
|
RecurringIntervalSeconds = external_exports.number().int().min(RECURRING_MIN_INTERVAL_SECONDS).max(RECURRING_MAX_INTERVAL_SECONDS);
|
|
5969
6041
|
ThreadRecurringPrompt = external_exports.object({
|
|
5970
6042
|
prompt: external_exports.string(),
|
|
@@ -5974,12 +6046,6 @@ var init_src = __esm({
|
|
|
5974
6046
|
stopHook: external_exports.boolean(),
|
|
5975
6047
|
heartbeat: external_exports.boolean(),
|
|
5976
6048
|
postCompaction: external_exports.boolean(),
|
|
5977
|
-
/** NOT a fourth trigger — a HOLD over all three. While it is on, nothing is sent for as long as the
|
|
5978
|
-
* thread is blocked on the human: an unanswered ```question fence, a native ask, or a permission
|
|
5979
|
-
* prompt. The footer panel seeds it ON for a fresh arming, with the stop hook, because they are one
|
|
5980
|
-
* intent — see scheduler.ts, "WHAT A PENDING QUESTION DOES TO ALL THREE TRIGGERS". The stored column
|
|
5981
|
-
* still defaults OFF, so an existing row and an older caller both keep the behaviour they had. */
|
|
5982
|
-
pauseOnQuestions: external_exports.boolean(),
|
|
5983
6049
|
intervalSeconds: external_exports.number().int().positive().optional(),
|
|
5984
6050
|
armedAt: external_exports.string(),
|
|
5985
6051
|
/** Last delivery per trigger; stamped separately so each reads its own clock. */
|
|
@@ -5989,40 +6055,164 @@ var init_src = __esm({
|
|
|
5989
6055
|
}).strict();
|
|
5990
6056
|
ALLDONE_SENTINEL = "ALLDONE";
|
|
5991
6057
|
OPT_OUT_NOTE = "To stop these, sign off with a ```done fence \u2014 but ONLY when the work is genuinely finished: it files this thread away, and nothing but new work from the human reopens it.";
|
|
6058
|
+
OVER_QUESTION_NOTE = "Your ```question is still unanswered, and a Goal armed at rest means the operator is not waiting to answer it: decide it yourself, say in one line which way you went and what would reverse it, and carry on. Do NOT re-ask it.";
|
|
5992
6059
|
TIMER_PROMPT_MAX = SNOOZE_PROMPT_MAX;
|
|
5993
6060
|
TIMER_MAX_DELAY_SECONDS = 30 * 24 * 60 * 60;
|
|
5994
6061
|
TIMER_MAX_ARMED = 64;
|
|
5995
|
-
WATCH_MAX_ARMED = 32;
|
|
5996
6062
|
TimerPromptText = external_exports.string().trim().min(1).max(TIMER_PROMPT_MAX);
|
|
5997
6063
|
SIGNOFF_NUDGE_MARKER = "**This message is from frizz, not from the human.**";
|
|
5998
6064
|
SIGNOFF_NUDGE_MESSAGE = [
|
|
5999
|
-
`${SIGNOFF_NUDGE_MARKER} Nothing about your task has changed, and
|
|
6065
|
+
`${SIGNOFF_NUDGE_MARKER} Nothing about your task has changed, and no new work is being asked of you.`,
|
|
6066
|
+
"",
|
|
6067
|
+
"You rested without a fence, so this thread cannot be triaged.",
|
|
6068
|
+
"",
|
|
6069
|
+
"**IF THE TASK STILL HAS PARTS LEFT, THE FENCE IS NOT WHAT YOU OWE \u2014 THE WORK IS.** If ANY part of the",
|
|
6070
|
+
"original task is unfinished, unverified, or deferred, resume it NOW, in THIS turn, and sign off once it",
|
|
6071
|
+
"is genuinely finished. A milestone, a green test run and a long turn are none of them endings, and",
|
|
6072
|
+
"neither is naming the next step or writing it into a scratch file.",
|
|
6000
6073
|
"",
|
|
6001
|
-
"
|
|
6074
|
+
"**AND THAT TASK IS ALSO THE CEILING \u2014 finish it, and nothing else.** Work you notice on the way is a",
|
|
6075
|
+
"FINDING TO REPORT in your sign-off, never work to take on: the bug beside the one you were sent for,",
|
|
6076
|
+
"the refactor the code obviously wants, the second issue the first one touches. Widening the job is not",
|
|
6077
|
+
"thoroughness \u2014 it is a different job nobody asked for, and it buries the answer they did ask for under",
|
|
6078
|
+
"changes they now have to review. If it should be done, name it in one line and let the human dispatch",
|
|
6079
|
+
"it.",
|
|
6080
|
+
"",
|
|
6081
|
+
"**IF WHAT YOU WERE ASKED FOR IS A DOCUMENT, THE DOCUMENT IS THE ENDING.** A triage, a review, an",
|
|
6082
|
+
"investigation, a recommendation, a plan \u2014 when that is the deliverable, the finished write-up IS the",
|
|
6083
|
+
"work. Implementing what it proposes is the NEXT job, and not yours unless you were asked. Sign off with",
|
|
6084
|
+
"the write-up.",
|
|
6085
|
+
"",
|
|
6086
|
+
"**DECIDE RATHER THAN ASK.** Stop only for a decision that is genuinely the human's AND that blocks you",
|
|
6087
|
+
"right now: ask that one in a question fence. Every other open choice INSIDE the task \u2014 a name, a",
|
|
6088
|
+
"default, a reversible design call \u2014 is yours to make: decide it, say in one line which way you went and",
|
|
6089
|
+
"what would reverse it, and carry on. A choice that would ENLARGE the task is not one of those: an",
|
|
6090
|
+
"unanswered question is not permission to go build the answer.",
|
|
6091
|
+
"",
|
|
6092
|
+
"Otherwise, add a fence at the END of your next message:",
|
|
6002
6093
|
"",
|
|
6003
6094
|
"- `` ```question `` \u2014 you need the human. One question per fence, lettered options, one recommended.",
|
|
6004
6095
|
"- `` ```done `` \u2014 genuinely FINISHED. A DISMISSAL: the card is filed away and nobody looks again, so",
|
|
6005
6096
|
" if anything is still owed, it is not done. Body: 1-3 sentences, then bullets, each opening with a",
|
|
6006
6097
|
" **bolded verb phrase**.",
|
|
6007
|
-
"-
|
|
6098
|
+
"- `` ```awaiting `` \u2014 you are WAITING on work that is actually running. It is PURE STRUCTURE: one line",
|
|
6099
|
+
" per thing you are waiting on, a REQUIRED `for:` duration, and one line of `reason:` prose.",
|
|
6100
|
+
"",
|
|
6101
|
+
" ```awaiting",
|
|
6102
|
+
" shell: <the id your runtime gave you>",
|
|
6103
|
+
" pr: owner/repo#123",
|
|
6104
|
+
" for: 2h",
|
|
6105
|
+
" reason: what you are waiting for, in one line",
|
|
6106
|
+
" ```",
|
|
6107
|
+
"",
|
|
6108
|
+
" Frizz CHECKS every line: name something that is not running and you are bumped rather than parked.",
|
|
6109
|
+
" A fence that names NOTHING is not a park at all \u2014 if you are not waiting on anything, you are not",
|
|
6110
|
+
" awaiting, you are done. Register a PR with `mcp__frizz__watch_pr` and a timer with",
|
|
6111
|
+
" `mcp__frizz__timer`; `mcp__frizz__activity` reads back everything you have running, with its id.",
|
|
6112
|
+
"",
|
|
6113
|
+
"**STILL OWED counts things you are not going to do yourself.** A decision you are RECOMMENDING, a",
|
|
6114
|
+
"draft you wrote but did not send, follow-up work you discovered \u2014 all of it dies with the card, even",
|
|
6115
|
+
"the part that is someone else's to do. Each ends on a question with your recommendation as option A,",
|
|
6116
|
+
"or you DO it first \u2014 a sub-agent's result comes BACK to you, so it lands on your card; a new card via",
|
|
6117
|
+
"`mcp__frizz__spawn_thread` is the LAST resort, since nothing it learns returns to you or its siblings.",
|
|
6118
|
+
"None of them is a `done`. And what is not",
|
|
6119
|
+
'worth a card is not worth a SENTENCE: delete the dangling "one thing to carry forward", never park it',
|
|
6120
|
+
"in the handoff.",
|
|
6008
6121
|
"",
|
|
6009
6122
|
"**DO NOT REPEAT YOURSELF.** If the message you just wrote already stands on its own, reply with the",
|
|
6010
6123
|
"fence ALONE \u2014 the human reads both together, so restating it costs them the second read for nothing.",
|
|
6124
|
+
"The same holds inside one message: the card is the ledger of what shipped, the prose is only what a",
|
|
6125
|
+
"ledger cannot hold, and a sentence that reads the same in either belongs in exactly one of them.",
|
|
6011
6126
|
"",
|
|
6012
6127
|
"Only if it does NOT stand alone, fix that first, briefly. It has to be readable cold: the human has",
|
|
6013
6128
|
"seen nothing since their own last message \u2014 the Goal, this reminder, a watcher wake all came from",
|
|
6014
6129
|
"frizz \u2014 so anything you assumed they had followed, they have not."
|
|
6015
6130
|
].join("\n");
|
|
6016
6131
|
ThreadWatchKind = external_exports.enum(["shell", "github"]);
|
|
6017
|
-
|
|
6018
|
-
|
|
6132
|
+
GithubChecksState = external_exports.enum(["none", "running", "passing", "failing"]);
|
|
6133
|
+
GithubMergeState = external_exports.enum(["mergeable", "blocked", "conflicting", "unknown"]);
|
|
6134
|
+
GithubWatchStatus = external_exports.object({
|
|
6135
|
+
checks: GithubChecksState,
|
|
6136
|
+
/** The counts behind the verdict, so the card can say "3 running, 12 passed" the way GitHub does
|
|
6137
|
+
* rather than only "checks are running". */
|
|
6138
|
+
running: external_exports.number().int().nonnegative(),
|
|
6139
|
+
passed: external_exports.number().int().nonnegative(),
|
|
6140
|
+
failed: external_exports.number().int().nonnegative(),
|
|
6141
|
+
/** The failing job NAMES, capped — what a human actually needs to decide whether to look. */
|
|
6142
|
+
failing: external_exports.array(external_exports.string()).max(8).default([]),
|
|
6143
|
+
merge: GithubMergeState,
|
|
6144
|
+
/** OPEN | CLOSED | MERGED, lowercased. A merged or closed PR ends the wait outright. */
|
|
6145
|
+
state: external_exports.enum(["open", "closed", "merged"]),
|
|
6146
|
+
/** When frizz last heard from GitHub. A poll can fail or be rate-limited, and a stale reading stated
|
|
6147
|
+
* as current is worse than no reading. */
|
|
6148
|
+
polledAt: external_exports.string(),
|
|
6149
|
+
/** The head commit this verdict was reached on. A PR watcher reports a CI verdict when it is NEWS, and
|
|
6150
|
+
* "red again on a new commit" is news that the bare word `failing` cannot express. Optional because a
|
|
6151
|
+
* reading taken before 2026-08-17 carries none. */
|
|
6152
|
+
head: external_exports.string().optional(),
|
|
6153
|
+
/** A digest of WHICH jobs are failing right now. The head commit alone cannot express a re-run of the
|
|
6154
|
+
* same job, or a slower job going red after the first — both of which are a second failure the worker
|
|
6155
|
+
* must hear about. Empty when nothing is failing. */
|
|
6156
|
+
failureSig: external_exports.string().optional()
|
|
6157
|
+
}).strict();
|
|
6019
6158
|
ThreadWatchView = external_exports.object({
|
|
6020
6159
|
id: external_exports.string(),
|
|
6021
6160
|
kind: ThreadWatchKind,
|
|
6022
6161
|
target: external_exports.string(),
|
|
6023
6162
|
state: external_exports.enum(["armed", "fired", "dropped"]),
|
|
6024
|
-
createdAt: external_exports.string()
|
|
6163
|
+
createdAt: external_exports.string(),
|
|
6164
|
+
/** `github` rows only, and absent until the first successful poll. */
|
|
6165
|
+
github: GithubWatchStatus.optional()
|
|
6166
|
+
}).strict();
|
|
6167
|
+
PR_WATCH_DEFAULT_FOR_MS = 6 * 60 * 60 * 1e3;
|
|
6168
|
+
PR_WATCH_MAX_ARMED = 32;
|
|
6169
|
+
PrWatchView = external_exports.object({
|
|
6170
|
+
id: external_exports.string(),
|
|
6171
|
+
/** `owner/repo#N`, normalized — the same string the board's row and the status book are keyed by. */
|
|
6172
|
+
target: external_exports.string(),
|
|
6173
|
+
state: external_exports.enum(["armed", "dropped", "settled"]),
|
|
6174
|
+
createdAt: external_exports.string(),
|
|
6175
|
+
/** The PR's checks/mergeability as the poller last saw it. Absent until the first successful poll. */
|
|
6176
|
+
github: GithubWatchStatus.optional()
|
|
6177
|
+
}).strict();
|
|
6178
|
+
AddOwnPrWatchInput = external_exports.object({
|
|
6179
|
+
slug: ThreadSlug,
|
|
6180
|
+
/** `owner/repo#123` or a PR URL. Parsed server-side; an unparseable ref is refused rather than stored,
|
|
6181
|
+
* because a watcher that can never fire is worse than none — the worker rests believing it is covered. */
|
|
6182
|
+
target: external_exports.string().trim().min(1).max(200),
|
|
6183
|
+
/** How long to watch, as a DURATION (`30m`, `2h`, `3d` — parseAwaitingDuration).
|
|
6184
|
+
*
|
|
6185
|
+
* A PR nobody ever reviews would otherwise be polled forever, and a thread parked on it would wait
|
|
6186
|
+
* forever with it — the same unbounded wait the awaiting fence's `for:` closes, one level down. A
|
|
6187
|
+
* duration rather than an instant for the same reason it is one there: it cannot be written in the
|
|
6188
|
+
* past (maintainer 2026-08-15, asking for it explicitly).
|
|
6189
|
+
*
|
|
6190
|
+
* REQUIRED BY THE TOOL, OPTIONAL ON THE WIRE, and the asymmetry is deliberate. A worker's MCP server
|
|
6191
|
+
* outlives every frizz restart, so a session dispatched before this existed still holds a binary that
|
|
6192
|
+
* cannot send it — and making the RPC reject that would break `watch_pr` outright for every thread
|
|
6193
|
+
* already running, with no recourse from inside those threads. Absent ⇒ PR_WATCH_DEFAULT_FOR, which
|
|
6194
|
+
* still BOUNDS the poll; the point of the field is that a worker chooses, and one that cannot choose
|
|
6195
|
+
* is better bounded than broken. */
|
|
6196
|
+
for: external_exports.string().trim().min(1).max(16).optional()
|
|
6197
|
+
}).strict();
|
|
6198
|
+
AddOwnPrWatchResult = external_exports.object({
|
|
6199
|
+
id: external_exports.string(),
|
|
6200
|
+
target: external_exports.string(),
|
|
6201
|
+
/** True when this exact PR was ALREADY watched by this thread, so the call registered nothing new.
|
|
6202
|
+
* Re-registering after a compaction is the common case, and a duplicate would double every wake. */
|
|
6203
|
+
alreadyArmed: external_exports.boolean(),
|
|
6204
|
+
watches: external_exports.array(PrWatchView)
|
|
6205
|
+
}).strict();
|
|
6206
|
+
DropOwnPrWatchInput = external_exports.object({
|
|
6207
|
+
slug: ThreadSlug,
|
|
6208
|
+
id: external_exports.string().min(1).max(64)
|
|
6025
6209
|
}).strict();
|
|
6210
|
+
DropOwnPrWatchResult = external_exports.object({
|
|
6211
|
+
dropped: external_exports.boolean(),
|
|
6212
|
+
watches: external_exports.array(PrWatchView)
|
|
6213
|
+
}).strict();
|
|
6214
|
+
ListOwnPrWatchesInput = external_exports.object({ slug: ThreadSlug }).strict();
|
|
6215
|
+
OwnPrWatchesResult = external_exports.object({ watches: external_exports.array(PrWatchView) }).strict();
|
|
6026
6216
|
ThreadTimerView = external_exports.object({
|
|
6027
6217
|
id: external_exports.string(),
|
|
6028
6218
|
prompt: external_exports.string(),
|
|
@@ -6180,6 +6370,16 @@ var init_src = __esm({
|
|
|
6180
6370
|
// AUTO-bump (the scheduler resumes the agent with exactly this text) rather than a reminder, which is
|
|
6181
6371
|
// the distinction the held row's tooltip renders. Absent ⇒ the card merely re-surfaces.
|
|
6182
6372
|
snoozePrompt: external_exports.string().optional(),
|
|
6373
|
+
/** The EVENT snooze on the resting card is armed for this exact rest — the human has said "hide this
|
|
6374
|
+
* until something reports". Distinct from `snoozedUntil`, which is a wall-clock park on the whole
|
|
6375
|
+
* thread: this one has no deadline and clears itself when the thread comes to a NEW rest.
|
|
6376
|
+
*
|
|
6377
|
+
* It travels because the chat has to honour it too (2026-08-14). Until then only the QUEUE did, on
|
|
6378
|
+
* the reasoning that the card states a FACT the drawer must keep showing or it blanks at rest and
|
|
6379
|
+
* reads as "the agent died". That reasoning holds for a thread nobody has parked; once the human has
|
|
6380
|
+
* explicitly parked THIS rest, showing them the same card with the same button one surface over is
|
|
6381
|
+
* not information, and they said so. */
|
|
6382
|
+
bgSnoozed: external_exports.boolean().optional(),
|
|
6183
6383
|
// Which Claude transport serves this thread: "broker" = a session-broker-owned Agent SDK session
|
|
6184
6384
|
// (typed control channel), "tmux" = the interactive TUI in a pane. Only the broker can be asked to
|
|
6185
6385
|
// reload its plugin closure in place, so the board needs it to decide whether to offer that verb at
|
|
@@ -6297,6 +6497,13 @@ var init_src = __esm({
|
|
|
6297
6497
|
// Optional so a pre-restart server keeps working; absent means "fall back to `/`", i.e. the old
|
|
6298
6498
|
// behaviour. Registry-derived, so it is the same slug every other surface links to.
|
|
6299
6499
|
projectSlug: external_exports.string().optional(),
|
|
6500
|
+
// The server's home directory — the expansion of a `~` a worker wrote in prose. Agents reference
|
|
6501
|
+
// files that way constantly (`~/.claude/CLAUDE.md`), and the browser has no way to derive it, so a
|
|
6502
|
+
// `~`-anchored Markdown link had no absolute path to become and stayed a same-origin anchor that
|
|
6503
|
+
// navigated out of Frizz. The client only ever uses it to build a path it then hands BACK to the
|
|
6504
|
+
// server, which realpath-gates it exactly as it gates one the author typed in full. Optional so a
|
|
6505
|
+
// pre-restart server keeps working; absent means `~` links stay unresolved, i.e. the old behaviour.
|
|
6506
|
+
homeDir: external_exports.string().optional(),
|
|
6300
6507
|
// (No `.frizz/ exists` bit here on purpose. Threads are session-first — the ui.db registry IS the
|
|
6301
6508
|
// board — so `.frizz/` presence says nothing about whether this project has one. Its only consumer
|
|
6302
6509
|
// was a shell gate that dead-ended `.frizz`-less repos; the server still probes the directory
|
|
@@ -6403,13 +6610,23 @@ var init_src = __esm({
|
|
|
6403
6610
|
// Default action for a vetted non-image local path in agent markdown. Image clicks always use the
|
|
6404
6611
|
// OS default viewer so screenshots retain their expected behavior.
|
|
6405
6612
|
localFileOpener: LocalFileOpener.optional(),
|
|
6406
|
-
// GitHub batch-dispatch prompt
|
|
6407
|
-
// unset OR blank the server falls back to its exported
|
|
6408
|
-
//
|
|
6409
|
-
//
|
|
6410
|
-
//
|
|
6411
|
-
|
|
6412
|
-
|
|
6613
|
+
// The GitHub batch-dispatch prompt template (the picker's per-item worker prompt). Optional: when
|
|
6614
|
+
// unset OR blank the server falls back to its exported DEFAULT_GITHUB_PROMPT. Substitution tokens
|
|
6615
|
+
// the server fills: {repo} {n} {title} {url} {labels} {body}. The leading `THREAD: <slug>` tag is
|
|
6616
|
+
// prepended by the server (not part of the editable template) so a custom prompt can never break the
|
|
6617
|
+
// thread↔.frizz-file binding. Optional so old settings blobs parse.
|
|
6618
|
+
//
|
|
6619
|
+
// ONE field, not one per kind. Issue and PR each had their own template until 2026-08-15, and the two
|
|
6620
|
+
// said the same thing twice: read the whole thread, be skeptical, cite what you checked, post nothing.
|
|
6621
|
+
// A person tuning "be more dubious" had to make the same edit in two boxes and keep them in step. The
|
|
6622
|
+
// ONE thing that genuinely differs per kind — which `gh` command reads the item — is a line in the
|
|
6623
|
+
// metadata block, so the merged template just carries both, and the worker picks the one that applies.
|
|
6624
|
+
//
|
|
6625
|
+
// There is no migration off the two old keys, and that is deliberate (the maintainer's call): Settings
|
|
6626
|
+
// is a non-strict z.object, so `githubIssuePrompt`/`githubPrPrompt` are STRIPPED the moment an old
|
|
6627
|
+
// blob is parsed. Any existing override is dropped and the reader gets the new shipped default —
|
|
6628
|
+
// exactly the intended backfill. Merging two customized templates into one has no correct answer.
|
|
6629
|
+
githubPrompt: external_exports.string().optional()
|
|
6413
6630
|
});
|
|
6414
6631
|
DispatchProviderPreferences = external_exports.object({
|
|
6415
6632
|
model: external_exports.string().trim().min(1).max(200).optional(),
|
|
@@ -6538,9 +6755,6 @@ var init_src = __esm({
|
|
|
6538
6755
|
// so a client that predates it — an older tab, an older MCP server — keeps writing the row correctly
|
|
6539
6756
|
// with the trigger off, which is the honest reading of a caller that has never heard of it.
|
|
6540
6757
|
postCompaction: external_exports.boolean().default(false),
|
|
6541
|
-
// The QUESTION HOLD (2026-08-11). Defaulted for the same reason as the trigger above: a caller that
|
|
6542
|
-
// has never heard of it means "don't hold", which is also this option's own default.
|
|
6543
|
-
pauseOnQuestions: external_exports.boolean().default(false),
|
|
6544
6758
|
intervalSeconds: RecurringIntervalSeconds.optional()
|
|
6545
6759
|
}).strict();
|
|
6546
6760
|
SetOwnThreadRecurringPromptInput = external_exports.object({
|
|
@@ -6552,8 +6766,14 @@ var init_src = __esm({
|
|
|
6552
6766
|
// so a client that predates it — an older tab, an older MCP server — keeps writing the row correctly
|
|
6553
6767
|
// with the trigger off, which is the honest reading of a caller that has never heard of it.
|
|
6554
6768
|
postCompaction: external_exports.boolean().default(false),
|
|
6555
|
-
|
|
6556
|
-
|
|
6769
|
+
/** ACCEPTED AND IGNORED. The question hold was deleted 2026-08-16 (see scheduler.ts) and no caller in
|
|
6770
|
+
* this repo sends it any more — but this object is `.strict()`, and the caller on the other end is a
|
|
6771
|
+
* DETACHED worker daemon holding the `frizz-mcp.mjs` it was spawned with. Those outlive a server
|
|
6772
|
+
* restart by design, so for as long as any pre-2026-08-16 worker is alive a `start` would otherwise
|
|
6773
|
+
* come back as a validation error on a field the model cannot see it is sending. Tolerated here rather
|
|
6774
|
+
* than in the router so the shape stays one declaration; delete it once no such worker can be running.
|
|
6775
|
+
* The BROWSER input above needs no such clause — a stale tab is one reload away. */
|
|
6776
|
+
pauseOnQuestions: external_exports.boolean().optional(),
|
|
6557
6777
|
intervalSeconds: RecurringIntervalSeconds.optional()
|
|
6558
6778
|
}).strict();
|
|
6559
6779
|
SetOwnThreadRecurringPromptResult = external_exports.object({
|
|
@@ -6580,6 +6800,23 @@ var init_src = __esm({
|
|
|
6580
6800
|
OwnThreadTimersResult = external_exports.object({
|
|
6581
6801
|
timers: external_exports.array(ThreadTimerView)
|
|
6582
6802
|
}).strict();
|
|
6803
|
+
ThreadActivityItem = external_exports.object({
|
|
6804
|
+
kind: external_exports.enum(["shell", "agent", "timer", "pr"]),
|
|
6805
|
+
/** The string a `<kind>:` fence line must carry. For a shell that is the runtime task id the worker
|
|
6806
|
+
* was shown; for a PR, `owner/repo#N`; for a timer, its `tmr_…` row id. */
|
|
6807
|
+
id: external_exports.string(),
|
|
6808
|
+
label: external_exports.string(),
|
|
6809
|
+
/** ISO8601 of when it started or was armed — absent when frizz has no instant for it. */
|
|
6810
|
+
since: external_exports.string().optional(),
|
|
6811
|
+
/** A timer's fire instant, or a PR's expiry. Absent for shells and sub-agents. */
|
|
6812
|
+
until: external_exports.string().optional()
|
|
6813
|
+
}).strict();
|
|
6814
|
+
ListOwnThreadActivityInput = external_exports.object({
|
|
6815
|
+
slug: ThreadSlug
|
|
6816
|
+
}).strict();
|
|
6817
|
+
OwnThreadActivityResult = external_exports.object({
|
|
6818
|
+
activity: external_exports.array(ThreadActivityItem)
|
|
6819
|
+
}).strict();
|
|
6583
6820
|
SetOwnThreadTimerResult = external_exports.object({
|
|
6584
6821
|
id: external_exports.string(),
|
|
6585
6822
|
fireAt: external_exports.string(),
|
|
@@ -6589,49 +6826,6 @@ var init_src = __esm({
|
|
|
6589
6826
|
cancelled: external_exports.boolean(),
|
|
6590
6827
|
timers: external_exports.array(ThreadTimerView)
|
|
6591
6828
|
}).strict();
|
|
6592
|
-
AddOwnThreadWatchInput = external_exports.object({
|
|
6593
|
-
slug: ThreadSlug,
|
|
6594
|
-
// REGISTERABLE kinds only, which is `shell` and nothing else — deliberately narrower than
|
|
6595
|
-
// `ThreadWatchKind`, whose `github` member is a board-derived VIEW row with no registration behind it.
|
|
6596
|
-
// The MCP tool already refuses a PR here in words ("a pr wait does not belong here — use an ```awaiting
|
|
6597
|
-
// fence"); this is the same refusal at the wire, where it cannot be talked past.
|
|
6598
|
-
kind: RegisterableThreadWatchKind,
|
|
6599
|
-
target: ThreadWatchTarget,
|
|
6600
|
-
/** The worker is BLOCKING on this one inside its tool call, so the scheduler settles it silently
|
|
6601
|
-
* instead of delivering a wake — see the column comment in storage.ts. Defaulted, so a caller that
|
|
6602
|
-
* has never heard of it registers an ordinary background watch. */
|
|
6603
|
-
foreground: external_exports.boolean().default(false)
|
|
6604
|
-
}).strict();
|
|
6605
|
-
PromoteOwnThreadWatchInput = external_exports.object({
|
|
6606
|
-
slug: ThreadSlug,
|
|
6607
|
-
id: external_exports.string().min(1).max(64)
|
|
6608
|
-
}).strict();
|
|
6609
|
-
PromoteOwnThreadWatchResult = external_exports.object({
|
|
6610
|
-
promoted: external_exports.boolean(),
|
|
6611
|
-
watches: external_exports.array(ThreadWatchView)
|
|
6612
|
-
}).strict();
|
|
6613
|
-
DropOwnThreadWatchInput = external_exports.object({
|
|
6614
|
-
slug: ThreadSlug,
|
|
6615
|
-
id: external_exports.string().min(1).max(64)
|
|
6616
|
-
}).strict();
|
|
6617
|
-
ListOwnThreadWatchesInput = external_exports.object({
|
|
6618
|
-
slug: ThreadSlug
|
|
6619
|
-
}).strict();
|
|
6620
|
-
OwnThreadWatchesResult = external_exports.object({
|
|
6621
|
-
watches: external_exports.array(ThreadWatchView)
|
|
6622
|
-
}).strict();
|
|
6623
|
-
AddOwnThreadWatchResult = external_exports.object({
|
|
6624
|
-
id: external_exports.string(),
|
|
6625
|
-
/** True when this exact (kind, target) was ALREADY armed on the thread, so the call registered
|
|
6626
|
-
* nothing new. A worker re-registering after a compaction is the common case, and silently minting a
|
|
6627
|
-
* duplicate would mean two wakes for one event. */
|
|
6628
|
-
alreadyArmed: external_exports.boolean(),
|
|
6629
|
-
watches: external_exports.array(ThreadWatchView)
|
|
6630
|
-
}).strict();
|
|
6631
|
-
DropOwnThreadWatchResult = external_exports.object({
|
|
6632
|
-
dropped: external_exports.boolean(),
|
|
6633
|
-
watches: external_exports.array(ThreadWatchView)
|
|
6634
|
-
}).strict();
|
|
6635
6829
|
SetOwnThreadStopHookInput = external_exports.object({
|
|
6636
6830
|
slug: ThreadSlug,
|
|
6637
6831
|
prompt: RecurringPromptText.nullable(),
|
|
@@ -6736,12 +6930,52 @@ ${DISPATCH_TASK_BANNER}
|
|
|
6736
6930
|
);
|
|
6737
6931
|
WAKE_ITEM = /^- (👤|🤖) (.+?) from @(\S+?)(?: at (\S+?))?(?:: (\S+))?$/;
|
|
6738
6932
|
WAKE_MORE = /^- …and (\d+) more not listed — check .+ for the rest$/;
|
|
6933
|
+
PR_WATCH_FINISHED = new RegExp(String.raw`^⏰ (${WAKE_REF}) was (MERGED|CLOSED)\.$`);
|
|
6934
|
+
PR_WATCH_CI_PASSED = new RegExp(String.raw`^✅ CI PASSED on (${WAKE_REF}) — (\d+) checks? green\.$`);
|
|
6935
|
+
PR_WATCH_CI_FAILED = new RegExp(String.raw`^❌ CI FAILED on (${WAKE_REF})(?:: (.+?))?\.$`);
|
|
6739
6936
|
GithubStatus = external_exports.object({
|
|
6740
6937
|
installed: external_exports.boolean(),
|
|
6741
6938
|
inRepo: external_exports.boolean(),
|
|
6742
6939
|
nameWithOwner: external_exports.string().nullable(),
|
|
6743
6940
|
authed: external_exports.boolean()
|
|
6744
6941
|
});
|
|
6942
|
+
GithubRefCard = external_exports.object({
|
|
6943
|
+
ref: external_exports.string(),
|
|
6944
|
+
kind: external_exports.enum(["issue", "pr", "commit"]),
|
|
6945
|
+
repo: external_exports.string(),
|
|
6946
|
+
// owner/repo, for the card's header line
|
|
6947
|
+
url: external_exports.string(),
|
|
6948
|
+
title: external_exports.string(),
|
|
6949
|
+
body: external_exports.string(),
|
|
6950
|
+
// already truncated server-side to the card's excerpt budget
|
|
6951
|
+
state: external_exports.string(),
|
|
6952
|
+
// OPEN | CLOSED | MERGED | DRAFT — empty for a commit, which has no state
|
|
6953
|
+
stateReason: external_exports.string().optional(),
|
|
6954
|
+
// COMPLETED | NOT_PLANNED | REOPENED — GitHub's closed-issue nuance
|
|
6955
|
+
at: external_exports.string().optional(),
|
|
6956
|
+
// ISO: opened-at for an issue/PR, committed-at for a commit
|
|
6957
|
+
authorLogin: external_exports.string().optional(),
|
|
6958
|
+
authorName: external_exports.string().optional(),
|
|
6959
|
+
// commits carry a git author name with no GitHub account behind it
|
|
6960
|
+
authorAvatar: external_exports.string().optional(),
|
|
6961
|
+
labels: external_exports.array(external_exports.object({ name: external_exports.string(), color: external_exports.string() })).default([]),
|
|
6962
|
+
additions: external_exports.number().int().nonnegative().optional(),
|
|
6963
|
+
deletions: external_exports.number().int().nonnegative().optional(),
|
|
6964
|
+
changedFiles: external_exports.number().int().nonnegative().optional(),
|
|
6965
|
+
comments: external_exports.number().int().nonnegative().optional(),
|
|
6966
|
+
// Epoch ms of the fetch this card came from. The CLIENT owns the freshness decision (render the
|
|
6967
|
+
// cached card instantly, then revalidate if it is old), so it has to be able to see the age.
|
|
6968
|
+
fetchedAt: external_exports.number().int().nonnegative()
|
|
6969
|
+
});
|
|
6970
|
+
GithubRefPreviewInput = external_exports.object({
|
|
6971
|
+
refs: external_exports.array(external_exports.string().min(3).max(120)).min(1).max(100),
|
|
6972
|
+
refresh: external_exports.boolean().default(false)
|
|
6973
|
+
});
|
|
6974
|
+
GithubRefPreviewResult = external_exports.object({
|
|
6975
|
+
cards: external_exports.array(GithubRefCard),
|
|
6976
|
+
missing: external_exports.array(external_exports.string()),
|
|
6977
|
+
error: external_exports.string().optional()
|
|
6978
|
+
});
|
|
6745
6979
|
GithubItem = external_exports.object({
|
|
6746
6980
|
kind: external_exports.enum(["issue", "pr"]),
|
|
6747
6981
|
number: external_exports.number().int().positive(),
|
|
@@ -7798,11 +8032,11 @@ ${GITHUB_DISPATCH_UI_BOUNDARY}
|
|
|
7798
8032
|
|
|
7799
8033
|
${filled}`;
|
|
7800
8034
|
}
|
|
7801
|
-
function effectiveTemplate(
|
|
8035
|
+
function effectiveTemplate(custom2) {
|
|
7802
8036
|
if (custom2 && custom2.trim().length > 0) return custom2;
|
|
7803
|
-
return
|
|
8037
|
+
return DEFAULT_GITHUB_PROMPT;
|
|
7804
8038
|
}
|
|
7805
|
-
var pexec, GH_TIMEOUT, GH_MAXBUF, SEARCH_SORT, SEARCH_RESULT_WINDOW, LINKED_PR_CHUNK, GITHUB_PAGE_SIZE, BODY_CAP,
|
|
8039
|
+
var pexec, GH_TIMEOUT, GH_MAXBUF, SEARCH_SORT, SEARCH_RESULT_WINDOW, LINKED_PR_CHUNK, GITHUB_PAGE_SIZE, BODY_CAP, DEFAULT_GITHUB_PROMPT;
|
|
7806
8040
|
var init_github = __esm({
|
|
7807
8041
|
"packages/server/src/github.ts"() {
|
|
7808
8042
|
"use strict";
|
|
@@ -7818,26 +8052,30 @@ var init_github = __esm({
|
|
|
7818
8052
|
LINKED_PR_CHUNK = 25;
|
|
7819
8053
|
GITHUB_PAGE_SIZE = 30;
|
|
7820
8054
|
BODY_CAP = 8 * 1024;
|
|
7821
|
-
|
|
8055
|
+
DEFAULT_GITHUB_PROMPT = `Triage this GitHub issue/PR and make recommendations. Be thoughtful, thorough, and dubious. Think through whether this is truly a good idea or not. Many ideas are bad.
|
|
7822
8056
|
|
|
7823
|
-
|
|
8057
|
+
Read the full thread first \u2014 body, labels, and the whole discussion \u2014 then classify the report as a BUG or a FEATURE request and say which it is and why.
|
|
7824
8058
|
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
8059
|
+
If it is a BUG REPORT: establish whether the reported behavior actually happens on the current tree (capture the exact steps, command and output if it reproduces; say what you saw instead if it does not), trace the cause to concrete code, and state the smallest correct fix \u2014 or the top 2 options with the tradeoff of each \u2014 plus the files it touches and the risk. Be sure you have high confidence that the bug is real. There should be a clean and elegant solution. If there isn't an elegant way to do something, then it often means that it's not a good idea or that we are in need of a larger codebase refactor. The issue: you should stay in the codebase for similar categories of bugs. We are constantly seeking to proactively self-improve the codebase.
|
|
8060
|
+
|
|
8061
|
+
If it is a FEATURE REQUEST: restate the request precisely (the use-case behind it and any ambiguity a maintainer must resolve), map where it lands in the code and what public API / UX surface it changes, and sketch a concrete plan for the smallest viable version with its risks, open design questions and a rough size estimate.
|
|
8062
|
+
|
|
8063
|
+
If it is a PR: review it. Read the PR description, comments, and changes. You should be dubious. Do not assume that the PR is high quality, many are not. For each substantive change ask: is there a better approach? Is it correct? Does it handle edges? Is it elegant? Does it break existing behavior or the public API? Are there tests, and do they actually cover the change? Trace the impact of the changes thoughtfully. Check CI too. Recommend either closing, making changes (explain them), or merging. Test the changes yourself if necessary/possible.
|
|
8064
|
+
|
|
8065
|
+
If it is a DOCS CHANGE: make sure that it is well written, minimal, no walls of text, well structured, consistent tonally with others docs/prose, and not AI slop.
|
|
8066
|
+
|
|
8067
|
+
Bias always towards proactivity and taking immediate action. If you encounter issues, feel free to add fixes for those in the same PR. Do not worry about perfect git hygiene.
|
|
7831
8068
|
|
|
7832
8069
|
---
|
|
7833
8070
|
|
|
7834
|
-
PR #{n}: {title}
|
|
8071
|
+
Issue/PR #{n}: {title}
|
|
7835
8072
|
Repository: {repo}
|
|
7836
8073
|
URL: {url}
|
|
7837
8074
|
Labels: {labels}
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
8075
|
+
Body + thread (issues): \`gh issue view {n} -R {repo} --comments\`
|
|
8076
|
+
Body + thread (PRs): \`gh pr view {n} -R {repo} --comments\`
|
|
8077
|
+
Diff (PRs): \`gh pr diff {n} -R {repo}\`
|
|
8078
|
+
CI (PRs): \`gh pr checks {n} -R {repo}\``;
|
|
7841
8079
|
}
|
|
7842
8080
|
});
|
|
7843
8081
|
|
|
@@ -10440,41 +10678,39 @@ function createStorage(dbPath) {
|
|
|
10440
10678
|
ON thread_timer(state, fire_at);
|
|
10441
10679
|
CREATE INDEX IF NOT EXISTS thread_timer_slug
|
|
10442
10680
|
ON thread_timer(thread_slug, state, fire_at);
|
|
10443
|
-
-- A worker's registered WATCHERS (2026-08-
|
|
10444
|
-
--
|
|
10445
|
-
--
|
|
10446
|
-
--
|
|
10447
|
-
-- from the final assistant message on every fold, and cleared by any newer record \u2014 so a park was
|
|
10448
|
-
-- lost the moment the worker said one more sentence, there was no id to cancel, and nothing for a
|
|
10449
|
-
-- Snooze button to be about. Maintainer 2026-08-11: "seems a little weird to register these watchers
|
|
10450
|
-
-- with no identifer and no elegant mechanism for dismissing the watcher. they just live forever in
|
|
10451
|
-
-- markdown\u2026i think that's why we dropped awaiting before."
|
|
10681
|
+
-- A worker's registered PR WATCHERS (2026-08-14). Registered by tool call, never by a fence: the
|
|
10682
|
+
-- fence states what a thread is waiting on, and watching is a separate, orthogonal thing that
|
|
10683
|
+
-- happens whether or not anything is written down (maintainer: "We should have a tool for this. The
|
|
10684
|
+
-- agent should have a tool to register a PR watcher").
|
|
10452
10685
|
--
|
|
10453
|
-
--
|
|
10454
|
-
--
|
|
10455
|
-
CREATE TABLE IF NOT EXISTS
|
|
10686
|
+
-- REPEATING, not one-shot. It reports CI turning green or red and every later review or comment, so
|
|
10687
|
+
-- it stays armed and carries a cursor; only a merge or a close settles it.
|
|
10688
|
+
CREATE TABLE IF NOT EXISTS pr_watch (
|
|
10456
10689
|
id TEXT PRIMARY KEY,
|
|
10457
10690
|
thread_slug TEXT NOT NULL,
|
|
10458
|
-
|
|
10459
|
-
|
|
10460
|
-
|
|
10691
|
+
owner TEXT NOT NULL,
|
|
10692
|
+
repo TEXT NOT NULL,
|
|
10693
|
+
number INTEGER NOT NULL,
|
|
10694
|
+
state TEXT NOT NULL CHECK (state IN ('armed', 'dropped', 'settled')),
|
|
10461
10695
|
created_at INTEGER NOT NULL,
|
|
10462
10696
|
settled_at INTEGER,
|
|
10463
|
-
-- Opaque per-kind progress marker: for a pr watcher, the activity baseline that decides what
|
|
10464
|
-
-- counts as NEW. Written by the scheduler, never read by anything else, so its grammar can change
|
|
10465
|
-
-- without a migration.
|
|
10466
10697
|
cursor TEXT,
|
|
10467
|
-
--
|
|
10468
|
-
--
|
|
10469
|
-
--
|
|
10470
|
-
|
|
10471
|
-
-- timeout expires first it PROMOTES the row (clears this), and the wake comes back.
|
|
10472
|
-
foreground INTEGER NOT NULL DEFAULT 0
|
|
10698
|
+
-- When this watcher stops polling by itself. REQUIRED at registration (2026-08-15): a PR nobody
|
|
10699
|
+
-- ever touches would otherwise be polled forever, and the thread parked on it would wait forever
|
|
10700
|
+
-- with it. Nullable in the column only so an older row reads; the tool refuses to arm without one.
|
|
10701
|
+
expires_at INTEGER
|
|
10473
10702
|
);
|
|
10474
|
-
CREATE INDEX IF NOT EXISTS
|
|
10475
|
-
ON
|
|
10476
|
-
CREATE INDEX IF NOT EXISTS
|
|
10477
|
-
ON
|
|
10703
|
+
CREATE INDEX IF NOT EXISTS pr_watch_armed
|
|
10704
|
+
ON pr_watch(state);
|
|
10705
|
+
CREATE INDEX IF NOT EXISTS pr_watch_slug
|
|
10706
|
+
ON pr_watch(thread_slug, state, created_at);
|
|
10707
|
+
-- thread_watch WAS HERE and is retired (2026-08-14). A worker's wait is a 'watch:' line in its own
|
|
10708
|
+
-- awaiting fence again, which is BOTH the park and the wake -- the scheduler matches the name against
|
|
10709
|
+
-- the thread's live shells and its retired-shell ring, so nothing needs registering and nothing can
|
|
10710
|
+
-- outlive the fence that declares it. The registry existed because a fence had no identity to drop;
|
|
10711
|
+
-- the answer turned out to be that a park nobody has to drop needs none. An old DB keeps the orphan
|
|
10712
|
+
-- table, which costs nothing and is safer than a migration to remove it.
|
|
10713
|
+
-- (No backticks in this comment: the whole schema is a template literal.)
|
|
10478
10714
|
`);
|
|
10479
10715
|
for (const col of [
|
|
10480
10716
|
"archived INTEGER NOT NULL DEFAULT 0",
|
|
@@ -10549,16 +10785,21 @@ function createStorage(dbPath) {
|
|
|
10549
10785
|
// which is the correct default: an existing prompt described the triggers its operator chose.
|
|
10550
10786
|
"recurring_on_compact INTEGER NOT NULL DEFAULT 0",
|
|
10551
10787
|
"recurring_compact_fired_at TEXT",
|
|
10552
|
-
//
|
|
10553
|
-
//
|
|
10554
|
-
//
|
|
10555
|
-
|
|
10788
|
+
// NO `recurring_pause_on_questions` HERE ANY MORE. It held every trigger while the thread was waiting
|
|
10789
|
+
// on the human and the footer showed it inverted as "Autonomous mode"; both were deleted 2026-08-16
|
|
10790
|
+
// (see scheduler.ts, "WHAT A PENDING QUESTION DOES TO THE THREE TRIGGERS"). A database created before
|
|
10791
|
+
// that release still carries the column — it is `NOT NULL DEFAULT 0`, so nothing needs to write it,
|
|
10792
|
+
// and dropping it would cost a table rebuild to reclaim one inert integer per row.
|
|
10556
10793
|
// THE BUILT-IN SIGN-OFF NUDGE (scheduler SOURCE 9, 2026-08-12). How many times in a row frizz has
|
|
10557
10794
|
// told this thread how to sign off without a fence appearing. Cleared ONLY when the thread signs
|
|
10558
10795
|
// off — never by a user record, because frizz's own delivery is one. The second column holds the
|
|
10559
10796
|
// last-nudged delivery id, for diagnosis.
|
|
10560
10797
|
"signoff_nudges INTEGER NOT NULL DEFAULT 0",
|
|
10561
10798
|
"signoff_nudge_anchor TEXT",
|
|
10799
|
+
// Cleared by `resetParkBumps` when a park is actually HONOURED — the one event that proves the
|
|
10800
|
+
// correction landed, and the one frizz cannot cause by correcting.
|
|
10801
|
+
"park_bumps INTEGER NOT NULL DEFAULT 0",
|
|
10802
|
+
"park_bump_anchor TEXT",
|
|
10562
10803
|
// Title provenance for the CURRENT text (2026-08-07): 1 = the worker's own title signal wrote it,
|
|
10563
10804
|
// 0 = the dispatch seeded it. DEFAULT 0 is the conservative direction — an existing row is assumed
|
|
10564
10805
|
// to hold its dispatch chop until the repair below (or the next title signal) says otherwise.
|
|
@@ -10569,6 +10810,10 @@ function createStorage(dbPath) {
|
|
|
10569
10810
|
} catch {
|
|
10570
10811
|
}
|
|
10571
10812
|
}
|
|
10813
|
+
try {
|
|
10814
|
+
db.exec("ALTER TABLE pr_watch ADD COLUMN expires_at INTEGER");
|
|
10815
|
+
} catch {
|
|
10816
|
+
}
|
|
10572
10817
|
try {
|
|
10573
10818
|
db.exec("UPDATE session SET tmux_name = 'frizz-' || substr(tmux_name, 6) WHERE tmux_name LIKE 'fray-%'");
|
|
10574
10819
|
} catch {
|
|
@@ -10925,10 +11170,6 @@ function createStorage(dbPath) {
|
|
|
10925
11170
|
recurring_on_rest = ?,
|
|
10926
11171
|
recurring_on_schedule = ?,
|
|
10927
11172
|
recurring_on_compact = ?,
|
|
10928
|
-
-- The hold is deliberately absent from every CASE below it: it changes neither the WORDS nor the
|
|
10929
|
-
-- cadence, so a delivery already queued still describes this row exactly. Flipping it must not
|
|
10930
|
-
-- mint a generation or drop a "last sent" stamp.
|
|
10931
|
-
recurring_pause_on_questions = ?,
|
|
10932
11173
|
recurring_armed_at = CASE
|
|
10933
11174
|
WHEN ? IS NULL THEN NULL
|
|
10934
11175
|
WHEN recurring_armed_at IS NOT NULL AND recurring_prompt IS ? AND recurring_interval_ms IS ? THEN recurring_armed_at
|
|
@@ -10945,7 +11186,7 @@ function createStorage(dbPath) {
|
|
|
10945
11186
|
WHEN ? IS NULL THEN NULL
|
|
10946
11187
|
WHEN recurring_armed_at IS NOT NULL AND recurring_prompt IS ? THEN recurring_compact_fired_at
|
|
10947
11188
|
ELSE NULL END`;
|
|
10948
|
-
const recurringArgs = ({ prompt, stopHook, heartbeat, postCompaction,
|
|
11189
|
+
const recurringArgs = ({ prompt, stopHook, heartbeat, postCompaction, intervalMs, armedAt }) => {
|
|
10949
11190
|
const ms = prompt === null ? null : intervalMs;
|
|
10950
11191
|
const flag = (on) => prompt === null || !on ? 0 : 1;
|
|
10951
11192
|
return [
|
|
@@ -10955,7 +11196,6 @@ function createStorage(dbPath) {
|
|
|
10955
11196
|
flag(stopHook),
|
|
10956
11197
|
flag(heartbeat),
|
|
10957
11198
|
flag(postCompaction),
|
|
10958
|
-
flag(pauseOnQuestions),
|
|
10959
11199
|
prompt,
|
|
10960
11200
|
prompt,
|
|
10961
11201
|
ms,
|
|
@@ -11015,35 +11255,41 @@ function createStorage(dbPath) {
|
|
|
11015
11255
|
UPDATE session SET signoff_nudges = 0, signoff_nudge_anchor = NULL
|
|
11016
11256
|
WHERE slug = ? AND signoff_nudges > 0
|
|
11017
11257
|
`);
|
|
11018
|
-
const
|
|
11019
|
-
|
|
11020
|
-
|
|
11258
|
+
const countParkBumpStmt = db.prepare(`
|
|
11259
|
+
UPDATE session SET park_bumps = park_bumps + 1, park_bump_anchor = ?
|
|
11260
|
+
WHERE slug = ?
|
|
11261
|
+
`);
|
|
11262
|
+
const resetParkBumpsStmt = db.prepare(`
|
|
11263
|
+
UPDATE session SET park_bumps = 0, park_bump_anchor = NULL
|
|
11264
|
+
WHERE slug = ? AND park_bumps > 0
|
|
11021
11265
|
`);
|
|
11022
|
-
const
|
|
11023
|
-
|
|
11266
|
+
const armPrWatchStmt = db.prepare(`
|
|
11267
|
+
INSERT INTO pr_watch (id, thread_slug, owner, repo, number, state, created_at, settled_at, cursor, expires_at)
|
|
11268
|
+
VALUES (@id, @slug, @owner, @repo, @number, 'armed', @createdAtMs, NULL, NULL, @expiresAtMs)
|
|
11269
|
+
`);
|
|
11270
|
+
const expiredPrWatchesStmt = db.prepare(
|
|
11271
|
+
"SELECT * FROM pr_watch WHERE state = 'armed' AND expires_at IS NOT NULL AND expires_at <= ? ORDER BY expires_at, id"
|
|
11024
11272
|
);
|
|
11025
|
-
const
|
|
11026
|
-
"SELECT * FROM
|
|
11273
|
+
const prWatchesBySlugStmt = db.prepare(
|
|
11274
|
+
"SELECT * FROM pr_watch WHERE thread_slug = ? ORDER BY created_at, id"
|
|
11027
11275
|
);
|
|
11028
|
-
const
|
|
11029
|
-
|
|
11030
|
-
"SELECT * FROM thread_watch WHERE state = 'armed' ORDER BY created_at, id"
|
|
11276
|
+
const armedPrWatchesBySlugStmt = db.prepare(
|
|
11277
|
+
"SELECT * FROM pr_watch WHERE thread_slug = ? AND state = 'armed' ORDER BY created_at, id"
|
|
11031
11278
|
);
|
|
11032
|
-
const
|
|
11033
|
-
|
|
11034
|
-
|
|
11035
|
-
|
|
11036
|
-
|
|
11279
|
+
const prWatchByIdStmt = db.prepare("SELECT * FROM pr_watch WHERE id = ?");
|
|
11280
|
+
const armedPrWatchesStmt = db.prepare(
|
|
11281
|
+
"SELECT * FROM pr_watch WHERE state = 'armed' ORDER BY created_at, id"
|
|
11282
|
+
);
|
|
11283
|
+
const dropPrWatchStmt = db.prepare(`
|
|
11284
|
+
UPDATE pr_watch SET state = 'dropped', settled_at = ?
|
|
11037
11285
|
WHERE id = ? AND thread_slug = ? AND state = 'armed'
|
|
11038
11286
|
`);
|
|
11039
|
-
const
|
|
11040
|
-
UPDATE
|
|
11287
|
+
const settlePrWatchStmt = db.prepare(`
|
|
11288
|
+
UPDATE pr_watch SET state = 'settled', settled_at = ?
|
|
11041
11289
|
WHERE id = ? AND state = 'armed'
|
|
11042
11290
|
`);
|
|
11043
|
-
const
|
|
11044
|
-
|
|
11045
|
-
`);
|
|
11046
|
-
const delThreadWatches = db.prepare("DELETE FROM thread_watch WHERE thread_slug = ?");
|
|
11291
|
+
const prWatchCursorStmt = db.prepare("UPDATE pr_watch SET cursor = ? WHERE id = ? AND state = 'armed'");
|
|
11292
|
+
const delPrWatches = db.prepare("DELETE FROM pr_watch WHERE thread_slug = ?");
|
|
11047
11293
|
const confirmAwaitingWaitStmt = db.prepare(`
|
|
11048
11294
|
UPDATE session
|
|
11049
11295
|
SET awaiting_fence_id = ?, awaiting_confirmed_at = ?, snoozed_until = ?
|
|
@@ -11106,7 +11352,7 @@ function createStorage(dbPath) {
|
|
|
11106
11352
|
}
|
|
11107
11353
|
delRetiredOps.run(existing.slug);
|
|
11108
11354
|
delThreadTimers.run(existing.slug);
|
|
11109
|
-
|
|
11355
|
+
delPrWatches.run(existing.slug);
|
|
11110
11356
|
delSession.run(existing.slug);
|
|
11111
11357
|
return existing;
|
|
11112
11358
|
};
|
|
@@ -11544,14 +11790,16 @@ function createStorage(dbPath) {
|
|
|
11544
11790
|
setRecurringPromptBySlug: (slug, write) => recurringBySlugStmt.run(...recurringArgs(write), slug).changes === 1,
|
|
11545
11791
|
countSignoffNudge: (slug, anchor) => void countNudgeStmt.run(anchor, slug),
|
|
11546
11792
|
resetSignoffNudges: (slug) => void resetNudgesStmt.run(slug),
|
|
11547
|
-
|
|
11548
|
-
|
|
11549
|
-
|
|
11550
|
-
|
|
11551
|
-
|
|
11552
|
-
|
|
11553
|
-
|
|
11554
|
-
|
|
11793
|
+
countParkBump: (slug, anchor) => void countParkBumpStmt.run(anchor, slug),
|
|
11794
|
+
resetParkBumps: (slug) => void resetParkBumpsStmt.run(slug),
|
|
11795
|
+
armPrWatch: (watch) => void armPrWatchStmt.run(watch),
|
|
11796
|
+
listPrWatches: (slug, opts) => (opts?.armedOnly ? armedPrWatchesBySlugStmt : prWatchesBySlugStmt).all(slug),
|
|
11797
|
+
getPrWatch: (id) => prWatchByIdStmt.get(id),
|
|
11798
|
+
armedPrWatches: () => armedPrWatchesStmt.all(),
|
|
11799
|
+
expiredPrWatches: (nowMs) => expiredPrWatchesStmt.all(nowMs),
|
|
11800
|
+
dropPrWatch: (slug, id, settledAtMs) => dropPrWatchStmt.run(settledAtMs, id, slug).changes === 1,
|
|
11801
|
+
settlePrWatch: (id, settledAtMs) => settlePrWatchStmt.run(settledAtMs, id).changes === 1,
|
|
11802
|
+
setPrWatchCursor: (id, cursor) => prWatchCursorStmt.run(cursor, id).changes === 1,
|
|
11555
11803
|
armThreadTimer: (timer) => void armTimerStmt.run(timer),
|
|
11556
11804
|
listThreadTimers: (slug, opts) => (opts?.armedOnly ? armedTimersBySlugStmt : timersBySlugStmt).all(slug),
|
|
11557
11805
|
getThreadTimer: (id) => timerByIdStmt.get(id),
|
|
@@ -13334,15 +13582,25 @@ function matchComposedText(items, recordText, contemporaneous) {
|
|
|
13334
13582
|
}
|
|
13335
13583
|
return matched;
|
|
13336
13584
|
}
|
|
13337
|
-
function ageDeliveries(items, nowMs) {
|
|
13585
|
+
function ageDeliveries(items, nowMs, observedUserAt) {
|
|
13338
13586
|
if (!items.length) return items;
|
|
13339
13587
|
let changed = false;
|
|
13588
|
+
const userAtMs = Date.parse(observedUserAt ?? "");
|
|
13589
|
+
const supersededByUserTurn = (item) => {
|
|
13590
|
+
if (!Number.isFinite(userAtMs)) return false;
|
|
13591
|
+
const born = Date.parse(item.at);
|
|
13592
|
+
return Number.isFinite(born) && userAtMs >= born;
|
|
13593
|
+
};
|
|
13340
13594
|
const next = [];
|
|
13341
13595
|
for (const item of items) {
|
|
13342
13596
|
if (item.state === "cancelled") {
|
|
13343
13597
|
next.push(item);
|
|
13344
13598
|
continue;
|
|
13345
13599
|
}
|
|
13600
|
+
if (supersededByUserTurn(item)) {
|
|
13601
|
+
changed = true;
|
|
13602
|
+
continue;
|
|
13603
|
+
}
|
|
13346
13604
|
const born = Date.parse(item.at);
|
|
13347
13605
|
if (item.state === "pending" && Number.isFinite(born) && nowMs - born > PENDING_TIMEOUT_MS) {
|
|
13348
13606
|
next.push({ ...item, state: "unconfirmed", updatedAt: new Date(nowMs).toISOString() });
|
|
@@ -14027,6 +14285,7 @@ function newTailState(slug, sessionId, path, foreign = false, nativeSessionId =
|
|
|
14027
14285
|
paneDead: false,
|
|
14028
14286
|
noTranscript: false,
|
|
14029
14287
|
nextDiscoverMs: 0,
|
|
14288
|
+
discoverMisses: 0,
|
|
14030
14289
|
stallLogged: false,
|
|
14031
14290
|
customTitleRevision: 0
|
|
14032
14291
|
};
|
|
@@ -14086,18 +14345,23 @@ function parseSignalFence(text) {
|
|
|
14086
14345
|
if (!kind) return void 0;
|
|
14087
14346
|
if (norm.slice(end).trim() !== "") return void 0;
|
|
14088
14347
|
if (kind === "done") return { kind, body: capFenceBody(raw2.trim()), hints: [] };
|
|
14348
|
+
const lines = raw2.split("\n");
|
|
14349
|
+
const delimiter2 = lines.findIndex((l) => /^\s*---+\s*$/.test(l));
|
|
14350
|
+
const frontmatter = delimiter2 === -1 ? lines : lines.slice(0, delimiter2);
|
|
14351
|
+
const prose = delimiter2 === -1 ? [] : lines.slice(delimiter2 + 1);
|
|
14089
14352
|
const hints = [];
|
|
14090
14353
|
const rest = [];
|
|
14091
|
-
for (const line of
|
|
14354
|
+
for (const line of frontmatter) {
|
|
14092
14355
|
const hm = line.match(AWAITING_HINT_RE);
|
|
14093
14356
|
const k = hm?.[1].toLowerCase();
|
|
14094
|
-
if (hm && (k === "
|
|
14357
|
+
if (hm && (k === "shell" || k === "agent" || k === "timer" || k === "pr" || k === "for" || k === "reason")) {
|
|
14095
14358
|
const value = hm[2].trim();
|
|
14096
14359
|
hints.push({ kind: k, value: value.length > HINT_VALUE_MAX ? value.slice(0, HINT_VALUE_MAX) : value });
|
|
14097
14360
|
} else {
|
|
14098
14361
|
rest.push(line);
|
|
14099
14362
|
}
|
|
14100
14363
|
}
|
|
14364
|
+
rest.push(...prose);
|
|
14101
14365
|
return { kind, body: capFenceBody(rest.join("\n").trim()), hints: hints.slice(0, HINT_MAX) };
|
|
14102
14366
|
}
|
|
14103
14367
|
function isRealUserMessage(content) {
|
|
@@ -14202,7 +14466,7 @@ function retireLive(state, entry, finishedAt, status) {
|
|
|
14202
14466
|
state.subAgents.delete(entry.toolUseId);
|
|
14203
14467
|
if (entry.kind === "shell") {
|
|
14204
14468
|
state.retiredShells.delete(entry.toolUseId);
|
|
14205
|
-
state.retiredShells.set(entry.toolUseId, { toolUseId: entry.toolUseId, command: entry.command, outputFile: entry.outputFile, status });
|
|
14469
|
+
state.retiredShells.set(entry.toolUseId, { toolUseId: entry.toolUseId, command: entry.command, outputFile: entry.outputFile, status, taskId: entry.taskId, label: entry.label, finishedAt });
|
|
14206
14470
|
while (state.retiredShells.size > RETAINED_SHELLS_MAX) {
|
|
14207
14471
|
const oldest = state.retiredShells.keys().next().value;
|
|
14208
14472
|
if (oldest === void 0) break;
|
|
@@ -14762,6 +15026,10 @@ function defaultBrokerDaemonAlive(project, now) {
|
|
|
14762
15026
|
return alive;
|
|
14763
15027
|
};
|
|
14764
15028
|
}
|
|
15029
|
+
function rowIsArchived(row) {
|
|
15030
|
+
if (row.state === "open" || row.state === "archived") return row.state === "archived";
|
|
15031
|
+
return row.archived === 1;
|
|
15032
|
+
}
|
|
14765
15033
|
function createTailer(deps) {
|
|
14766
15034
|
const now = deps.now ?? Date.now;
|
|
14767
15035
|
const paneDead = deps.paneDead ?? (() => true);
|
|
@@ -14977,10 +15245,17 @@ function createTailer(deps) {
|
|
|
14977
15245
|
for (const e of state.subAgents.values()) {
|
|
14978
15246
|
if (e.kind !== "shell") continue;
|
|
14979
15247
|
const lastActivityAt = entryLastActivity(e);
|
|
14980
|
-
|
|
15248
|
+
const since = Date.parse(lastActivityAt ?? e.startedAt);
|
|
15249
|
+
const shellState = Number.isFinite(since) && now() - since > BGSHELL_STALE_MS ? "stale" : "running";
|
|
15250
|
+
out.push({ label: e.label, startedAt: e.startedAt, state: shellState, id: e.toolUseId, ...e.taskId ? { stoppable: true, taskId: e.taskId } : {}, ...lastActivityAt ? { lastActivityAt } : {} });
|
|
14981
15251
|
}
|
|
14982
15252
|
return out;
|
|
14983
15253
|
}
|
|
15254
|
+
function retiredShellViews(state) {
|
|
15255
|
+
const out = [];
|
|
15256
|
+
for (const r of state.retiredShells.values()) out.push({ id: r.toolUseId, taskId: r.taskId, label: r.label, status: r.status, finishedAt: r.finishedAt });
|
|
15257
|
+
return out;
|
|
15258
|
+
}
|
|
14984
15259
|
function codexBgShellViews(state) {
|
|
14985
15260
|
if (!deps.codexBackgroundExecs || state.foreign || state.paneDead) return [];
|
|
14986
15261
|
const execs = deps.codexBackgroundExecs(state.slug, state.sessionId);
|
|
@@ -15373,7 +15648,7 @@ ${ask}`;
|
|
|
15373
15648
|
const next = items.filter((item) => item.state !== "pending" && item.state !== "enqueued");
|
|
15374
15649
|
return next.length === items.length ? items : next;
|
|
15375
15650
|
}
|
|
15376
|
-
function ledgerFold(row, nowMs) {
|
|
15651
|
+
function ledgerFold(row, nowMs, state) {
|
|
15377
15652
|
if (!row.delivery_ledger) {
|
|
15378
15653
|
return { finish: () => ({ changed: false, value: null }) };
|
|
15379
15654
|
}
|
|
@@ -15393,7 +15668,7 @@ ${ask}`;
|
|
|
15393
15668
|
},
|
|
15394
15669
|
finish: () => {
|
|
15395
15670
|
items = retireDeliveriesLostWithTheDaemon(row, items);
|
|
15396
|
-
items = ageDeliveries(items, nowMs);
|
|
15671
|
+
items = ageDeliveries(items, nowMs, state.lastUserAt);
|
|
15397
15672
|
if (items === before) return { changed: false, value: null };
|
|
15398
15673
|
const value = serializeDeliveryLedger(items);
|
|
15399
15674
|
deps.storage.setDeliveryLedger(row.slug, value);
|
|
@@ -15562,6 +15837,7 @@ ${ask}`;
|
|
|
15562
15837
|
}
|
|
15563
15838
|
function captureStall(state, row) {
|
|
15564
15839
|
if (state.stallLogged) return;
|
|
15840
|
+
if (row.exited && rowIsArchived(row)) return;
|
|
15565
15841
|
state.stallLogged = true;
|
|
15566
15842
|
let pane = "";
|
|
15567
15843
|
try {
|
|
@@ -15571,11 +15847,11 @@ ${ask}`;
|
|
|
15571
15847
|
}
|
|
15572
15848
|
const authFailure = row.backend !== "codex" && isClaudeAuthErrorText(pane);
|
|
15573
15849
|
if (authFailure) state.authFault = "authentication_rejected";
|
|
15574
|
-
const evidence = isBrokerClaudeRow(row) && deps.project.stateDir ? `no tmux pane (broker runtime). Daemon diagnostics: ${claudeBrokerDiagnosticLogPath(deps.project.stateDir, row.session_id)}` : isHeadlessRow(row) ? `no tmux pane (headless ${row.backend === "codex" ? "codex app-server" : "claude broker"} runtime)` : "";
|
|
15575
|
-
const detail = authFailure ? "(claude authentication failure \u2014 pane content redacted; sign in and retry)" : pane.trim() || evidence
|
|
15850
|
+
const evidence = isBrokerClaudeRow(row) && deps.project.stateDir ? `no tmux pane (broker runtime). Daemon diagnostics: ${claudeBrokerDiagnosticLogPath(deps.project.stateDir, row.session_id)}` : isHeadlessRow(row) ? `no tmux pane (headless ${row.backend === "codex" ? "codex app-server" : "claude broker"} runtime)` : "no pane captured (this build runs no tmux)";
|
|
15851
|
+
const detail = authFailure ? "(claude authentication failure \u2014 pane content redacted; sign in and retry)" : pane.trim() || evidence;
|
|
15576
15852
|
log.error(
|
|
15577
15853
|
"tailer",
|
|
15578
|
-
`thread ${row.slug} (session ${row.session_id}): no transcript ${DISCOVERY_GRACE_MS / 1e3}s after dispatch \u2014 likely a boot failure. ${
|
|
15854
|
+
`thread ${row.slug} (session ${row.session_id}): no transcript ${DISCOVERY_GRACE_MS / 1e3}s after dispatch \u2014 likely a boot failure. ${pane.trim() && !authFailure ? "Pane:\n" : ""}${detail.slice(0, 4e3)}`
|
|
15579
15855
|
);
|
|
15580
15856
|
try {
|
|
15581
15857
|
mkdirSync10(stallLogDir, { recursive: true });
|
|
@@ -15598,6 +15874,7 @@ ${detail}
|
|
|
15598
15874
|
if (size > 0) {
|
|
15599
15875
|
state.noTranscript = false;
|
|
15600
15876
|
state.stallLogged = false;
|
|
15877
|
+
state.discoverMisses = 0;
|
|
15601
15878
|
return true;
|
|
15602
15879
|
}
|
|
15603
15880
|
const spawnedMs = Date.parse(row.spawned_at);
|
|
@@ -15612,6 +15889,7 @@ ${detail}
|
|
|
15612
15889
|
state.primed = false;
|
|
15613
15890
|
state.noTranscript = false;
|
|
15614
15891
|
state.stallLogged = false;
|
|
15892
|
+
state.discoverMisses = 0;
|
|
15615
15893
|
return true;
|
|
15616
15894
|
}
|
|
15617
15895
|
const found = discoverTranscriptId(logDir, row.session_id, { nowMs, exclude: claimedIds(row.slug) });
|
|
@@ -15634,8 +15912,14 @@ ${detail}
|
|
|
15634
15912
|
state.primed = false;
|
|
15635
15913
|
state.noTranscript = false;
|
|
15636
15914
|
state.stallLogged = false;
|
|
15915
|
+
state.discoverMisses = 0;
|
|
15637
15916
|
return true;
|
|
15638
15917
|
}
|
|
15918
|
+
state.discoverMisses++;
|
|
15919
|
+
state.nextDiscoverMs = nowMs + Math.min(
|
|
15920
|
+
DISCOVER_RETRY_MS * 2 ** (state.discoverMisses - 1),
|
|
15921
|
+
DISCOVER_RETRY_MAX_MS
|
|
15922
|
+
);
|
|
15639
15923
|
state.noTranscript = true;
|
|
15640
15924
|
captureStall(state, row);
|
|
15641
15925
|
return true;
|
|
@@ -15647,6 +15931,15 @@ ${detail}
|
|
|
15647
15931
|
const nowMs = now();
|
|
15648
15932
|
adoptionBindings = /* @__PURE__ */ new Map();
|
|
15649
15933
|
const rows = deps.storage.allSessions();
|
|
15934
|
+
let coldVisibleRow = false;
|
|
15935
|
+
for (const row of rows) {
|
|
15936
|
+
if (rowIsArchived(row)) continue;
|
|
15937
|
+
const known = states.get(row.slug);
|
|
15938
|
+
if (!known || !known.primed) {
|
|
15939
|
+
coldVisibleRow = true;
|
|
15940
|
+
break;
|
|
15941
|
+
}
|
|
15942
|
+
}
|
|
15650
15943
|
let primed = 0;
|
|
15651
15944
|
let primedRows = 0;
|
|
15652
15945
|
const tickStartedMs = performance.now();
|
|
@@ -15672,13 +15965,19 @@ ${detail}
|
|
|
15672
15965
|
const prevNoTranscript = state.noTranscript;
|
|
15673
15966
|
if (row.backend !== "codex" && !resolveTranscript(state, row, nowMs)) continue;
|
|
15674
15967
|
if (!state.primed) {
|
|
15675
|
-
if (
|
|
15968
|
+
if (
|
|
15969
|
+
// Yield the slot to a still-cold visible row — but never at the cost of the forward-progress
|
|
15970
|
+
// guarantee below. `primedRows > 0` keeps the archive advancing by at least one row per tick
|
|
15971
|
+
// even if a visible row were somehow to stay cold indefinitely, so deferral can never become
|
|
15972
|
+
// starvation. The visible row still primes on this same tick; it just is not necessarily first.
|
|
15973
|
+
coldVisibleRow && primedRows > 0 && rowIsArchived(row) || primedRows >= MAX_PRIME_ROWS_PER_TICK || primedRows > 0 && performance.now() - tickStartedMs > PRIME_BUDGET_MS
|
|
15974
|
+
) {
|
|
15676
15975
|
primeIncomplete = true;
|
|
15677
15976
|
continue;
|
|
15678
15977
|
}
|
|
15679
15978
|
primedRows++;
|
|
15680
15979
|
const primeOffset = state.offset;
|
|
15681
|
-
const primeLedger = ledgerFold(row, nowMs);
|
|
15980
|
+
const primeLedger = ledgerFold(row, nowMs, state);
|
|
15682
15981
|
consume(state, backend, chainOnLine(primeLedger.onLine, codexSubAgentOnLine(row, state)));
|
|
15683
15982
|
state.codexSubAgents?.poll(nowMs);
|
|
15684
15983
|
const primedLedger = primeLedger.finish();
|
|
@@ -15734,7 +16033,7 @@ ${detail}
|
|
|
15734
16033
|
const prevTurn = state.turn;
|
|
15735
16034
|
const rowLedger = row.delivery_ledger ?? null;
|
|
15736
16035
|
const ledgerDrifted = rowLedger !== (state.deliveryLedgerSeen ?? null);
|
|
15737
|
-
const ledger = ledgerFold(row, nowMs);
|
|
16036
|
+
const ledger = ledgerFold(row, nowMs, state);
|
|
15738
16037
|
consume(state, backend, chainOnLine(ledger.onLine, codexSubAgentOnLine(row, state)));
|
|
15739
16038
|
state.codexSubAgents?.poll(nowMs);
|
|
15740
16039
|
const ledgerResult = ledger.finish();
|
|
@@ -15949,7 +16248,7 @@ ${detail}
|
|
|
15949
16248
|
if (!s) return void 0;
|
|
15950
16249
|
const pendingQuestion = s.turn === "idle" && s.lastAssistantHasQuestion;
|
|
15951
16250
|
const nowMs = now();
|
|
15952
|
-
return { turn: s.turn, permPrompt: s.permPrompt, permPolicy: s.permPolicy, permDenies: s.permDenies, nativeInputRequired: s.nativeInputRequired, model: s.model, effort: s.effort, profileAt: s.profileAt, profileRevision: s.profileRevision, permissionMode: s.permissionMode, permissionModeAt: s.permissionModeAt, permissionModeRevision: s.permissionModeRevision, lastActivityAt: s.lastActivityAt, lastAssistantAt: s.lastAssistantAt, lastAssistant: s.lastAssistant, aiTitle: s.aiTitle, customTitle: s.customTitle, customTitleRevision: s.customTitleRevision, subAgents: subAgentViews(s, nowMs), droppedReports: [...s.queuedReports.values()], bgShells: [...bgShellViews(s), ...codexBgShellViews(s)], pendingAsk: s.pendingAsk, pendingQuestion, lastAssistantAllDone: s.lastAssistantAllDone, lastUserAt: s.lastUserAt, lastUserText: s.lastUserText, lastFence: s.lastFence, noTranscript: s.noTranscript, authFault: s.authFault, limitFault: s.limitFault, contextTokens: s.contextTokens, contextWindow: s.contextWindow, lastCompactionAt: s.lastCompactionAt };
|
|
16251
|
+
return { turn: s.turn, permPrompt: s.permPrompt, permPolicy: s.permPolicy, permDenies: s.permDenies, nativeInputRequired: s.nativeInputRequired, model: s.model, effort: s.effort, profileAt: s.profileAt, profileRevision: s.profileRevision, permissionMode: s.permissionMode, permissionModeAt: s.permissionModeAt, permissionModeRevision: s.permissionModeRevision, lastActivityAt: s.lastActivityAt, lastAssistantAt: s.lastAssistantAt, lastAssistant: s.lastAssistant, aiTitle: s.aiTitle, customTitle: s.customTitle, customTitleRevision: s.customTitleRevision, subAgents: subAgentViews(s, nowMs), droppedReports: [...s.queuedReports.values()], bgShells: [...bgShellViews(s), ...codexBgShellViews(s)], retiredShells: retiredShellViews(s), pendingAsk: s.pendingAsk, pendingQuestion, lastAssistantAllDone: s.lastAssistantAllDone, lastUserAt: s.lastUserAt, lastUserText: s.lastUserText, lastFence: s.lastFence, noTranscript: s.noTranscript, authFault: s.authFault, limitFault: s.limitFault, contextTokens: s.contextTokens, contextWindow: s.contextWindow, lastCompactionAt: s.lastCompactionAt };
|
|
15953
16252
|
},
|
|
15954
16253
|
// The CURRENT fresh foreign session ids (mtime within FOREIGN_FRESH_MS, capped), mtime-desc. Kept
|
|
15955
16254
|
// as the last scan's result — recomputed at most every FOREIGN_SCAN_EVERY ticks.
|
|
@@ -16002,7 +16301,7 @@ function landsAfterRead(eventAt, lastReadAt) {
|
|
|
16002
16301
|
if (!Number.isFinite(e) || !Number.isFinite(r)) return true;
|
|
16003
16302
|
return e > r;
|
|
16004
16303
|
}
|
|
16005
|
-
var IDLE_BACKSTOP_MS, RUNTIME_CHASE_MAX, POLL_MS2, MAX_PRIME_ROWS_PER_TICK, PRIME_BUDGET_MS, TRANSCRIPT_READ_WINDOW, EMPTY_BUFFER, MAX_POLL_MS, SUBAGENT_STALE_MS, FOREIGN_FRESH_MS, FOREIGN_MAX, FOREIGN_SCAN_EVERY, CACHE_FLUSH_MS, PRIME_PROGRESS_EVERY, DISCOVER_RETRY_MS, PERM_YES_OPTION, PERM_QUESTION, PERM_FOOTER, PERM_COMPOSER_FOOTER, PERM_MODAL_TAIL_ROWS, PERM_COMPOSER_TAIL_ROWS, BOOT_APIKEY_HEADLINE, BOOT_APIKEY_QUESTION, BOOT_TRUST_QUESTION, BOOT_TRUST_OPTION, BOOT_MODAL_TAIL_ROWS, RETAINED_SUBAGENTS_MAX, RETAINED_SHELLS_MAX, DESCENDANT_TERMINALS_MAX, PENDING_RESUMES_MAX, PENDING_SHELLS_MAX, RESUME_REPLAY_SLACK_MS, QUESTION_BLOCK_RE, SIGNAL_FENCE_RE, AWAITING_HINT_RE, FENCE_BODY_MAX, HINT_MAX, HINT_VALUE_MAX, LAUNCH_ACK_RE, AUTO_BACKGROUND_ACK_RE, DESCENDANT_SIDECAR_MAX, DESCENDANT_NOTIFY_GRACE_MS, MAX_OWNED_TOOL_USE_IDS, BROKER_LIVENESS_TTL_MS, UNRESTORED_TAIL_FIELDS;
|
|
16304
|
+
var IDLE_BACKSTOP_MS, RUNTIME_CHASE_MAX, POLL_MS2, MAX_PRIME_ROWS_PER_TICK, PRIME_BUDGET_MS, TRANSCRIPT_READ_WINDOW, EMPTY_BUFFER, MAX_POLL_MS, SUBAGENT_STALE_MS, BGSHELL_STALE_MS, FOREIGN_FRESH_MS, FOREIGN_MAX, FOREIGN_SCAN_EVERY, CACHE_FLUSH_MS, PRIME_PROGRESS_EVERY, DISCOVER_RETRY_MS, DISCOVER_RETRY_MAX_MS, PERM_YES_OPTION, PERM_QUESTION, PERM_FOOTER, PERM_COMPOSER_FOOTER, PERM_MODAL_TAIL_ROWS, PERM_COMPOSER_TAIL_ROWS, BOOT_APIKEY_HEADLINE, BOOT_APIKEY_QUESTION, BOOT_TRUST_QUESTION, BOOT_TRUST_OPTION, BOOT_MODAL_TAIL_ROWS, RETAINED_SUBAGENTS_MAX, RETAINED_SHELLS_MAX, DESCENDANT_TERMINALS_MAX, PENDING_RESUMES_MAX, PENDING_SHELLS_MAX, RESUME_REPLAY_SLACK_MS, QUESTION_BLOCK_RE, SIGNAL_FENCE_RE, AWAITING_HINT_RE, FENCE_BODY_MAX, HINT_MAX, HINT_VALUE_MAX, LAUNCH_ACK_RE, AUTO_BACKGROUND_ACK_RE, DESCENDANT_SIDECAR_MAX, DESCENDANT_NOTIFY_GRACE_MS, MAX_OWNED_TOOL_USE_IDS, BROKER_LIVENESS_TTL_MS, UNRESTORED_TAIL_FIELDS;
|
|
16006
16305
|
var init_tailer = __esm({
|
|
16007
16306
|
"packages/server/src/tailer.ts"() {
|
|
16008
16307
|
"use strict";
|
|
@@ -16031,12 +16330,14 @@ var init_tailer = __esm({
|
|
|
16031
16330
|
EMPTY_BUFFER = Buffer.alloc(0);
|
|
16032
16331
|
MAX_POLL_MS = 1e4;
|
|
16033
16332
|
SUBAGENT_STALE_MS = 15 * 6e4;
|
|
16333
|
+
BGSHELL_STALE_MS = 12 * 60 * 6e4;
|
|
16034
16334
|
FOREIGN_FRESH_MS = 24 * 60 * 6e4;
|
|
16035
16335
|
FOREIGN_MAX = 20;
|
|
16036
16336
|
FOREIGN_SCAN_EVERY = 5;
|
|
16037
16337
|
CACHE_FLUSH_MS = 3e4;
|
|
16038
16338
|
PRIME_PROGRESS_EVERY = 20;
|
|
16039
16339
|
DISCOVER_RETRY_MS = 15e3;
|
|
16340
|
+
DISCOVER_RETRY_MAX_MS = 15 * 6e4;
|
|
16040
16341
|
PERM_YES_OPTION = /(^|\n)\s*(❯\s*)?1\.\s+Yes\b/;
|
|
16041
16342
|
PERM_QUESTION = /\b(?:Do you want|Would you like)\b/;
|
|
16042
16343
|
PERM_FOOTER = /\bEsc to (cancel|reject)\b/;
|
|
@@ -16056,7 +16357,7 @@ var init_tailer = __esm({
|
|
|
16056
16357
|
RESUME_REPLAY_SLACK_MS = 6e4;
|
|
16057
16358
|
QUESTION_BLOCK_RE = /^```question(?:[ \t]+[A-Za-z][^\r\n]*?)?[ \t]*\r?\n[\s\S]*?\r?\n```[ \t]*$/gm;
|
|
16058
16359
|
SIGNAL_FENCE_RE = /^```(done|awaiting)[ \t]*\n([\s\S]*?)\n```[ \t]*$/gm;
|
|
16059
|
-
AWAITING_HINT_RE = /^(
|
|
16360
|
+
AWAITING_HINT_RE = /^(shell|agent|timer|pr|for|reason):\s*(\S.*)$/i;
|
|
16060
16361
|
FENCE_BODY_MAX = 500;
|
|
16061
16362
|
HINT_MAX = 8;
|
|
16062
16363
|
HINT_VALUE_MAX = 200;
|
|
@@ -16089,6 +16390,7 @@ var init_tailer = __esm({
|
|
|
16089
16390
|
"permDenies",
|
|
16090
16391
|
"noTranscript",
|
|
16091
16392
|
"nextDiscoverMs",
|
|
16393
|
+
"discoverMisses",
|
|
16092
16394
|
"stallLogged",
|
|
16093
16395
|
"deliveryLedgerSeen",
|
|
16094
16396
|
// The chase bookkeeping is compared against an IN-MEMORY, per-process counter — the ingest's `live`
|
|
@@ -16451,8 +16753,8 @@ function parseCodexSubAgentLine(line, toolNameFor) {
|
|
|
16451
16753
|
if (pt === "function_call" && p.name === "spawn_agent") {
|
|
16452
16754
|
const args = parseToolArguments(p.arguments);
|
|
16453
16755
|
const a = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
16454
|
-
const
|
|
16455
|
-
return { kind: "spawn", at, callId, taskName:
|
|
16756
|
+
const str2 = (v) => typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
16757
|
+
return { kind: "spawn", at, callId, taskName: str2(a.task_name), model: str2(a.model), effort: str2(a.reasoning_effort), agentType: str2(a.agent_type) };
|
|
16456
16758
|
}
|
|
16457
16759
|
if (pt !== "function_call_output") return void 0;
|
|
16458
16760
|
const name = toolNameFor(callId);
|
|
@@ -18602,7 +18904,7 @@ var require_sender = __commonJS({
|
|
|
18602
18904
|
* @param {Function} [cb] Callback
|
|
18603
18905
|
* @private
|
|
18604
18906
|
*/
|
|
18605
|
-
getBlobData(blob,
|
|
18907
|
+
getBlobData(blob, compress2, options, cb) {
|
|
18606
18908
|
this._bufferedBytes += options[kByteLength];
|
|
18607
18909
|
this._state = GET_BLOB_DATA;
|
|
18608
18910
|
blob.arrayBuffer().then((arrayBuffer) => {
|
|
@@ -18615,12 +18917,12 @@ var require_sender = __commonJS({
|
|
|
18615
18917
|
}
|
|
18616
18918
|
this._bufferedBytes -= options[kByteLength];
|
|
18617
18919
|
const data = toBuffer(arrayBuffer);
|
|
18618
|
-
if (!
|
|
18920
|
+
if (!compress2) {
|
|
18619
18921
|
this._state = DEFAULT;
|
|
18620
18922
|
this.sendFrame(_Sender.frame(data, options), cb);
|
|
18621
18923
|
this.dequeue();
|
|
18622
18924
|
} else {
|
|
18623
|
-
this.dispatch(data,
|
|
18925
|
+
this.dispatch(data, compress2, options, cb);
|
|
18624
18926
|
}
|
|
18625
18927
|
}).catch((err) => {
|
|
18626
18928
|
process.nextTick(onError, this, err, cb);
|
|
@@ -18649,8 +18951,8 @@ var require_sender = __commonJS({
|
|
|
18649
18951
|
* @param {Function} [cb] Callback
|
|
18650
18952
|
* @private
|
|
18651
18953
|
*/
|
|
18652
|
-
dispatch(data,
|
|
18653
|
-
if (!
|
|
18954
|
+
dispatch(data, compress2, options, cb) {
|
|
18955
|
+
if (!compress2) {
|
|
18654
18956
|
this.sendFrame(_Sender.frame(data, options), cb);
|
|
18655
18957
|
return;
|
|
18656
18958
|
}
|
|
@@ -24857,20 +25159,64 @@ function parsePrRef(value) {
|
|
|
24857
25159
|
return { owner: m[1], repo: m[2].replace(/\.git$/, ""), number };
|
|
24858
25160
|
}
|
|
24859
25161
|
function isActionableAwaitingHint(hint) {
|
|
24860
|
-
|
|
24861
|
-
|
|
24862
|
-
|
|
24863
|
-
|
|
25162
|
+
return hint !== void 0 && false;
|
|
25163
|
+
}
|
|
25164
|
+
function readAwaitingPark(hints) {
|
|
25165
|
+
const items = [];
|
|
25166
|
+
let forMs = null;
|
|
25167
|
+
let reason = "";
|
|
25168
|
+
for (const h of hints) {
|
|
25169
|
+
const value = h.value.trim();
|
|
25170
|
+
if (isAwaitingItemKind(h.kind)) {
|
|
25171
|
+
if (value) items.push({ kind: h.kind, value });
|
|
25172
|
+
} else if (h.kind === "for") {
|
|
25173
|
+
forMs = parseAwaitingDuration(value);
|
|
25174
|
+
} else if (h.kind === "reason" && !reason) {
|
|
25175
|
+
reason = value;
|
|
25176
|
+
}
|
|
25177
|
+
}
|
|
25178
|
+
return { items, forMs, reason };
|
|
25179
|
+
}
|
|
25180
|
+
function unaccountedItems(items, live) {
|
|
25181
|
+
return items.filter((i) => !live[LIVE_SET[i.kind]].has(i.value));
|
|
25182
|
+
}
|
|
25183
|
+
function parkIsHonoured(park, live) {
|
|
25184
|
+
if (park.items.length === 0) return false;
|
|
25185
|
+
if (park.forMs === null) return false;
|
|
25186
|
+
return unaccountedItems(park.items, live).length === 0;
|
|
25187
|
+
}
|
|
25188
|
+
function parkExpiresAt(park, fenceAtMs) {
|
|
25189
|
+
if (park.forMs === null || !Number.isFinite(fenceAtMs)) return null;
|
|
25190
|
+
return fenceAtMs + Math.min(park.forMs, AWAITING_FOR_MAX_MS);
|
|
24864
25191
|
}
|
|
24865
25192
|
function awaitingFenceIdentity(hint, fenceAt) {
|
|
24866
25193
|
return createHash11("sha256").update(fenceAt).update("\0").update(hint.kind).update("\0").update(hint.value).digest("hex");
|
|
24867
25194
|
}
|
|
24868
|
-
|
|
25195
|
+
function readGithubStatusBook(raw2) {
|
|
25196
|
+
if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) return {};
|
|
25197
|
+
const out = {};
|
|
25198
|
+
for (const [key, value] of Object.entries(raw2)) {
|
|
25199
|
+
const parsed = GithubWatchStatus.safeParse(value);
|
|
25200
|
+
if (parsed.success) out[key] = parsed.data;
|
|
25201
|
+
}
|
|
25202
|
+
return out;
|
|
25203
|
+
}
|
|
25204
|
+
function githubStatusKey(ref) {
|
|
25205
|
+
return `${ref.owner}/${ref.repo}#${ref.number}`;
|
|
25206
|
+
}
|
|
25207
|
+
var PR_REF_RE, LIVE_SET, GITHUB_STATUS_SETTING;
|
|
24869
25208
|
var init_awaiting = __esm({
|
|
24870
25209
|
"packages/server/src/awaiting.ts"() {
|
|
24871
25210
|
"use strict";
|
|
24872
25211
|
init_src();
|
|
24873
25212
|
PR_REF_RE = /(?:https?:\/\/github\.com\/)?([A-Za-z0-9][\w.-]*)\/([A-Za-z0-9][\w.-]*?)(?:\/pull\/|\/pulls\/|#)(\d+)/;
|
|
25213
|
+
LIVE_SET = {
|
|
25214
|
+
shell: "shells",
|
|
25215
|
+
agent: "agents",
|
|
25216
|
+
timer: "timers",
|
|
25217
|
+
pr: "prs"
|
|
25218
|
+
};
|
|
25219
|
+
GITHUB_STATUS_SETTING = "waker.github.status.v1";
|
|
24874
25220
|
}
|
|
24875
25221
|
});
|
|
24876
25222
|
|
|
@@ -24971,11 +25317,12 @@ triage: it says neither "answer me" nor "this is finished", so it sits in the qu
|
|
|
24971
25317
|
Frizz corrects it, twice, and then stops: rest without a fence and you will be told to pick one, which
|
|
24972
25318
|
costs you a turn each time.
|
|
24973
25319
|
|
|
24974
|
-
Pick \`question\` when you need the human, \`done\` when the work is genuinely finished, and
|
|
24975
|
-
|
|
24976
|
-
|
|
24977
|
-
|
|
24978
|
-
|
|
25320
|
+
Pick \`question\` when you need the human, \`done\` when the work is genuinely finished, and \`awaiting\`
|
|
25321
|
+
when you are WAITING \u2014 on a PR, or on your own background work \u2014 naming what you wait on, one line each.
|
|
25322
|
+
|
|
25323
|
+
\`awaiting\` REGISTERS NOTHING. It is how you come to rest without being bumped for a handoff, and how
|
|
25324
|
+
the human sees what you are waiting on. Your background shells and sub-agents are watched AUTOMATICALLY,
|
|
25325
|
+
fence or no fence: frizz wakes you when one finishes, every time.
|
|
24979
25326
|
|
|
24980
25327
|
**WRITE IT TO BE READ COLD.** The human reads your final message in a queue, hours later, with none of
|
|
24981
25328
|
your context \u2014 and they have NOT seen anything since their own last message. Every prompt you received
|
|
@@ -24986,6 +25333,16 @@ you would do next \u2014 rather than only the last thing you touched. Do not ass
|
|
|
24986
25333
|
Be concise, but do not omit. Use whatever markup makes it readable: headings, bullets, tables, code.
|
|
24987
25334
|
The one place brevity is a RULE is a \`done\` body \u2014 see its entry below.
|
|
24988
25335
|
|
|
25336
|
+
**NAME THE PULL REQUEST, EVERY TIME YOU REST.** If your effort has a PR \u2014 one you opened, one you are
|
|
25337
|
+
reviewing, one you are fixing \u2014 write its number in EVERY resting message, not just the one where it
|
|
25338
|
+
first appeared. The human reads these as a queue of cards from a dozen threads at once, and a card that
|
|
25339
|
+
says "pushed the fix, CI is green" without a number is a card they cannot place: they have to open the
|
|
25340
|
+
thread to learn which PR it was about (maintainer 2026-08-16: "I keep being unclear what PR is being
|
|
25341
|
+
implemented in a given chat"). One \`#123\` in the first line or two costs you nothing and answers it.
|
|
25342
|
+
|
|
25343
|
+
Several PRs in play \u21D2 name them all. No PR yet \u21D2 say what the work is against instead (a branch, an
|
|
25344
|
+
issue, a file) \u2014 the point is that the card identifies its own subject without being opened.
|
|
25345
|
+
|
|
24989
25346
|
Frizz turns \`#123\`, \`owner/repo#123\` and bare commit hashes into github.com links itself, in every
|
|
24990
25347
|
surface it draws your prose, for any project whose origin remote is github.com. Write the bare form;
|
|
24991
25348
|
wrapping one in a \`[link](url)\` yourself is redundant. Everything else you want clickable \u2014 a file, a
|
|
@@ -24995,8 +25352,14 @@ Use at most ONE fenced signal block, at the very END of the final message. The f
|
|
|
24995
25352
|
state; the body is the card the human reads. A \` \`\`\`question \` block and real permission prompts are
|
|
24996
25353
|
higher-priority asks than either fence below.
|
|
24997
25354
|
|
|
25355
|
+
**THE FENCE AND THE PROSE ABOVE IT ARE TWO SURFACES, NOT ONE MESSAGE WRITTEN TWICE.** The card is the
|
|
25356
|
+
LEDGER: one bullet per deliverable, what shipped and where. The prose is only what a ledger cannot hold
|
|
25357
|
+
\u2014 the reasoning, the caveat, the thing the human has to do. If a sentence would read the same in either,
|
|
25358
|
+
it belongs in exactly ONE of them. Writing the summary and then restating it as bullets costs the human
|
|
25359
|
+
a second read for nothing, and it is the commonest thing wrong with an otherwise good handoff.
|
|
25360
|
+
|
|
24998
25361
|
- \` \`\`\`done \` \u2014 you COMPLETED the effort's real work: code LANDED on the project's mainline, a plan or
|
|
24999
|
-
doc written, or a commissioned research/audit report finished. Body: ONE TO THREE SENTENCES, then a
|
|
25362
|
+
doc written, or a commissioned research/audit report finished INTO A FILE. Body: ONE TO THREE SENTENCES, then a
|
|
25000
25363
|
BULLET LIST, one \`- \` item per task, each opening with a **bolded verb phrase** and naming what
|
|
25001
25364
|
shipped and where. This is the ONE place the short shape is a rule \u2014 a dismissal card is scanned, not
|
|
25002
25365
|
read, so it is a ledger. Everywhere else, say as much as the reader needs. The card renders inline markdown, so backtick every path,
|
|
@@ -25011,62 +25374,151 @@ higher-priority asks than either fence below.
|
|
|
25011
25374
|
\`\`\`
|
|
25012
25375
|
|
|
25013
25376
|
**\`done\` is a DISMISSAL, not a summary** \u2014 its card files the thread away where nobody looks again,
|
|
25014
|
-
and anything living only in the conversation goes with it.
|
|
25015
|
-
|
|
25016
|
-
|
|
25017
|
-
|
|
25018
|
-
|
|
25019
|
-
|
|
25020
|
-
|
|
25021
|
-
|
|
25022
|
-
|
|
25023
|
-
|
|
25024
|
-
|
|
25025
|
-
|
|
25026
|
-
|
|
25027
|
-
|
|
25028
|
-
|
|
25029
|
-
|
|
25030
|
-
|
|
25031
|
-
|
|
25032
|
-
|
|
25033
|
-
|
|
25034
|
-
|
|
25035
|
-
|
|
25036
|
-
|
|
25037
|
-
|
|
25038
|
-
|
|
25039
|
-
|
|
25040
|
-
|
|
25041
|
-
|
|
25042
|
-
|
|
25043
|
-
|
|
25044
|
-
|
|
25045
|
-
|
|
25046
|
-
|
|
25047
|
-
|
|
25048
|
-
|
|
25049
|
-
|
|
25377
|
+
and anything living only in the conversation goes with it. So the test is never "have I stopped
|
|
25378
|
+
working": it is **WHAT IS LOST IF NOBODY EVER OPENS THIS THREAD AGAIN?** Name one thing and it is not
|
|
25379
|
+
done. If the thread points at future work AT ALL, you are not done \u2014 see **When the work is finished
|
|
25380
|
+
but the thread found more** for the ways out of that, none of which is stretching \`done\`. Uncertain
|
|
25381
|
+
is not done.
|
|
25382
|
+
|
|
25383
|
+
**A RECOMMENDATION IS NOT A CONCLUSION, AND AN UNSENT DRAFT IS NOT A DELIVERABLE.** When the verdict
|
|
25384
|
+
is that SOMEONE SHOULD NOW DO SOMETHING \u2014 merge it, decline it, post this comment, pick one of these
|
|
25385
|
+
two designs, press the button you are not allowed to press \u2014 that someone is the human, so the effort
|
|
25386
|
+
ends on a \` \`\`\`question \` carrying your recommendation as option A. Same for anything you WROTE but
|
|
25387
|
+
did not SEND: a drafted comment, reply, issue body or release note you were right not to publish
|
|
25388
|
+
unilaterally is one click short of delivered, and \`done\` files that draft away with the thread.
|
|
25389
|
+
|
|
25390
|
+
**A \`done\` MESSAGE SAYS WHAT HAPPENED. DELETE EVERY DANGLING "WORTH DOING LATER".** "One thing to
|
|
25391
|
+
carry forward\u2026", "a follow-up could\u2026", "someone should add a changelog line before this ships" \u2014 a
|
|
25392
|
+
forward-reference parked in a dismissal card is clutter: too weak for anyone to act on, too present to
|
|
25393
|
+
ignore, and archived unread. Each one resolves four ways and there is no fifth \u2014 DO it, SPAWN it onto
|
|
25394
|
+
its own card, ASK about it, or DROP it. Something the human must do NOW ("re-pull before you restart")
|
|
25395
|
+
is not a dangling idea; that is the handoff, and it belongs in the prose.
|
|
25396
|
+
|
|
25397
|
+
What blocks \`done\` is work still OWED \u2014 by you, or by the human because of what you found \u2014 not a
|
|
25398
|
+
BACKGROUND PROCESS that happens to still be running: a watcher, dev server, or poller you have already
|
|
25399
|
+
moved on from does not hold it back \u2014 name it in the body and fence anyway. **Read that carve-out
|
|
25400
|
+
narrowly: its subject is a running process, and nothing else.** Follow-up work you DISCOVERED blocks
|
|
25401
|
+
\`done\` just as hard as work you were assigned, even when it is someone else's to do \u2014 "not mine" is
|
|
25402
|
+
not "not owed", because the card takes the finding with it either way. A \`done\` fence REPLACES the
|
|
25403
|
+
awaiting-background card with your own completion card, which is what turns a finished thread with a
|
|
25404
|
+
live watcher into a one-click dismissal instead of a card the human has to keep deciding about. Code
|
|
25405
|
+
written but not LANDED is not done (a commit, a pushed branch, an open PR is work still ahead of the
|
|
25406
|
+
merge \u2014 where the project uses PRs, \`done\` waits for the MERGE, so park the PR on \` \`\`\`awaiting \`
|
|
25407
|
+
with \`pr:\`). An investigation headed for a fix is NOT \`done\`; the fix is still owed. Two cases
|
|
25408
|
+
DO earn \`done\` on something other than landed code, and BOTH earn it the same way \u2014 by passing the
|
|
25409
|
+
test above, because the deliverable is a FILE the human opens without this thread: a commissioned
|
|
25410
|
+
research or audit EFFORT, whose finished report is written to that file, earns \`done\`; and so does a
|
|
25411
|
+
PLANNING session whose plan file is FULLY written and PERSISTED (\`.frizz/plans/<topic>.md\`), because
|
|
25412
|
+
that artifact already lives outside the thread, so dismissing the thread loses nothing. Neither
|
|
25413
|
+
exception stretches to a report that ENDS IN A DECISION the human has yet to make: write the file AND
|
|
25414
|
+
ask the question.
|
|
25415
|
+
|
|
25416
|
+
- \` \`\`\`awaiting \` \u2014 you have STOPPED, and you are waiting on work that is actually running. The
|
|
25417
|
+
fence is PURE STRUCTURE: a list of things frizz can look up, how long the park may stand, and one line
|
|
25418
|
+
of prose. Nothing else belongs in it.
|
|
25050
25419
|
|
|
25051
25420
|
\`\`\`awaiting
|
|
25052
|
-
|
|
25053
|
-
|
|
25421
|
+
shell: bzvtnt3ig
|
|
25422
|
+
agent: <the runtime agent id>
|
|
25423
|
+
timer: tmr_a1b2c3d4e5f6
|
|
25424
|
+
pr: acme/app#391
|
|
25425
|
+
for: 2h
|
|
25426
|
+
---
|
|
25427
|
+
Waiting on the three-platform run before porting the v2 drivers.
|
|
25428
|
+
|
|
25429
|
+
- the macOS leg is the one that has been flaky
|
|
25430
|
+
- if it goes red I will bisect rather than re-run
|
|
25054
25431
|
\`\`\`
|
|
25055
25432
|
|
|
25056
|
-
|
|
25057
|
-
|
|
25433
|
+
**FRONTMATTER, THEN MARKDOWN.** The structural lines come first; a \`---\` line ends them and everything
|
|
25434
|
+
after it is ORDINARY MARKDOWN \u2014 as much as you want, with paragraphs, lists and links. That prose is what
|
|
25435
|
+
the human reads on your resting card, so write it as a handoff, not as a label.
|
|
25436
|
+
|
|
25437
|
+
Omit the \`---\` and the whole fence is structure. A single \`reason: <one line>\` line also still works, but
|
|
25438
|
+
prefer the delimiter: a handoff is prose, and one line is not enough room for one.
|
|
25439
|
+
|
|
25058
25440
|
|
|
25059
|
-
|
|
25060
|
-
|
|
25061
|
-
|
|
25441
|
+
- \`shell:\` / \`agent:\` \u2014 the runtime id you were handed when you launched it ("Command running in
|
|
25442
|
+
background with ID: bzvtnt3ig").
|
|
25443
|
+
|
|
25444
|
+
**LOST AN ID? \`mcp__frizz__activity\` PRINTS THEM ALL** \u2014 every shell, sub-agent, timer and PR you have
|
|
25445
|
+
out, each with the exact line your fence needs. Call it rather than guessing: a guessed id names
|
|
25446
|
+
nothing, so frizz refuses the park and bumps you.
|
|
25447
|
+
- \`timer:\` \u2014 a timer id from \`mcp__frizz__timer\`. NOT an instant; the tool creates the timer.
|
|
25448
|
+
- \`pr:\` \u2014 a pull request you REGISTERED with \`mcp__frizz__watch_pr\`, written \`owner/repo#N\`. Register
|
|
25449
|
+
FIRST: this line states the wait, it does not create one. That tool wakes you on CI turning green or red,
|
|
25450
|
+
and on every later review, approval or comment, human or bot, for as long as the PR lives.
|
|
25451
|
+
- \`for:\` \u2014 **REQUIRED**, and a DURATION: \`30s\`, \`15m\`, \`2h\`, \`3d\`. Never an instant. When it runs
|
|
25452
|
+
out frizz brings you back to re-check everything; re-parking is fine and uncapped.
|
|
25453
|
+
- \`reason:\` \u2014 a one-line alternative to the \`---\` block above, kept for fences already written
|
|
25454
|
+
that way. Prefer the delimiter.
|
|
25455
|
+
|
|
25456
|
+
**REGISTERING IS NOT PARKING, AND PARKING IS NOT REGISTERING.** Launching a shell, dispatching a
|
|
25457
|
+
sub-agent, setting a timer, registering a PR watcher \u2014 those just happen, and frizz watches them
|
|
25458
|
+
whether or not you ever write a fence. This fence is only how you declare that you have STOPPED, and
|
|
25459
|
+
name which of them you stopped for.
|
|
25460
|
+
|
|
25461
|
+
**A FENCE THAT NAMES NOTHING IS NOT A PARK.** \`for:\` and \`reason:\` describe a wait; they are not one.
|
|
25462
|
+
If you are waiting on CI, REGISTER the PR and name it \u2014 do not write "waiting on the checks" and a
|
|
25463
|
+
duration, because nothing will wake you and your thread just sits in the queue. And if you are not
|
|
25464
|
+
actually waiting on anything, you are not awaiting: end with \`\`\`done, or ask a \`\`\`question.
|
|
25465
|
+
|
|
25466
|
+
**FRIZZ CHECKS EVERY NAME THE MOMENT YOU REST.** All of them live \u21D2 your thread parks in the dimmed
|
|
25467
|
+
Held band. Any one of them dead, unknown, or not yours \u21D2 you are BUMPED immediately and told which.
|
|
25468
|
+
A fence naming nothing at all is not a park either \u2014 it is a claim to be waiting with no way to be
|
|
25469
|
+
woken, which is the exact failure this grammar exists to make impossible.
|
|
25470
|
+
|
|
25471
|
+
**WAITING ON A PERSON IS A \` \`\`\`question \`.** There is no human gate and there is no prose park.
|
|
25472
|
+
If you need a human, ask them something answerable.
|
|
25473
|
+
|
|
25474
|
+
**CI, RELEASES, DEPLOYS AND MERGE PROGRESSION ARE AUTOMATABLE \u2014 never \` \`\`\`awaiting \` them
|
|
25475
|
+
BLINDLY.** For a PULL REQUEST that is what \`mcp__frizz__watch_pr\` is for: register it and you are told
|
|
25476
|
+
when CI settles, so the wait is real rather than a hope. For anything else, stay ACTIVE \u2014 dispatch a
|
|
25477
|
+
sub-agent to own the wait (its return re-invokes you), or set a timer and name it here.
|
|
25478
|
+
|
|
25479
|
+
**THE PARK SURVIVES A RESTART.** The registration and the fence are both durable, so frizz brings you
|
|
25480
|
+
back (\`{{FRIZZ_RESUME_CMD}}\`) even if its own server or your daemon is replaced while you wait.
|
|
25062
25481
|
|
|
25063
25482
|
**A follow-up clears the previous fence.** If the human says "back to awaiting", never answer that it
|
|
25064
|
-
is already parked and never rely on the old fence: re-check
|
|
25065
|
-
|
|
25066
|
-
|
|
25483
|
+
is already parked and never rely on the old fence: re-check what you are waiting on, confirm it is
|
|
25484
|
+
still running, and emit a FRESH fence naming it. If it turns out to be automatable, arm the active
|
|
25485
|
+
wait instead and do not fence.
|
|
25067
25486
|
|
|
25068
25487
|
- \` \`\`\`question \` \u2014 you need the human's input; see **Questions for the human**.
|
|
25069
25488
|
|
|
25489
|
+
### When the work is finished but the thread found more
|
|
25490
|
+
|
|
25491
|
+
Your mandate is complete and you turned up something else: a bug you were not sent for, a fix nobody
|
|
25492
|
+
landed, a follow-up the change implies. \`done\` is barred \u2014 the finding dies with the card. \`awaiting\`
|
|
25493
|
+
is barred \u2014 nothing is running. \`question\` looks barred \u2014 the human asked nothing. **That deadlock is
|
|
25494
|
+
not real, and stretching \`done\` is not how you break it.** Take the first exit that fits:
|
|
25495
|
+
|
|
25496
|
+
1. **DO IT \u2014 the default, and it covers far more than "small".** Anything inside your change's own blast
|
|
25497
|
+
radius is exit 1: the fix your change implies, the test it needs, the breakage it revealed, the caller
|
|
25498
|
+
it left stale. **Size is not what disqualifies it \u2014 a big job you can finish is still exit 1.** If it
|
|
25499
|
+
is more than one sitting, dispatch an in-session SUB-AGENT: its result comes BACK to you, so the
|
|
25500
|
+
follow-up lands on YOUR card, in your diff, under one review. One thread that finished two things
|
|
25501
|
+
beats two threads that each finished one and never learned of the other. Then \`done\` is honest with
|
|
25502
|
+
nothing left over.
|
|
25503
|
+
2. **ASK** \u2014 the human should choose whether or how it happens. An ordinary \` \`\`\`question \`; "nothing
|
|
25504
|
+
was pending" is not an objection. This is the exit for work you cannot bound, cannot verify, or that
|
|
25505
|
+
is genuinely the human's call \u2014 and it is where a separable effort goes, as an OPTION they pick,
|
|
25506
|
+
rather than a card you create for them.
|
|
25507
|
+
3. **HAND IT OFF TO ITS OWN CARD \u2014 the LAST resort, not the tidy one.** \`mcp__frizz__spawn_thread\` puts
|
|
25508
|
+
a separable effort on the board as its own thread; put the returned \`[title](/thread/<slug>)\` link in
|
|
25509
|
+
the body. **A spawned thread is FIRE-AND-FORGET: it reports to the HUMAN, never to you, so nothing it
|
|
25510
|
+
learns can ever reach this effort or its siblings.** That is the whole cost, and it is not
|
|
25511
|
+
theoretical \u2014 one thread here spawned four, three of those spawned more, and three separate
|
|
25512
|
+
descendants independently rediscovered the SAME root-cause commit over twenty hours because none of
|
|
25513
|
+
them could tell the others. Reach for it only when the work genuinely cannot ride on your card: a
|
|
25514
|
+
different repo, a different long-lived runtime, an effort that must outlive yours. If a sub-agent
|
|
25515
|
+
could do it, exit 1. If the human should choose, exit 2. Never spawn merely to clear your own \`done\`.
|
|
25516
|
+
4. **DROP IT** \u2014 it is not worth an effort, a card, or a question. Then it is not worth a SENTENCE
|
|
25517
|
+
either: delete it. Do not park it in the handoff as "one thing to carry forward".
|
|
25518
|
+
|
|
25519
|
+
A bare rest is the residual, not a plan \u2014 legitimate only when nothing above fits, and frizz will ask
|
|
25520
|
+
you twice for a fence before it gives up.
|
|
25521
|
+
|
|
25070
25522
|
A mid-conversation turn carries NO fence. Nor is a turn on a thread that still points at future work \u2014
|
|
25071
25523
|
a live code-change discussion above all \u2014 ever \`done\`.`;
|
|
25072
25524
|
AGENT_COMPLETION = `## Agent completion invariant
|
|
@@ -25165,6 +25617,23 @@ Three specific traps, each of which has ended a turn that should have continued:
|
|
|
25165
25617
|
- **Pre-emptively worrying about context budget.** You do not get to stop early because the turn feels
|
|
25166
25618
|
long. If context genuinely runs out, the harness summarizes and you continue.
|
|
25167
25619
|
|
|
25620
|
+
**AND THE INSTRUCTION IS ALSO THE CEILING.** Everything above pushes you not to stop early; none of it
|
|
25621
|
+
is a licence to grow the job. "Keep going" has no upper bound of its own \u2014 there is always more to do
|
|
25622
|
+
in any repository \u2014 so a worker that may not stop until nothing is left can only ever stop by widening
|
|
25623
|
+
what it was asked. Finish the mandate you were given, and nothing else.
|
|
25624
|
+
|
|
25625
|
+
- **Work you notice on the way is a FINDING, not a task.** The bug beside the one you were sent for,
|
|
25626
|
+
the refactor the code obviously wants, the second issue the first one touches: name each in one line
|
|
25627
|
+
in your handoff and let the human dispatch it. Widening is not thoroughness \u2014 it buries the answer
|
|
25628
|
+
they asked for under changes they now have to review, and it spends hours they did not agree to.
|
|
25629
|
+
- **If the deliverable is a DOCUMENT, the document is the ending.** A triage, a review, an
|
|
25630
|
+
investigation, a recommendation, a plan \u2014 when that is what you were asked for, the finished
|
|
25631
|
+
write-up IS the work. Implementing what it proposes is the NEXT job, and it is not yours unless you
|
|
25632
|
+
were asked.
|
|
25633
|
+
- **An unanswered question is not permission to build the answer.** If you asked and nobody has
|
|
25634
|
+
replied, decide what the task itself needs and carry on INSIDE it. Do not treat the silence as
|
|
25635
|
+
authorization for the work the answer would have unlocked.
|
|
25636
|
+
|
|
25168
25637
|
**Your default is to DECIDE.** A reversible call costs minutes to redo; a round-trip to the human costs
|
|
25169
25638
|
hours \u2014 they may not read your question for hours, and the whole effort sits idle until they do. If it
|
|
25170
25639
|
is derivable from the code, the conventions, or ordinary engineering judgment, it is yours: make it,
|
|
@@ -25173,12 +25642,20 @@ say which way you went, and keep moving.
|
|
|
25173
25642
|
**The test that catches almost every bad question: if you are about to mark one option
|
|
25174
25643
|
\`(recommended)\`, you already know the answer \u2014 so implement it instead of asking.** Writing a
|
|
25175
25644
|
confident rationale for your own recommendation is proof the decision was yours to make. The same goes
|
|
25176
|
-
for "want me to fix it?" on a bug you were dispatched to fix
|
|
25645
|
+
for "want me to fix it?" on a bug you were dispatched to fix, "should I also
|
|
25177
25646
|
handle X?" where X is obviously in scope, and any question about a name, a default value, a file
|
|
25178
25647
|
location, an error message, a flag spelling, or which of two equivalent designs to use. Those are
|
|
25179
|
-
granular implementation calls.
|
|
25648
|
+
granular implementation calls. (A bug you merely NOTICED is not a question either \u2014 by the ceiling
|
|
25649
|
+
above it is a line in your handoff, not work to adopt and not permission to seek.) Make them, note the
|
|
25650
|
+
notable ones in your handoff, and move on \u2014 a
|
|
25180
25651
|
decision the human can see and reverse in a line of code is not worth an hours-long stall.
|
|
25181
25652
|
|
|
25653
|
+
**That test inverts when knowing the answer and being ABLE TO ACT ON IT come apart.** Where the act is
|
|
25654
|
+
one you are not permitted or not able to perform \u2014 a read-only boundary, a comment that goes out under
|
|
25655
|
+
the human's name, a merge, a close, a publish, a spend \u2014 your recommendation is not a decision you take
|
|
25656
|
+
silently. It becomes the QUESTION, with the recommendation as option A and the act spelled out concretely
|
|
25657
|
+
enough to approve in one word. Never resolve that fork by fencing \` \`\`\`done \` on the investigation.
|
|
25658
|
+
|
|
25182
25659
|
Stop only when a wrong guess would be BOTH costly AND hard to undo:
|
|
25183
25660
|
|
|
25184
25661
|
- destructive or irreversible actions (history rewrite, data loss, force-merge, a published release);
|
|
@@ -25471,8 +25948,9 @@ When delegation is explicitly authorized:
|
|
|
25471
25948
|
Keep automatable waits inside the active turn through the selected persistent \`exec_command\` /
|
|
25472
25949
|
\`write_stdin\` monitor session until it reaches a terminal condition. Then diagnose/fix/retry/merge as
|
|
25473
25950
|
authorized. Do not emit \`awaiting\` for CI,
|
|
25474
|
-
automated review, release, or merge progression. Those tool sessions are process-bound;
|
|
25475
|
-
|
|
25951
|
+
automated review, release, or merge progression. Those tool sessions are process-bound; set a durable
|
|
25952
|
+
timer with \`mcp__frizz__timer\` and name it in your fence only when the next check belongs at a later
|
|
25953
|
+
wall-clock time. A partial
|
|
25476
25954
|
\`gh pr checks\` rollup is not a CI-green verdict: inspect workflow runs for the exact PR head too, and
|
|
25477
25955
|
treat \`ACTION_REQUIRED\` fork gates as pending. When no valid project monitor is declared, run the
|
|
25478
25956
|
bundled \`ci-watch.mjs\` / \`review-watch.mjs\` named above instead of inventing a loop of your own.
|
|
@@ -25497,7 +25975,7 @@ So when a wait is longer than a single yield budget:
|
|
|
25497
25975
|
3. Drain each yield fully before the next one, and stop as soon as the result is terminal.
|
|
25498
25976
|
|
|
25499
25977
|
If you find yourself on the third identical poll with no new output, you are in this anti-pattern:
|
|
25500
|
-
switch to a blocking monitor, or
|
|
25978
|
+
switch to a blocking monitor, or set a timer with \`mcp__frizz__timer\` for a genuinely distant check.
|
|
25501
25979
|
|
|
25502
25980
|
When you genuinely need to work alongside a disposable local process (a dev server or long gate), use
|
|
25503
25981
|
the managed unified-exec handoff: create the \`tools.exec_command(...)\` promise, call
|
|
@@ -25512,8 +25990,9 @@ the cell can wake a rested turn.
|
|
|
25512
25990
|
|
|
25513
25991
|
Never implement a recurring wake as a shell \`sleep\` loop. Shell output cannot create a new Codex turn,
|
|
25514
25992
|
so an unattended loop is not a wake mechanism. When a later wall-clock check genuinely belongs after a
|
|
25515
|
-
rest,
|
|
25516
|
-
new turn
|
|
25993
|
+
rest, set one with \`mcp__frizz__timer\` and name the id it returns on a \`timer:\` line in your
|
|
25994
|
+
\`\`\`awaiting fence; Frizz's durable scheduler starts the new turn then. Each timer is one-shot \u2014 set
|
|
25995
|
+
several if you need several, and do not try to make one recur.
|
|
25517
25996
|
|
|
25518
25997
|
## Your model and reasoning effort
|
|
25519
25998
|
|
|
@@ -25538,7 +26017,15 @@ effort that deserves its own card and whose output you do not need.
|
|
|
25538
26017
|
|
|
25539
26018
|
Give it a self-contained \`prompt\` and choose \`model\` + \`effort\` by the new task's complexity (both
|
|
25540
26019
|
required). It returns a \`[title](/thread/<slug>)\` link \u2014 put that in your handoff so the human can open
|
|
25541
|
-
it
|
|
26020
|
+
it.
|
|
26021
|
+
|
|
26022
|
+
**It is the LAST resort among the exits, never the tidy one.** A finding you turned up is not by itself a
|
|
26023
|
+
reason to spawn: the ordered exits put DOING it (with a sub-agent, whose result comes back to you) first
|
|
26024
|
+
and ASKING second, precisely because both keep the work reachable. A spawned thread does not \u2014 it reports
|
|
26025
|
+
to the HUMAN and nothing it learns returns to you or to its siblings, so a chain of them re-derives the
|
|
26026
|
+
same facts in parallel and nobody notices. Spawn only when the work genuinely cannot ride on your card,
|
|
26027
|
+
and never merely to clear your own \` \`\`\`done \`. See **When the work is finished but the thread found
|
|
26028
|
+
more**.`;
|
|
25542
26029
|
THREAD_EXECUTION = {
|
|
25543
26030
|
claude: `## Thread types
|
|
25544
26031
|
|
|
@@ -26967,17 +27454,6 @@ function buildClaudeResumeCommand(opts) {
|
|
|
26967
27454
|
if (opts.message) argv.push(opts.message);
|
|
26968
27455
|
return argv;
|
|
26969
27456
|
}
|
|
26970
|
-
function armDefaultGoal(storage, slug) {
|
|
26971
|
-
try {
|
|
26972
|
-
storage.setRecurringPromptBySlug(slug, {
|
|
26973
|
-
prompt: DEFAULT_RECURRING_PROMPT,
|
|
26974
|
-
...DEFAULT_GOAL_TRIGGERS,
|
|
26975
|
-
intervalMs: null,
|
|
26976
|
-
armedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
26977
|
-
});
|
|
26978
|
-
} catch {
|
|
26979
|
-
}
|
|
26980
|
-
}
|
|
26981
27457
|
function createDispatcher(deps) {
|
|
26982
27458
|
const readBoardSource = deps.readBoard ?? readBoard;
|
|
26983
27459
|
const frizzDir = join23(deps.project.dir, ".frizz");
|
|
@@ -27063,7 +27539,6 @@ function createDispatcher(deps) {
|
|
|
27063
27539
|
effort: effort ?? null,
|
|
27064
27540
|
permission_mode: permissionMode
|
|
27065
27541
|
});
|
|
27066
|
-
armDefaultGoal(deps.storage, slug);
|
|
27067
27542
|
deps.storage.setBackend(slug, "codex");
|
|
27068
27543
|
deps.storage.setAgentSession(slug, spawned.binding.codexSessionId);
|
|
27069
27544
|
deps.storage.setCodexRuntime(slug, "app-server");
|
|
@@ -27124,7 +27599,6 @@ function createDispatcher(deps) {
|
|
|
27124
27599
|
effort: effort ?? null,
|
|
27125
27600
|
permission_mode: permissionMode
|
|
27126
27601
|
});
|
|
27127
|
-
armDefaultGoal(deps.storage, slug);
|
|
27128
27602
|
deps.storage.setBackend(slug, "claude");
|
|
27129
27603
|
deps.storage.setClaudeRuntime(slug, "broker");
|
|
27130
27604
|
void deps.board.rebuild().catch(() => {
|
|
@@ -27501,6 +27975,7 @@ var init_plan_files = __esm({
|
|
|
27501
27975
|
import {
|
|
27502
27976
|
watch as fsWatch
|
|
27503
27977
|
} from "node:fs";
|
|
27978
|
+
import { homedir as homedir12 } from "node:os";
|
|
27504
27979
|
import { join as join25 } from "node:path";
|
|
27505
27980
|
import watcher from "@parcel/watcher";
|
|
27506
27981
|
function appServerTurnStalled(liveness, lastActivityAt, nowMs) {
|
|
@@ -27564,6 +28039,53 @@ function stampStoppableShells(shells, row) {
|
|
|
27564
28039
|
if (isHeadlessRow(row)) return shells;
|
|
27565
28040
|
return shells.map((shell) => shell.stoppable ? { ...shell, stoppable: false } : shell);
|
|
27566
28041
|
}
|
|
28042
|
+
function declaredWaitIds(tele) {
|
|
28043
|
+
if (tele?.lastFence?.kind !== "awaiting") return [];
|
|
28044
|
+
return readAwaitingPark(tele.lastFence.hints).items.filter((i) => i.kind === "shell" || i.kind === "agent").map((i) => i.value);
|
|
28045
|
+
}
|
|
28046
|
+
function liveWaitHandles(tele) {
|
|
28047
|
+
const handles = /* @__PURE__ */ new Set();
|
|
28048
|
+
for (const shell of tele?.bgShells ?? []) {
|
|
28049
|
+
if (shell.state !== "running") continue;
|
|
28050
|
+
if (shell.id) handles.add(shell.id);
|
|
28051
|
+
if (shell.taskId) handles.add(shell.taskId);
|
|
28052
|
+
if (shell.label) handles.add(shell.label);
|
|
28053
|
+
}
|
|
28054
|
+
for (const agent of tele?.subAgents ?? []) {
|
|
28055
|
+
if (!isDirectSubAgent(agent) || agent.state !== "running") continue;
|
|
28056
|
+
handles.add(agent.id);
|
|
28057
|
+
if (agent.label) handles.add(agent.label);
|
|
28058
|
+
}
|
|
28059
|
+
return handles;
|
|
28060
|
+
}
|
|
28061
|
+
function hasDeclaredBackgroundPark(tele, nowMs) {
|
|
28062
|
+
const named = declaredWaitIds(tele);
|
|
28063
|
+
if (named.length === 0) return false;
|
|
28064
|
+
const live = liveWaitHandles(tele);
|
|
28065
|
+
if (!named.every((id) => live.has(id))) return false;
|
|
28066
|
+
const restedMs = Date.parse(tele?.lastAssistantAt ?? "");
|
|
28067
|
+
if (Number.isFinite(restedMs) && nowMs - restedMs > DECLARED_PARK_MAX_MS) return false;
|
|
28068
|
+
return true;
|
|
28069
|
+
}
|
|
28070
|
+
function hasDeclaredWait(tele, nowMs) {
|
|
28071
|
+
if (hasDeclaredBackgroundPark(tele, nowMs)) return true;
|
|
28072
|
+
if (tele?.lastFence?.kind === "awaiting" && hasParkedPrWatch(tele)) return true;
|
|
28073
|
+
return (tele?.subAgents ?? []).some((a) => isDirectSubAgent(a) && a.state === "running");
|
|
28074
|
+
}
|
|
28075
|
+
function heldByRunningChecks(tele, github, registered) {
|
|
28076
|
+
if (tele?.lastFence?.kind !== "awaiting") return false;
|
|
28077
|
+
const refs = [];
|
|
28078
|
+
for (const hint of tele.lastFence.hints) {
|
|
28079
|
+
if (hint.kind !== "pr") continue;
|
|
28080
|
+
const ref = parsePrRef(hint.value);
|
|
28081
|
+
if (ref && registered.has(githubStatusKey(ref))) refs.push(githubStatusKey(ref));
|
|
28082
|
+
}
|
|
28083
|
+
if (refs.length === 0) return false;
|
|
28084
|
+
return refs.every((key) => {
|
|
28085
|
+
const status = github[key];
|
|
28086
|
+
return status?.checks === "running" && status.state === "open";
|
|
28087
|
+
});
|
|
28088
|
+
}
|
|
27567
28089
|
function hasLiveOwnWork(tele) {
|
|
27568
28090
|
return Boolean(
|
|
27569
28091
|
tele?.subAgents?.some((agent) => isDirectSubAgent(agent) && agent.state === "running") || tele?.bgShells?.some((shell) => shell.state === "running") || // A PARKED PR WATCHER COUNTS (2026-08-13). It is the same shape of thing as a detached shell: the
|
|
@@ -27577,46 +28099,47 @@ function hasLiveOwnWork(tele) {
|
|
|
27577
28099
|
}
|
|
27578
28100
|
function hasParkedPrWatch(tele) {
|
|
27579
28101
|
if (tele?.lastFence?.kind !== "awaiting") return false;
|
|
27580
|
-
return tele.lastFence.hints.some((hint) => hint.kind === "pr
|
|
28102
|
+
return tele.lastFence.hints.some((hint) => hint.kind === "pr" && parsePrRef(hint.value) !== void 0);
|
|
27581
28103
|
}
|
|
27582
28104
|
function bgSnoozeArmed(row) {
|
|
27583
28105
|
return row.bg_snooze_rested_at != null && row.rested_at != null && row.bg_snooze_rested_at === row.rested_at;
|
|
27584
28106
|
}
|
|
27585
|
-
function hasParkedExternalWait(tele,
|
|
27586
|
-
|
|
27587
|
-
return tele.lastFence.hints.some(
|
|
27588
|
-
(hint) => hint.kind === "human" || hint.kind === "timer" && isValidAwaitingTimer(hint.value) && Date.parse(hint.value) > nowMs
|
|
27589
|
-
);
|
|
28107
|
+
function hasParkedExternalWait(tele, _nowMs) {
|
|
28108
|
+
return false;
|
|
27590
28109
|
}
|
|
27591
|
-
function
|
|
27592
|
-
if (tele?.lastFence?.kind !== "awaiting") return [];
|
|
28110
|
+
function fenceWatchViews(slug, tele, fenceAt, github = {}, registered = []) {
|
|
27593
28111
|
const seen = /* @__PURE__ */ new Set();
|
|
27594
28112
|
const out = [];
|
|
27595
|
-
for (const
|
|
27596
|
-
if (
|
|
27597
|
-
|
|
27598
|
-
if (!ref) continue;
|
|
27599
|
-
const target = `${ref.owner}/${ref.repo}#${ref.number}`;
|
|
27600
|
-
if (seen.has(target)) continue;
|
|
27601
|
-
seen.add(target);
|
|
28113
|
+
for (const w of registered) {
|
|
28114
|
+
if (seen.has(`github:${w.target}`)) continue;
|
|
28115
|
+
seen.add(`github:${w.target}`);
|
|
27602
28116
|
out.push({
|
|
27603
|
-
id: `github:${slug}:${target}`,
|
|
28117
|
+
id: `github:${slug}:${w.target}`,
|
|
27604
28118
|
kind: "github",
|
|
27605
|
-
target,
|
|
28119
|
+
target: w.target,
|
|
27606
28120
|
state: "armed",
|
|
27607
|
-
|
|
27608
|
-
//
|
|
27609
|
-
//
|
|
27610
|
-
|
|
28121
|
+
createdAt: w.createdAt,
|
|
28122
|
+
// Absent until the poller's first successful read. The card says "checking…" then, rather than
|
|
28123
|
+
// inventing a verdict — an unpolled PR and a PR with no CI are different facts.
|
|
28124
|
+
...github[w.target] ? { github: github[w.target] } : {}
|
|
27611
28125
|
});
|
|
27612
28126
|
}
|
|
28127
|
+
if (tele?.lastFence?.kind !== "awaiting") return out;
|
|
28128
|
+
const createdAt = fenceAt ?? tele.lastAssistantAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
28129
|
+
for (const hint of tele.lastFence.hints) {
|
|
28130
|
+
if (hint.kind !== "shell" && hint.kind !== "agent") continue;
|
|
28131
|
+
const target = hint.value.trim();
|
|
28132
|
+
if (!target || seen.has(`shell:${target}`) || !liveWaitHandles(tele).has(target)) continue;
|
|
28133
|
+
seen.add(`shell:${target}`);
|
|
28134
|
+
out.push({ id: `shell:${slug}:${target}`, kind: "shell", target, state: "armed", createdAt });
|
|
28135
|
+
}
|
|
27613
28136
|
return out;
|
|
27614
28137
|
}
|
|
27615
28138
|
function hasFreshDelivery(row, processGone) {
|
|
27616
28139
|
if (processGone) return false;
|
|
27617
28140
|
return parseDeliveryLedger(row.delivery_ledger).some((d) => d.state === "pending" || d.state === "enqueued");
|
|
27618
28141
|
}
|
|
27619
|
-
function deriveNeedsYou(row, tele, runtime, hasActionableInteraction = false, nowMs = Date.now(), limitPause = void 0, excuseLiveOwnWork = true, deliveryProcessGone = false) {
|
|
28142
|
+
function deriveNeedsYou(row, tele, runtime, hasActionableInteraction = false, nowMs = Date.now(), limitPause = void 0, excuseLiveOwnWork = true, deliveryProcessGone = false, github = {}, registeredPrWatches = /* @__PURE__ */ new Set()) {
|
|
27620
28143
|
if (futureSnooze(row, nowMs)) return false;
|
|
27621
28144
|
if (hasActionableInteraction) return true;
|
|
27622
28145
|
if (runtime === "perm-prompt") return true;
|
|
@@ -27630,15 +28153,17 @@ function deriveNeedsYou(row, tele, runtime, hasActionableInteraction = false, no
|
|
|
27630
28153
|
if (hasParkedExternalWait(tele, nowMs)) return false;
|
|
27631
28154
|
if (limitPause?.autoResume) return false;
|
|
27632
28155
|
if (excuseLiveOwnWork && runtime !== "exited" && hasLiveBackgroundWork(tele) && !tele?.lastFence) return false;
|
|
28156
|
+
if (excuseLiveOwnWork && runtime !== "exited" && hasDeclaredBackgroundPark(tele, nowMs)) return false;
|
|
28157
|
+
if (excuseLiveOwnWork && runtime !== "exited" && heldByRunningChecks(tele, github, registeredPrWatches)) return false;
|
|
27633
28158
|
if (runtime !== "exited" && hasLiveOwnWork(tele) && tele?.lastFence?.kind !== "done") return !bgSnoozeArmed(row);
|
|
27634
28159
|
if (tele?.lastFence?.kind === "done") return true;
|
|
27635
28160
|
return true;
|
|
27636
28161
|
}
|
|
27637
|
-
function deriveAwaitingBackground(row, tele, runtime, hasActionableInteraction = false, nowMs = Date.now(), limitPause = void 0, deliveryProcessGone = false) {
|
|
27638
|
-
if (runtime !== "turn-idle" || !
|
|
28162
|
+
function deriveAwaitingBackground(row, tele, runtime, hasActionableInteraction = false, nowMs = Date.now(), limitPause = void 0, deliveryProcessGone = false, github = {}, registeredPrWatches = /* @__PURE__ */ new Set()) {
|
|
28163
|
+
if (runtime !== "turn-idle" || !hasDeclaredWait(tele, nowMs)) return false;
|
|
27639
28164
|
if (hasActionableInteraction || tele?.pendingAsk || tele?.nativeInputRequired || tele?.pendingQuestion) return false;
|
|
27640
28165
|
if (tele?.lastFence?.kind === "done") return false;
|
|
27641
|
-
if (tele?.lastFence?.kind === "awaiting" && !hasParkedPrWatch(tele)) return false;
|
|
28166
|
+
if (tele?.lastFence?.kind === "awaiting" && !hasParkedPrWatch(tele) && !hasDeclaredBackgroundPark(tele, nowMs)) return false;
|
|
27642
28167
|
return deriveNeedsYou({ ...row, bg_snooze_rested_at: null }, tele, runtime, hasActionableInteraction, nowMs, limitPause, false, deliveryProcessGone);
|
|
27643
28168
|
}
|
|
27644
28169
|
function resolveSessionProfile(row, tele) {
|
|
@@ -27683,7 +28208,6 @@ function resolveRecurringPrompt(row) {
|
|
|
27683
28208
|
stopHook: row.recurring_on_rest === 1,
|
|
27684
28209
|
heartbeat: row.recurring_on_schedule === 1,
|
|
27685
28210
|
postCompaction: row.recurring_on_compact === 1,
|
|
27686
|
-
pauseOnQuestions: row.recurring_pause_on_questions === 1,
|
|
27687
28211
|
// Carried whenever a cadence has ever been chosen, INCLUDING while the heartbeat is off — the
|
|
27688
28212
|
// minutes field has to read back what switching it on again would use.
|
|
27689
28213
|
intervalSeconds: row.recurring_interval_ms ? Math.round(row.recurring_interval_ms / 1e3) : void 0,
|
|
@@ -27717,7 +28241,12 @@ function resolveSessionTitle(row, tele) {
|
|
|
27717
28241
|
aiTitle: locked ? void 0 : tele?.aiTitle ?? persisted
|
|
27718
28242
|
};
|
|
27719
28243
|
}
|
|
27720
|
-
function sessionThreadView(projectDir, storage, row, tele, registeredLegacyTerminal, interactionPresence, nowMs, codexTurnLiveness, claudeBrokerDaemonAlive) {
|
|
28244
|
+
function sessionThreadView(projectDir, storage, row, tele, registeredLegacyTerminal, interactionPresence, nowMs, codexTurnLiveness, claudeBrokerDaemonAlive, github = {}) {
|
|
28245
|
+
const armedPrWatches = storage.listPrWatches(row.slug, { armedOnly: true }).map((w) => ({
|
|
28246
|
+
target: `${w.owner}/${w.repo}#${w.number}`,
|
|
28247
|
+
createdAt: new Date(w.created_at).toISOString()
|
|
28248
|
+
}));
|
|
28249
|
+
const registeredPrWatches = new Set(armedPrWatches.map((w) => w.target));
|
|
27721
28250
|
const headlessStalled = row.codex_runtime === "app-server" ? appServerTurnStalled(codexTurnLiveness(row.slug, row.session_id), tele?.lastActivityAt, nowMs) : isBrokerClaudeRow(row) ? !claudeBrokerDaemonAlive(row.session_id) : false;
|
|
27722
28251
|
const headlessLostWork = isBrokerClaudeRow(row) && headlessStalled && hasUnretiredOwnAgents(tele);
|
|
27723
28252
|
const deliveryProcessGone = isBrokerClaudeRow(row) && headlessStalled;
|
|
@@ -27728,8 +28257,8 @@ function sessionThreadView(projectDir, storage, row, tele, registeredLegacyTermi
|
|
|
27728
28257
|
const state = effectiveSessionState(row, registeredLegacyTerminal);
|
|
27729
28258
|
const archived = state === "archived";
|
|
27730
28259
|
const limitPause = resolveLimitPause(row, tele, nowMs);
|
|
27731
|
-
const needsYou = archived ? false : deriveNeedsYou(row, tele, runtime, interactionPresence.needsUser, nowMs, limitPause, true, deliveryProcessGone);
|
|
27732
|
-
const awaitingBackground = archived ? false : deriveAwaitingBackground(row, tele, runtime, interactionPresence.needsUser, nowMs, limitPause, deliveryProcessGone);
|
|
28260
|
+
const needsYou = archived ? false : deriveNeedsYou(row, tele, runtime, interactionPresence.needsUser, nowMs, limitPause, true, deliveryProcessGone, github, registeredPrWatches);
|
|
28261
|
+
const awaitingBackground = archived ? false : deriveAwaitingBackground(row, tele, runtime, interactionPresence.needsUser, nowMs, limitPause, deliveryProcessGone, github, registeredPrWatches);
|
|
27733
28262
|
const crashed = runtime === "exited" && (tele?.turn === "in-flight" || hasLiveBackgroundWork(tele) || headlessLostWork);
|
|
27734
28263
|
const snoozedUntil = futureSnooze(row, nowMs);
|
|
27735
28264
|
const profile = resolveSessionProfile(row, tele);
|
|
@@ -27761,20 +28290,11 @@ function sessionThreadView(projectDir, storage, row, tele, registeredLegacyTermi
|
|
|
27761
28290
|
lastAssistantAt: tele?.lastAssistantAt,
|
|
27762
28291
|
subAgents: stampStoppable(tele?.subAgents ?? [], row),
|
|
27763
28292
|
bgShells: stampStoppableShells(tele?.bgShells ?? [], row),
|
|
27764
|
-
//
|
|
27765
|
-
//
|
|
27766
|
-
//
|
|
27767
|
-
//
|
|
27768
|
-
watches:
|
|
27769
|
-
...storage.listThreadWatches(row.slug, { armedOnly: true }).map((w) => ({
|
|
27770
|
-
id: w.id,
|
|
27771
|
-
kind: w.kind,
|
|
27772
|
-
target: w.target,
|
|
27773
|
-
state: w.state,
|
|
27774
|
-
createdAt: new Date(w.created_at).toISOString()
|
|
27775
|
-
})),
|
|
27776
|
-
...githubWatchViews(row.slug, tele, tele?.lastAssistantAt)
|
|
27777
|
-
],
|
|
28293
|
+
// ONE SOURCE: the FENCE. Both kinds are derived from what the worker wrote — `pr-watch:` lines
|
|
28294
|
+
// become the github rows, `watch:` lines the shell rows — so this strip lists exactly what will
|
|
28295
|
+
// actually wake the thread, and the two cannot drift into claiming different things. There is no
|
|
28296
|
+
// registry behind either any more (`thread_watch`, retired 2026-08-14).
|
|
28297
|
+
watches: fenceWatchViews(row.slug, tele, tele?.lastAssistantAt, github, armedPrWatches),
|
|
27778
28298
|
pendingAsk: tele?.pendingAsk ? { questions: tele.pendingAsk.questions } : void 0,
|
|
27779
28299
|
nativeInputRequired: tele?.nativeInputRequired,
|
|
27780
28300
|
pendingQuestion: tele?.pendingQuestion ?? false,
|
|
@@ -27795,6 +28315,10 @@ function sessionThreadView(projectDir, storage, row, tele, registeredLegacyTermi
|
|
|
27795
28315
|
// Only meaningful while the snooze is still pending — a prompt without a live deadline is an
|
|
27796
28316
|
// already-delivered (or superseded) bump the row has not been swept clean of yet.
|
|
27797
28317
|
snoozePrompt: snoozedUntil ? row.snooze_prompt ?? void 0 : void 0,
|
|
28318
|
+
// The event-snooze, as a fact the CHAT can read. `awaitingBackground` still ignores it — that flag
|
|
28319
|
+
// states whether the thread is waiting, which the snooze does not change — so the suppression is a
|
|
28320
|
+
// presentation rule the client applies, not a second opinion about the thread's state.
|
|
28321
|
+
bgSnoozed: bgSnoozeArmed(row) || void 0,
|
|
27798
28322
|
claudeRuntime: row.claude_runtime === "broker" ? "broker" : row.claude_runtime === "tmux" ? "tmux" : void 0,
|
|
27799
28323
|
// The recurring prompt — the same projection the worker's own `action: "get"` reads back.
|
|
27800
28324
|
recurringPrompt: resolveRecurringPrompt(row),
|
|
@@ -27881,6 +28405,7 @@ function createBoard(project, storage, bus, tailer, bootId, deps = {}) {
|
|
|
27881
28405
|
let plansCache = [];
|
|
27882
28406
|
function buildSessionThreads(nowMs) {
|
|
27883
28407
|
const rows = storage.allSessions().filter((row) => ThreadSlug.safeParse(row.slug).success);
|
|
28408
|
+
const github = readGithubStatusBook(storage.getSetting(GITHUB_STATUS_SETTING));
|
|
27884
28409
|
const currentInteractionKeys = /* @__PURE__ */ new Set();
|
|
27885
28410
|
const out = [];
|
|
27886
28411
|
for (const row of rows) {
|
|
@@ -27916,7 +28441,8 @@ function createBoard(project, storage, bus, tailer, bootId, deps = {}) {
|
|
|
27916
28441
|
interactionPresence,
|
|
27917
28442
|
nowMs,
|
|
27918
28443
|
codexTurnLiveness,
|
|
27919
|
-
claudeBrokerDaemonAlive
|
|
28444
|
+
claudeBrokerDaemonAlive,
|
|
28445
|
+
github
|
|
27920
28446
|
));
|
|
27921
28447
|
}
|
|
27922
28448
|
for (const key of pendingInteractionCache.keys()) {
|
|
@@ -27932,7 +28458,9 @@ function createBoard(project, storage, bus, tailer, bootId, deps = {}) {
|
|
|
27932
28458
|
projectName: project.name,
|
|
27933
28459
|
projectLabel: project.label,
|
|
27934
28460
|
...project.githubRepo ? { githubRepo: project.githubRepo } : {},
|
|
27935
|
-
projectSlug: findByPath(project.dir)?.slug
|
|
28461
|
+
projectSlug: findByPath(project.dir)?.slug,
|
|
28462
|
+
// So the client can expand a `~` a worker wrote in prose (see BoardSnapshot.homeDir).
|
|
28463
|
+
homeDir: homedir12()
|
|
27936
28464
|
};
|
|
27937
28465
|
const sessionThreads = buildSessionThreads(assembledAtMs);
|
|
27938
28466
|
armSnoozeWake(sessionThreads, assembledAtMs);
|
|
@@ -28113,7 +28641,7 @@ function createBoard(project, storage, bus, tailer, bootId, deps = {}) {
|
|
|
28113
28641
|
}
|
|
28114
28642
|
};
|
|
28115
28643
|
}
|
|
28116
|
-
var DEBOUNCE_MS, RECONCILE_MS, STALL_GRACE_MS, LINE_CAP;
|
|
28644
|
+
var DEBOUNCE_MS, RECONCILE_MS, STALL_GRACE_MS, LINE_CAP, DECLARED_PARK_MAX_MS;
|
|
28117
28645
|
var init_board = __esm({
|
|
28118
28646
|
"packages/server/src/board.ts"() {
|
|
28119
28647
|
"use strict";
|
|
@@ -28134,6 +28662,7 @@ var init_board = __esm({
|
|
|
28134
28662
|
RECONCILE_MS = 15e3;
|
|
28135
28663
|
STALL_GRACE_MS = 3e4;
|
|
28136
28664
|
LINE_CAP = 240;
|
|
28665
|
+
DECLARED_PARK_MAX_MS = 24 * 60 * 60 * 1e3;
|
|
28137
28666
|
}
|
|
28138
28667
|
});
|
|
28139
28668
|
|
|
@@ -28641,6 +29170,17 @@ var init_github_review = __esm({
|
|
|
28641
29170
|
import { execFile as execFile6 } from "node:child_process";
|
|
28642
29171
|
import { promisify as promisify6 } from "node:util";
|
|
28643
29172
|
import { createHash as createHash14, randomUUID as randomUUID14 } from "node:crypto";
|
|
29173
|
+
function failureSignature(rollup) {
|
|
29174
|
+
const ids = [];
|
|
29175
|
+
for (const c of Array.isArray(rollup) ? rollup : []) {
|
|
29176
|
+
if (!c || typeof c !== "object") continue;
|
|
29177
|
+
if (!rollupEntryFailed(typeof c.conclusion === "string" ? c.conclusion : void 0, typeof c.state === "string" ? c.state : void 0)) continue;
|
|
29178
|
+
const label = c.name ?? c.context ?? c.workflowName ?? "?";
|
|
29179
|
+
ids.push(`${label}@${c.detailsUrl ?? c.targetUrl ?? c.completedAt ?? ""}`);
|
|
29180
|
+
}
|
|
29181
|
+
if (ids.length === 0) return "";
|
|
29182
|
+
return createHash14("sha1").update(ids.sort().join("\n")).digest("hex").slice(0, 12);
|
|
29183
|
+
}
|
|
28644
29184
|
function parsePrRef2(value) {
|
|
28645
29185
|
const m = value.trim().match(PR_REF_RE2);
|
|
28646
29186
|
if (!m) return void 0;
|
|
@@ -28652,7 +29192,7 @@ function refKey2(r) {
|
|
|
28652
29192
|
return `${r.owner}/${r.repo}#${r.number}`;
|
|
28653
29193
|
}
|
|
28654
29194
|
function ghPrViewArgs(ref) {
|
|
28655
|
-
return ["pr", "view", String(ref.number), "--repo", `${ref.owner}/${ref.repo}`, "--json", "state,mergedAt,statusCheckRollup,headRefOid"];
|
|
29195
|
+
return ["pr", "view", String(ref.number), "--repo", `${ref.owner}/${ref.repo}`, "--json", "state,mergedAt,statusCheckRollup,headRefOid,mergeable,reviewDecision"];
|
|
28656
29196
|
}
|
|
28657
29197
|
function latestWorkflowRuns(runs) {
|
|
28658
29198
|
const latest = /* @__PURE__ */ new Map();
|
|
@@ -28708,12 +29248,40 @@ function failedCheckNames(rollup, runs = [], cap = 8) {
|
|
|
28708
29248
|
}
|
|
28709
29249
|
return { names: names.slice(0, cap), omitted: Math.max(0, names.length - cap) };
|
|
28710
29250
|
}
|
|
28711
|
-
function
|
|
28712
|
-
|
|
29251
|
+
function githubWatchStatus(pr, polledAt) {
|
|
29252
|
+
const entries = Array.isArray(pr.rollup) ? pr.rollup.filter((c) => c && typeof c === "object") : [];
|
|
29253
|
+
let running = 0;
|
|
29254
|
+
let passed = 0;
|
|
29255
|
+
let failed = 0;
|
|
29256
|
+
for (const c of entries) {
|
|
29257
|
+
const status = typeof c.status === "string" ? c.status : void 0;
|
|
29258
|
+
const state2 = typeof c.state === "string" ? c.state : void 0;
|
|
29259
|
+
const conclusion = typeof c.conclusion === "string" ? c.conclusion : void 0;
|
|
29260
|
+
const terminal = status ? status === "COMPLETED" : state2 ? state2 !== "PENDING" && state2 !== "EXPECTED" : false;
|
|
29261
|
+
if (!terminal) running++;
|
|
29262
|
+
else if (rollupEntryFailed(conclusion, state2)) failed++;
|
|
29263
|
+
else passed++;
|
|
29264
|
+
}
|
|
29265
|
+
const state = pr.state === "MERGED" ? "merged" : pr.state === "CLOSED" ? "closed" : "open";
|
|
29266
|
+
const checks = entries.length === 0 ? "none" : failed > 0 ? "failing" : running > 0 ? "running" : "passing";
|
|
29267
|
+
const merge = pr.mergeable === "CONFLICTING" ? "conflicting" : pr.mergeable !== "MERGEABLE" ? "unknown" : checks === "failing" || pr.reviewDecision === "CHANGES_REQUESTED" || pr.reviewDecision === "REVIEW_REQUIRED" ? "blocked" : "mergeable";
|
|
29268
|
+
return {
|
|
29269
|
+
checks,
|
|
29270
|
+
running,
|
|
29271
|
+
passed,
|
|
29272
|
+
failed,
|
|
29273
|
+
failing: failedCheckNames(entries, pr.workflowRuns ?? []).names,
|
|
29274
|
+
merge,
|
|
29275
|
+
state,
|
|
29276
|
+
polledAt,
|
|
29277
|
+
head: pr.head,
|
|
29278
|
+
failureSig: failureSignature(entries)
|
|
29279
|
+
};
|
|
29280
|
+
}
|
|
29281
|
+
function isPrWatchHint(_kind) {
|
|
29282
|
+
return false;
|
|
28713
29283
|
}
|
|
28714
|
-
function isActionable(
|
|
28715
|
-
if (hint.kind === "timer") return isValidAwaitingTimer(hint.value);
|
|
28716
|
-
if (isPrWatchHint(hint.kind) || hint.kind === "pr" || hint.kind === "ci") return parsePrRef2(hint.value) !== void 0;
|
|
29284
|
+
function isActionable(_hint) {
|
|
28717
29285
|
return false;
|
|
28718
29286
|
}
|
|
28719
29287
|
function fenceIdentity(hints, fenceAt) {
|
|
@@ -28763,20 +29331,28 @@ function isHeartbeatFenceId(fenceId) {
|
|
|
28763
29331
|
function saidDone(tele) {
|
|
28764
29332
|
return tele.lastFence?.kind === "done" || tele.lastAssistantAllDone === true;
|
|
28765
29333
|
}
|
|
28766
|
-
function
|
|
28767
|
-
return
|
|
29334
|
+
function armedTimerIdsOf(storage, slug) {
|
|
29335
|
+
return new Set(storage.listThreadTimers(slug, { armedOnly: true }).map((t) => t.id));
|
|
28768
29336
|
}
|
|
28769
|
-
function
|
|
28770
|
-
|
|
28771
|
-
|
|
28772
|
-
return fence.hints.some((h) => isActionable(h) || h.kind === "human");
|
|
29337
|
+
function restMessageIsSignedOff(tele, _registeredPrWatches = /* @__PURE__ */ new Set(), _armedTimerIds = /* @__PURE__ */ new Set()) {
|
|
29338
|
+
if (tele.lastFence?.kind === "awaiting") return true;
|
|
29339
|
+
return saidDone(tele);
|
|
28773
29340
|
}
|
|
28774
|
-
function
|
|
28775
|
-
|
|
29341
|
+
function liveActivityOf(tele, registeredPrWatches, armedTimerIds) {
|
|
29342
|
+
const shells = /* @__PURE__ */ new Set();
|
|
29343
|
+
for (const sh of tele.bgShells ?? []) {
|
|
29344
|
+
if (sh.state !== "running") continue;
|
|
29345
|
+
for (const h of [sh.taskId, sh.id, sh.label]) if (h) shells.add(h);
|
|
29346
|
+
}
|
|
29347
|
+
const agents = /* @__PURE__ */ new Set();
|
|
29348
|
+
for (const a of tele.subAgents ?? []) {
|
|
29349
|
+
if (a.state !== "running") continue;
|
|
29350
|
+
for (const h of [a.id, a.label]) if (h) agents.add(h);
|
|
29351
|
+
}
|
|
29352
|
+
return { shells, agents, timers: armedTimerIds, prs: registeredPrWatches };
|
|
28776
29353
|
}
|
|
28777
|
-
function
|
|
28778
|
-
|
|
28779
|
-
return tele !== void 0 && blockedOnHuman(tele);
|
|
29354
|
+
function registeredPrWatchesOf(storage, slug) {
|
|
29355
|
+
return new Set(storage.listPrWatches(slug, { armedOnly: true }).map((w) => `${w.owner}/${w.repo}#${w.number}`));
|
|
28780
29356
|
}
|
|
28781
29357
|
function armedSchedule(row) {
|
|
28782
29358
|
const prompt = row.recurring_prompt?.trim();
|
|
@@ -28814,20 +29390,36 @@ function armedCompact(row) {
|
|
|
28814
29390
|
if (row.recurring_on_compact !== 1) return void 0;
|
|
28815
29391
|
return { prompt, armedAt };
|
|
28816
29392
|
}
|
|
29393
|
+
function shellFenceId(toolUseId) {
|
|
29394
|
+
return `${SHELL_FENCE_PREFIX}:${toolUseId}`;
|
|
29395
|
+
}
|
|
29396
|
+
function isShellFenceId(fenceId) {
|
|
29397
|
+
return fenceId.startsWith(`${SHELL_FENCE_PREFIX}:`);
|
|
29398
|
+
}
|
|
29399
|
+
function prWatchFenceId(watchId, report) {
|
|
29400
|
+
return `${PR_WATCH_FENCE_PREFIX}:${watchId}:${report}`;
|
|
29401
|
+
}
|
|
29402
|
+
function prWatchExpiredFenceId(watchId) {
|
|
29403
|
+
return `${PR_WATCH_EXPIRED_FENCE_PREFIX}:${watchId}`;
|
|
29404
|
+
}
|
|
29405
|
+
function isPrWatchFenceId(fenceId) {
|
|
29406
|
+
return fenceId.startsWith(`${PR_WATCH_FENCE_PREFIX}:`) || fenceId.startsWith(`${PR_WATCH_EXPIRED_FENCE_PREFIX}:`);
|
|
29407
|
+
}
|
|
28817
29408
|
function signoffFenceId(restedAt) {
|
|
28818
29409
|
return `${SIGNOFF_FENCE_PREFIX}:${restedAt}`;
|
|
28819
29410
|
}
|
|
28820
29411
|
function isSignoffFenceId(fenceId) {
|
|
28821
29412
|
return fenceId.startsWith(`${SIGNOFF_FENCE_PREFIX}:`);
|
|
28822
29413
|
}
|
|
28823
|
-
function
|
|
28824
|
-
return `${
|
|
29414
|
+
function parkFenceId(cause, spokeAt) {
|
|
29415
|
+
return `${PARK_FENCE_PREFIX}:${cause}:${spokeAt}`;
|
|
28825
29416
|
}
|
|
28826
|
-
function
|
|
28827
|
-
return fenceId.startsWith(`${
|
|
29417
|
+
function isParkFenceId(fenceId) {
|
|
29418
|
+
return fenceId.startsWith(`${PARK_FENCE_PREFIX}:`);
|
|
28828
29419
|
}
|
|
28829
|
-
function
|
|
28830
|
-
|
|
29420
|
+
function parkFenceRestOf(fenceId) {
|
|
29421
|
+
const cut = fenceId.indexOf(":", PARK_FENCE_PREFIX.length + 1);
|
|
29422
|
+
return cut < 0 ? "" : fenceId.slice(cut + 1);
|
|
28831
29423
|
}
|
|
28832
29424
|
function timerFenceId(timerId) {
|
|
28833
29425
|
return `${TIMER_FENCE_PREFIX}:${timerId}`;
|
|
@@ -28839,12 +29431,6 @@ function timerIdOf(fenceId) {
|
|
|
28839
29431
|
return fenceId.slice(TIMER_FENCE_PREFIX.length + 1);
|
|
28840
29432
|
}
|
|
28841
29433
|
function evalHint(hint, nowMs, prCache, fenceBody) {
|
|
28842
|
-
if (hint.kind === "timer") {
|
|
28843
|
-
const target = Date.parse(hint.value);
|
|
28844
|
-
if (!isValidAwaitingTimer(hint.value) || !Number.isFinite(target)) return void 0;
|
|
28845
|
-
const desc = fenceBody.trim().replace(/\s+/g, " ").slice(0, 200);
|
|
28846
|
-
return { met: nowMs >= target, steer: `\u23F0 Your timer fired${desc ? `: ${desc}` : ""}. Continue.`, reason: `timer ${hint.value}` };
|
|
28847
|
-
}
|
|
28848
29434
|
if (isPrWatchHint(hint.kind)) return void 0;
|
|
28849
29435
|
const ref = parsePrRef2(hint.value);
|
|
28850
29436
|
if (!ref) return void 0;
|
|
@@ -28895,6 +29481,9 @@ async function defaultFetchPr(ref) {
|
|
|
28895
29481
|
state: j.state,
|
|
28896
29482
|
mergedAt: typeof j.mergedAt === "string" ? j.mergedAt : null,
|
|
28897
29483
|
rollup: Array.isArray(j.statusCheckRollup) ? j.statusCheckRollup : [],
|
|
29484
|
+
head: j.headRefOid,
|
|
29485
|
+
mergeable: typeof j.mergeable === "string" ? j.mergeable : void 0,
|
|
29486
|
+
reviewDecision: typeof j.reviewDecision === "string" ? j.reviewDecision : void 0,
|
|
28898
29487
|
workflowRuns
|
|
28899
29488
|
};
|
|
28900
29489
|
} catch {
|
|
@@ -28937,6 +29526,14 @@ function createScheduler(deps) {
|
|
|
28937
29526
|
let timer = null;
|
|
28938
29527
|
let activeTick = null;
|
|
28939
29528
|
let stopped = false;
|
|
29529
|
+
function publishGithubStatus(key, pr, nowMs) {
|
|
29530
|
+
const raw2 = deps.storage.getSetting(GITHUB_STATUS_SETTING);
|
|
29531
|
+
const book = raw2 && typeof raw2 === "object" && !Array.isArray(raw2) ? { ...raw2 } : {};
|
|
29532
|
+
book[key] = githubWatchStatus(pr, new Date(nowMs).toISOString());
|
|
29533
|
+
const keys = Object.keys(book);
|
|
29534
|
+
for (const stale of keys.slice(0, Math.max(0, keys.length - GITHUB_STATUS_CAP))) delete book[stale];
|
|
29535
|
+
deps.storage.setSetting(GITHUB_STATUS_SETTING, book);
|
|
29536
|
+
}
|
|
28940
29537
|
function loadFired() {
|
|
28941
29538
|
const raw2 = deps.storage.getSetting("waker.fired");
|
|
28942
29539
|
return Array.isArray(raw2) ? raw2.filter((x) => typeof x === "string") : [];
|
|
@@ -29067,6 +29664,12 @@ function createScheduler(deps) {
|
|
|
29067
29664
|
if (!fault || limitFenceId(fault) !== item.fenceId) return "superseded";
|
|
29068
29665
|
return tele.turn === "idle" ? "current-idle" : "current-busy";
|
|
29069
29666
|
}
|
|
29667
|
+
if (isShellFenceId(item.fenceId)) {
|
|
29668
|
+
return tele.turn === "idle" ? "current-idle" : "current-busy";
|
|
29669
|
+
}
|
|
29670
|
+
if (isPrWatchFenceId(item.fenceId)) {
|
|
29671
|
+
return tele.turn === "idle" ? "current-idle" : "current-busy";
|
|
29672
|
+
}
|
|
29070
29673
|
if (isSnoozeFenceId(item.fenceId)) {
|
|
29071
29674
|
if (armedSnooze(row)?.fenceId !== item.fenceId) return "superseded";
|
|
29072
29675
|
return tele.turn === "idle" ? "current-idle" : "current-busy";
|
|
@@ -29079,7 +29682,7 @@ function createScheduler(deps) {
|
|
|
29079
29682
|
if (isStopHookFenceId(item.fenceId)) {
|
|
29080
29683
|
const armed = armedRest(row);
|
|
29081
29684
|
if (!armed || item.fenceId !== stopHookFenceId(armed.armedAt, tele.lastAssistantAt ?? "")) return "superseded";
|
|
29082
|
-
if (restMessageIsSignedOff(tele)) return "superseded";
|
|
29685
|
+
if (restMessageIsSignedOff(tele, registeredPrWatchesOf(deps.storage, item.slug), armedTimerIdsOf(deps.storage, item.slug))) return "superseded";
|
|
29083
29686
|
return tele.turn === "idle" ? "current-idle" : "current-busy";
|
|
29084
29687
|
}
|
|
29085
29688
|
if (isCompactFenceId(item.fenceId)) {
|
|
@@ -29096,15 +29699,19 @@ function createScheduler(deps) {
|
|
|
29096
29699
|
if (tele.lastFence || tele.pendingQuestion) return "superseded";
|
|
29097
29700
|
return tele.turn === "idle" ? "current-idle" : "current-busy";
|
|
29098
29701
|
}
|
|
29099
|
-
if (isWatchFenceId(item.fenceId)) {
|
|
29100
|
-
if (deps.storage.getThreadWatch(watchIdOf(item.fenceId))?.state !== "armed") return "superseded";
|
|
29101
|
-
return tele.turn === "idle" ? "current-idle" : "current-busy";
|
|
29102
|
-
}
|
|
29103
29702
|
if (isReportFenceId(item.fenceId)) {
|
|
29104
29703
|
const stillMissing = tele.droppedReports?.some((r) => reportFenceId(r.taskId) === item.fenceId);
|
|
29105
29704
|
if (!stillMissing) return "superseded";
|
|
29106
29705
|
return tele.turn === "idle" ? "current-idle" : "current-busy";
|
|
29107
29706
|
}
|
|
29707
|
+
if (isParkFenceId(item.fenceId)) {
|
|
29708
|
+
if (parkFenceRestOf(item.fenceId) !== (tele.lastAssistantAt ?? "")) return "superseded";
|
|
29709
|
+
return tele.turn === "idle" ? "current-idle" : "current-busy";
|
|
29710
|
+
}
|
|
29711
|
+
if (!item.fenceId.includes("")) {
|
|
29712
|
+
log3(`waker: UNROUTED wake prefix ${item.fenceId.split(":")[0]} for ${item.slug} \u2014 no deliveryContext branch, superseding`);
|
|
29713
|
+
return "superseded";
|
|
29714
|
+
}
|
|
29108
29715
|
const fence = tele.lastFence;
|
|
29109
29716
|
if (!fence || fence.kind !== "awaiting" || !fence.hints.some(isActionable)) return "superseded";
|
|
29110
29717
|
if (fenceIdentity(fence.hints, tele.lastActivityAt) !== item.fenceId) return "superseded";
|
|
@@ -29246,26 +29853,30 @@ function createScheduler(deps) {
|
|
|
29246
29853
|
const deliveryId = wakeDeliveryId(slug, sessionId, fenceId);
|
|
29247
29854
|
if (outbox.get(deliveryId)) st.fired = true;
|
|
29248
29855
|
if (st.fired) return;
|
|
29249
|
-
const needsPr =
|
|
29856
|
+
const needsPr = false;
|
|
29250
29857
|
const needsReview = actionable.some((h) => isPrWatchHint(h.kind));
|
|
29251
29858
|
if ((needsPr || needsReview) && (st.lastPollAt === 0 || nowMs - st.lastPollAt >= pollMs)) {
|
|
29252
29859
|
st.lastPollAt = nowMs;
|
|
29253
29860
|
const refs = /* @__PURE__ */ new Map();
|
|
29254
29861
|
const reviewRefs = /* @__PURE__ */ new Map();
|
|
29862
|
+
const verdictRefs = /* @__PURE__ */ new Set();
|
|
29255
29863
|
for (const h of actionable) {
|
|
29256
29864
|
const ref = parsePrRef2(h.value);
|
|
29257
29865
|
if (!ref) continue;
|
|
29258
|
-
if (h.kind === "pr" || h.kind === "ci") refs.set(refKey2(ref), ref);
|
|
29259
29866
|
if (isPrWatchHint(h.kind)) reviewRefs.set(refKey2(ref), ref);
|
|
29260
29867
|
}
|
|
29261
29868
|
await Promise.all([
|
|
29262
29869
|
...[...refs].map(async ([k, ref]) => {
|
|
29263
29870
|
try {
|
|
29264
29871
|
const s = await fetchPr(ref);
|
|
29265
|
-
if (s)
|
|
29266
|
-
|
|
29872
|
+
if (s) {
|
|
29873
|
+
st.prCache.set(k, s);
|
|
29874
|
+
publishGithubStatus(k, s, nowMs);
|
|
29875
|
+
} else if (verdictRefs.has(k)) {
|
|
29876
|
+
log3(`waker: gh check skipped for ${k} (${slug}) \u2014 gh unavailable / not authed / rate-limited`);
|
|
29877
|
+
}
|
|
29267
29878
|
} catch (err) {
|
|
29268
|
-
log3(`waker: gh check errored for ${k} (${slug}): ${err instanceof Error ? err.message : String(err)}`);
|
|
29879
|
+
if (verdictRefs.has(k)) log3(`waker: gh check errored for ${k} (${slug}): ${err instanceof Error ? err.message : String(err)}`);
|
|
29269
29880
|
}
|
|
29270
29881
|
}),
|
|
29271
29882
|
...[...reviewRefs].map(async ([k, ref]) => {
|
|
@@ -29451,7 +30062,19 @@ function createScheduler(deps) {
|
|
|
29451
30062
|
sessionId: row.session_id,
|
|
29452
30063
|
fenceId,
|
|
29453
30064
|
hintKey: SIGNOFF_HINT_KEY,
|
|
29454
|
-
|
|
30065
|
+
// The reminder plus THIS THREAD's live ops and their ids, so the agent can write a correct
|
|
30066
|
+
// `watch:` line without going looking for an id it cannot see. Shells are named by `taskId` —
|
|
30067
|
+
// the handle the runtime actually showed the worker — because that is the string it will
|
|
30068
|
+
// naturally reach for, and the one the fence's own integrity check matches on.
|
|
30069
|
+
message: signoffNudgeMessage({
|
|
30070
|
+
shells: (tele.bgShells ?? []).filter((sh) => sh.state === "running").map((sh) => ({ id: sh.taskId ?? sh.id, label: sh.label })),
|
|
30071
|
+
subAgents: (tele.subAgents ?? []).filter((a) => a.state === "running").map((a) => ({ id: a.id, label: a.label })),
|
|
30072
|
+
// The other two registries, so the nudge lists EVERY kind an awaiting fence can name rather
|
|
30073
|
+
// than the two the fold happens to know about — a worker told about half its work writes half
|
|
30074
|
+
// a fence, and the half it left out is not what gets it bumped.
|
|
30075
|
+
timers: deps.storage.listThreadTimers(row.slug, { armedOnly: true }).map((t) => ({ id: t.id, label: t.prompt.trim().replace(/\s+/g, " ").slice(0, 80) })),
|
|
30076
|
+
prs: deps.storage.listPrWatches(row.slug, { armedOnly: true }).map((w) => ({ id: `${w.owner}/${w.repo}#${w.number}`, label: `${w.owner}/${w.repo}#${w.number}` }))
|
|
30077
|
+
}),
|
|
29455
30078
|
reason: "rested without signing off"
|
|
29456
30079
|
}, nowMs).delivery;
|
|
29457
30080
|
log3(`waker: queued ${row.slug} \u2014 ${item.reason}`);
|
|
@@ -29462,27 +30085,99 @@ function createScheduler(deps) {
|
|
|
29462
30085
|
if (!isSignoffFenceId(item.fenceId)) return;
|
|
29463
30086
|
deps.storage.countSignoffNudge(item.slug, item.fenceId);
|
|
29464
30087
|
}
|
|
29465
|
-
function
|
|
29466
|
-
for (const
|
|
29467
|
-
if (
|
|
29468
|
-
const row = deps.storage.getSession(watch.thread_slug);
|
|
29469
|
-
if (!row || row.state === "archived" || row.archived === 1) continue;
|
|
30088
|
+
function evalParkIntegrity(nowMs) {
|
|
30089
|
+
for (const row of deps.storage.allSessions()) {
|
|
30090
|
+
if (row.state === "archived" || row.archived === 1) continue;
|
|
29470
30091
|
const tele = deps.tailer.get(row.slug);
|
|
29471
|
-
if (!tele) continue;
|
|
29472
|
-
|
|
29473
|
-
|
|
30092
|
+
if (!tele || tele.turn !== "idle") continue;
|
|
30093
|
+
if (tele.lastFence?.kind !== "awaiting") continue;
|
|
30094
|
+
const spokeAt = tele.lastAssistantAt;
|
|
30095
|
+
if (!spokeAt) continue;
|
|
30096
|
+
if (tele.lastUserAt && Date.parse(tele.lastUserAt) >= Date.parse(spokeAt)) continue;
|
|
30097
|
+
if (tele.authFault) continue;
|
|
30098
|
+
const park = readAwaitingPark(tele.lastFence.hints);
|
|
30099
|
+
const live = liveActivityOf(
|
|
30100
|
+
tele,
|
|
30101
|
+
registeredPrWatchesOf(deps.storage, row.slug),
|
|
30102
|
+
armedTimerIdsOf(deps.storage, row.slug)
|
|
29474
30103
|
);
|
|
29475
|
-
|
|
29476
|
-
|
|
29477
|
-
|
|
29478
|
-
|
|
29479
|
-
if (
|
|
29480
|
-
if (
|
|
29481
|
-
|
|
29482
|
-
|
|
29483
|
-
|
|
29484
|
-
|
|
29485
|
-
|
|
30104
|
+
const dead = unaccountedItems(park.items, live);
|
|
30105
|
+
const expiresAt = parkExpiresAt(park, Date.parse(spokeAt));
|
|
30106
|
+
const expired = expiresAt !== null && nowMs >= expiresAt;
|
|
30107
|
+
const nameless = park.items.length === 0;
|
|
30108
|
+
if (parkIsHonoured(park, live) && (row.park_bumps ?? 0) > 0) deps.storage.resetParkBumps(row.slug);
|
|
30109
|
+
if (dead.length === 0 && !expired && !nameless) continue;
|
|
30110
|
+
if (park.forMs === null && !nameless) continue;
|
|
30111
|
+
const status = park.items.map((i) => {
|
|
30112
|
+
const gone = dead.some((d) => d.kind === i.kind && d.value === i.value);
|
|
30113
|
+
return `- \`${i.kind}: ${i.value}\` \u2014 ${gone ? "NOT RUNNING" : "still running"}`;
|
|
30114
|
+
});
|
|
30115
|
+
const retired = retiredAwaitingKindsIn(tele.lastFence.body ?? "");
|
|
30116
|
+
const head = retired.length > 0 ? [
|
|
30117
|
+
`\u26D4 Your \`\`\`awaiting fence uses ${retired.length === 1 ? "a line kind" : "line kinds"} that no longer exist, so frizz ignored ${retired.length === 1 ? "it" : "them"} \u2014 the`,
|
|
30118
|
+
"fence named nothing and your thread stayed in the queue.",
|
|
30119
|
+
"",
|
|
30120
|
+
...retired.map((k) => `- \`${k}:\` is GONE \u2192 ${RETIRED_AWAITING_REPLACEMENT[k]}`),
|
|
30121
|
+
"",
|
|
30122
|
+
"THE ONLY LINE KINDS NOW SUPPORTED, and every one is checked against something frizz can see:",
|
|
30123
|
+
"",
|
|
30124
|
+
" shell: <runtime task id> a background shell you launched",
|
|
30125
|
+
" agent: <runtime agent id> a sub-agent you dispatched",
|
|
30126
|
+
" timer: tmr_\u2026 a timer from `mcp__frizz__timer`",
|
|
30127
|
+
" pr: owner/repo#123 a PR registered with `mcp__frizz__watch_pr`",
|
|
30128
|
+
" for: 2h REQUIRED \u2014 a DURATION, never an instant",
|
|
30129
|
+
" reason: one line the only prose",
|
|
30130
|
+
"",
|
|
30131
|
+
"`mcp__frizz__activity` lists everything you have running with the exact id each line needs.",
|
|
30132
|
+
"If you are not waiting on anything, you are not awaiting \u2014 end with ```done, or ask a ```question."
|
|
30133
|
+
].join("\n") : nameless ? [
|
|
30134
|
+
"\u26A0\uFE0F Your ```awaiting fence names nothing to wait on, so it is not a park \u2014 your thread is still",
|
|
30135
|
+
"in the queue, and nothing will wake you.",
|
|
30136
|
+
"",
|
|
30137
|
+
"A wait has to be something frizz can SEE. Register one, then name it:",
|
|
30138
|
+
"",
|
|
30139
|
+
"- a pull request \u2192 `mcp__frizz__watch_pr` (it wakes you on CI going green or red, and on every",
|
|
30140
|
+
" later review and comment) \u2192 `pr: owner/repo#123`",
|
|
30141
|
+
"- a wall-clock check \u2192 `mcp__frizz__timer` \u2192 `timer: tmr_\u2026`",
|
|
30142
|
+
"- work you already launched \u2192 `shell: <the id your runtime gave you>` or `agent: <id>`",
|
|
30143
|
+
"",
|
|
30144
|
+
"Use `mcp__frizz__activity` to read back everything you have running, with the exact id each",
|
|
30145
|
+
"line needs.",
|
|
30146
|
+
"",
|
|
30147
|
+
"AND IF YOU ARE NOT WAITING ON ANYTHING, you are not awaiting \u2014 you are done. End with ```done,",
|
|
30148
|
+
"or ask a ```question if you need the human."
|
|
30149
|
+
].join("\n") : expired ? [
|
|
30150
|
+
"\u23F0 Your wait expired, nothing resolved. Check back in on everything.",
|
|
30151
|
+
"",
|
|
30152
|
+
...status,
|
|
30153
|
+
"",
|
|
30154
|
+
"Re-park if they are genuinely still going \u2014 there is no limit on that, and a long job is not a",
|
|
30155
|
+
"failure. If something is finished, read its result. If nothing is left, end in ```done or ask a",
|
|
30156
|
+
"```question."
|
|
30157
|
+
].join("\n") : [
|
|
30158
|
+
`\u26A0\uFE0F Your \`\`\`awaiting fence names ${dead.length === 1 ? "something that is" : "things that are"} not running, so it is not a park and your thread stayed in the queue.`,
|
|
30159
|
+
"",
|
|
30160
|
+
...status,
|
|
30161
|
+
"",
|
|
30162
|
+
"Use `mcp__frizz__activity` to read back what you actually have out, with the exact id each line",
|
|
30163
|
+
"needs, then re-fence naming only those \u2014 or end in ```done or a ```question if there is nothing",
|
|
30164
|
+
"left to wait for."
|
|
30165
|
+
].join("\n");
|
|
30166
|
+
const cause = retired.length > 0 ? "retired" : nameless ? "nameless" : expired ? "expired" : "dead";
|
|
30167
|
+
const ops = cause === "expired" ? [] : liveOpsLines({
|
|
30168
|
+
shells: (tele.bgShells ?? []).filter((sh) => sh.state === "running").map((sh) => ({ id: sh.taskId ?? sh.id, label: sh.label })),
|
|
30169
|
+
subAgents: (tele.subAgents ?? []).filter((a) => a.state === "running").map((a) => ({ id: a.id, label: a.label })),
|
|
30170
|
+
timers: deps.storage.listThreadTimers(row.slug, { armedOnly: true }).map((t) => ({ id: t.id, label: t.prompt.trim().replace(/\s+/g, " ").slice(0, 80) })),
|
|
30171
|
+
prs: deps.storage.listPrWatches(row.slug, { armedOnly: true }).map((w) => ({ id: `${w.owner}/${w.repo}#${w.number}`, label: `${w.owner}/${w.repo}#${w.number}` }))
|
|
30172
|
+
});
|
|
30173
|
+
const message = ops.length > 0 ? `${head}
|
|
30174
|
+
${ops.join("\n")}` : cause === "expired" ? head : `${head}
|
|
30175
|
+
|
|
30176
|
+
You have NOTHING running right now \u2014 no shell, no sub-agent, no timer, no registered
|
|
30177
|
+
pull request. There is nothing that could wake you, so this thread is not awaiting: finish in
|
|
30178
|
+
\`\`\`done, or ask a \`\`\`question.`;
|
|
30179
|
+
if (cause !== "expired" && (row.park_bumps ?? 0) >= PARK_BUMP_MAX) continue;
|
|
30180
|
+
const fenceId = parkFenceId(cause, spokeAt);
|
|
29486
30181
|
const deliveryId = wakeDeliveryId(row.slug, row.session_id, fenceId);
|
|
29487
30182
|
if (outbox.get(deliveryId)) continue;
|
|
29488
30183
|
const item = outbox.enqueue({
|
|
@@ -29490,17 +30185,199 @@ function createScheduler(deps) {
|
|
|
29490
30185
|
slug: row.slug,
|
|
29491
30186
|
sessionId: row.session_id,
|
|
29492
30187
|
fenceId,
|
|
29493
|
-
hintKey:
|
|
29494
|
-
message
|
|
29495
|
-
reason: `
|
|
30188
|
+
hintKey: fenceId,
|
|
30189
|
+
message,
|
|
30190
|
+
reason: retired.length > 0 ? `awaiting fence uses retired kind(s): ${retired.join(", ")}` : nameless ? "awaiting park names nothing" : expired ? "awaiting park expired" : `awaiting park named ${dead.length} dead item(s)`
|
|
29496
30191
|
}, nowMs).delivery;
|
|
30192
|
+
if (cause !== "expired") deps.storage.countParkBump(row.slug, fenceId);
|
|
29497
30193
|
log3(`waker: queued ${row.slug} \u2014 ${item.reason}`);
|
|
29498
30194
|
checkpoint("after-enqueue", item);
|
|
29499
30195
|
}
|
|
29500
30196
|
}
|
|
29501
|
-
|
|
29502
|
-
|
|
29503
|
-
|
|
30197
|
+
const prWatchPolledAt = /* @__PURE__ */ new Map();
|
|
30198
|
+
function readPrWatchCursor(raw2) {
|
|
30199
|
+
try {
|
|
30200
|
+
const parsed = raw2 ? JSON.parse(raw2) : null;
|
|
30201
|
+
if (!parsed || typeof parsed !== "object") return { seen: [] };
|
|
30202
|
+
const seen = Array.isArray(parsed.seen) ? parsed.seen.filter((x) => typeof x === "string") : [];
|
|
30203
|
+
return { seen, checks: typeof parsed.checks === "string" ? parsed.checks : void 0, report: Number(parsed.report) || 0 };
|
|
30204
|
+
} catch {
|
|
30205
|
+
return { seen: [] };
|
|
30206
|
+
}
|
|
30207
|
+
}
|
|
30208
|
+
async function evalPrWatches(nowMs) {
|
|
30209
|
+
for (const w of deps.storage.expiredPrWatches(nowMs)) {
|
|
30210
|
+
const row = deps.storage.getSession(w.thread_slug);
|
|
30211
|
+
deps.storage.settlePrWatch(w.id, nowMs);
|
|
30212
|
+
const ref = `${w.owner}/${w.repo}#${w.number}`;
|
|
30213
|
+
log3(`waker: settled ${w.thread_slug} \u2014 pr watcher ${w.id} expired (${ref})`);
|
|
30214
|
+
if (!row || row.state === "archived" || row.archived === 1) continue;
|
|
30215
|
+
const fenceId = prWatchExpiredFenceId(w.id);
|
|
30216
|
+
const deliveryId = wakeDeliveryId(row.slug, row.session_id, fenceId);
|
|
30217
|
+
if (outbox.get(deliveryId)) continue;
|
|
30218
|
+
const item = outbox.enqueue({
|
|
30219
|
+
id: deliveryId,
|
|
30220
|
+
slug: row.slug,
|
|
30221
|
+
sessionId: row.session_id,
|
|
30222
|
+
fenceId,
|
|
30223
|
+
hintKey: fenceId,
|
|
30224
|
+
message: `\u23F0 Your watcher on ${ref} has expired and is no longer armed \u2014 nothing on that PR will wake you now.
|
|
30225
|
+
|
|
30226
|
+
If you still care about it, register it again with \`mcp__frizz__watch_pr\` and a fresh \`for:\`. If you do not, and it was the only thing you were waiting on, end in a proper terminal state instead of parking on it again.`,
|
|
30227
|
+
reason: `pr watcher ${w.id} expired (${ref})`
|
|
30228
|
+
}, nowMs).delivery;
|
|
30229
|
+
log3(`waker: queued ${row.slug} \u2014 ${item.reason}`);
|
|
30230
|
+
checkpoint("after-enqueue", item);
|
|
30231
|
+
}
|
|
30232
|
+
const armed = deps.storage.armedPrWatches();
|
|
30233
|
+
if (armed.length === 0) return;
|
|
30234
|
+
const refs = /* @__PURE__ */ new Map();
|
|
30235
|
+
for (const w of armed) {
|
|
30236
|
+
const key = `${w.owner}/${w.repo}#${w.number}`;
|
|
30237
|
+
const last = prWatchPolledAt.get(key) ?? 0;
|
|
30238
|
+
if (nowMs - last < PR_WATCH_POLL_MS) continue;
|
|
30239
|
+
refs.set(key, { owner: w.owner, repo: w.repo, number: w.number });
|
|
30240
|
+
}
|
|
30241
|
+
const status = /* @__PURE__ */ new Map();
|
|
30242
|
+
const activity = /* @__PURE__ */ new Map();
|
|
30243
|
+
await Promise.all([...refs].map(async ([key, ref]) => {
|
|
30244
|
+
prWatchPolledAt.set(key, nowMs);
|
|
30245
|
+
try {
|
|
30246
|
+
const pr = await fetchPr(ref);
|
|
30247
|
+
if (pr) {
|
|
30248
|
+
status.set(key, githubWatchStatus(pr, new Date(nowMs).toISOString()));
|
|
30249
|
+
publishGithubStatus(key, pr, nowMs);
|
|
30250
|
+
}
|
|
30251
|
+
} catch (err) {
|
|
30252
|
+
log3(`waker: pr-watch status fetch failed for ${key}: ${err instanceof Error ? err.message : String(err)}`);
|
|
30253
|
+
}
|
|
30254
|
+
try {
|
|
30255
|
+
const result = normalizeReviewResult(await fetchGithubReview(ref));
|
|
30256
|
+
if (result.status === "ok") {
|
|
30257
|
+
activity.set(key, result.activity);
|
|
30258
|
+
recordReviewSuccess(key, "pr-watch registry");
|
|
30259
|
+
} else if (result.status === "error") recordReviewFailure(key, "pr-watch registry", result, nowMs);
|
|
30260
|
+
} catch (err) {
|
|
30261
|
+
recordReviewFailure(key, "pr-watch registry", {
|
|
30262
|
+
status: "error",
|
|
30263
|
+
failure: { kind: "network", message: err instanceof Error ? err.message : String(err) }
|
|
30264
|
+
}, nowMs);
|
|
30265
|
+
}
|
|
30266
|
+
}));
|
|
30267
|
+
for (const w of armed) {
|
|
30268
|
+
const row = deps.storage.getSession(w.thread_slug);
|
|
30269
|
+
if (!row || row.state === "archived" || row.archived === 1) continue;
|
|
30270
|
+
const key = `${w.owner}/${w.repo}#${w.number}`;
|
|
30271
|
+
const st = status.get(key);
|
|
30272
|
+
const acts = activity.get(key);
|
|
30273
|
+
if (!st && !acts) continue;
|
|
30274
|
+
const cursor = readPrWatchCursor(w.cursor);
|
|
30275
|
+
if (st && st.state !== "open") {
|
|
30276
|
+
deps.storage.settlePrWatch(w.id, nowMs);
|
|
30277
|
+
enqueuePrWatchWake(row, w.id, nextReport(cursor), prWatchWakeMessage({
|
|
30278
|
+
target: key,
|
|
30279
|
+
merged: st.state === "merged",
|
|
30280
|
+
closed: st.state === "closed"
|
|
30281
|
+
}), `pr-watch ${key} ${st.state}`, nowMs);
|
|
30282
|
+
continue;
|
|
30283
|
+
}
|
|
30284
|
+
const terminal = st && (st.checks === "passing" || st.checks === "failing") ? st.checks : void 0;
|
|
30285
|
+
const stamp = terminal !== void 0 ? `${st?.head ?? "?"}:${terminal}:${st?.failureSig ?? ""}` : void 0;
|
|
30286
|
+
const checksChanged = stamp !== void 0 && cursor.checks !== stamp;
|
|
30287
|
+
const seen = new Set(cursor.seen);
|
|
30288
|
+
const firstPoll = w.cursor === null;
|
|
30289
|
+
const newestFirst = [...acts ?? []].sort((a, b) => {
|
|
30290
|
+
const at = Date.parse(b.at ?? "") - Date.parse(a.at ?? "");
|
|
30291
|
+
return Number.isFinite(at) && at !== 0 ? at : b.id.localeCompare(a.id);
|
|
30292
|
+
});
|
|
30293
|
+
const fresh = newestFirst.filter((a) => {
|
|
30294
|
+
if (seen.has(a.id)) return false;
|
|
30295
|
+
if (!firstPoll) return true;
|
|
30296
|
+
const landed = Date.parse(a.at ?? "");
|
|
30297
|
+
return Number.isFinite(landed) && landed > w.created_at;
|
|
30298
|
+
});
|
|
30299
|
+
const named = fresh.slice(0, REVIEW_STEER_CAP);
|
|
30300
|
+
const reviewSteer = fresh.length > 0 ? formatGithubWakeSteer({
|
|
30301
|
+
ref: key,
|
|
30302
|
+
omitted: fresh.length - named.length,
|
|
30303
|
+
items: [...named].reverse().map((a) => ({
|
|
30304
|
+
label: activityLabel(a),
|
|
30305
|
+
actor: a.actor,
|
|
30306
|
+
bot: isBotGithubActor(a),
|
|
30307
|
+
...a.at ? { at: a.at } : {},
|
|
30308
|
+
...a.url ? { url: a.url } : {}
|
|
30309
|
+
}))
|
|
30310
|
+
}) : void 0;
|
|
30311
|
+
const nextCursor = {
|
|
30312
|
+
seen: acts ? [.../* @__PURE__ */ new Set([...cursor.seen, ...acts.map((a) => a.id)])].slice(-REVIEW_SEEN_CAP) : cursor.seen,
|
|
30313
|
+
// The STAMP, so the next poll compares against the commit as well as the colour.
|
|
30314
|
+
checks: stamp ?? cursor.checks,
|
|
30315
|
+
report: cursor.report ?? 0
|
|
30316
|
+
};
|
|
30317
|
+
if (!checksChanged && !reviewSteer) {
|
|
30318
|
+
if (JSON.stringify(nextCursor) !== JSON.stringify(cursor)) {
|
|
30319
|
+
deps.storage.setPrWatchCursor(w.id, JSON.stringify(nextCursor));
|
|
30320
|
+
}
|
|
30321
|
+
continue;
|
|
30322
|
+
}
|
|
30323
|
+
deps.storage.setSnoozedUntil(row.slug, null);
|
|
30324
|
+
const report = nextReport(cursor);
|
|
30325
|
+
enqueuePrWatchWake(row, w.id, report, prWatchWakeMessage({
|
|
30326
|
+
target: key,
|
|
30327
|
+
...checksChanged && terminal ? { checks: { verdict: terminal, passed: st.passed, failed: st.failed, failing: st.failing } } : {},
|
|
30328
|
+
...reviewSteer ? { review: reviewSteer } : {}
|
|
30329
|
+
}), `pr-watch ${key}${checksChanged ? ` CI ${terminal}` : ""}${reviewSteer ? " review" : ""}`, nowMs);
|
|
30330
|
+
deps.storage.setPrWatchCursor(w.id, JSON.stringify({ ...nextCursor, report }));
|
|
30331
|
+
}
|
|
30332
|
+
}
|
|
30333
|
+
function nextReport(cursor) {
|
|
30334
|
+
return (cursor.report ?? 0) + 1;
|
|
30335
|
+
}
|
|
30336
|
+
function enqueuePrWatchWake(row, watchId, report, message, reason, nowMs) {
|
|
30337
|
+
const fenceId = prWatchFenceId(watchId, report);
|
|
30338
|
+
const deliveryId = wakeDeliveryId(row.slug, row.session_id, fenceId);
|
|
30339
|
+
if (outbox.get(deliveryId)) return;
|
|
30340
|
+
const item = outbox.enqueue({
|
|
30341
|
+
id: deliveryId,
|
|
30342
|
+
slug: row.slug,
|
|
30343
|
+
sessionId: row.session_id,
|
|
30344
|
+
fenceId,
|
|
30345
|
+
hintKey: `${PR_WATCH_FENCE_PREFIX}:${watchId}`,
|
|
30346
|
+
message,
|
|
30347
|
+
reason
|
|
30348
|
+
}, nowMs).delivery;
|
|
30349
|
+
log3(`waker: queued ${row.slug} \u2014 ${item.reason}`);
|
|
30350
|
+
checkpoint("after-enqueue", item);
|
|
30351
|
+
}
|
|
30352
|
+
function evalShellCompletions(nowMs) {
|
|
30353
|
+
for (const row of deps.storage.allSessions()) {
|
|
30354
|
+
if (row.state === "archived" || row.archived === 1) continue;
|
|
30355
|
+
const tele = deps.tailer.get(row.slug);
|
|
30356
|
+
if (!tele) continue;
|
|
30357
|
+
if (tele.turn !== "idle") continue;
|
|
30358
|
+
if (tele.authFault) continue;
|
|
30359
|
+
const restedAt = Date.parse(tele.lastAssistantAt ?? "");
|
|
30360
|
+
if (!Number.isFinite(restedAt)) continue;
|
|
30361
|
+
for (const shell of tele.retiredShells ?? []) {
|
|
30362
|
+
const finishedAt = Date.parse(shell.finishedAt ?? "");
|
|
30363
|
+
if (!Number.isFinite(finishedAt) || finishedAt <= restedAt) continue;
|
|
30364
|
+
const fenceId = shellFenceId(shell.id);
|
|
30365
|
+
const deliveryId = wakeDeliveryId(row.slug, row.session_id, fenceId);
|
|
30366
|
+
if (outbox.get(deliveryId)) continue;
|
|
30367
|
+
const item = outbox.enqueue({
|
|
30368
|
+
id: deliveryId,
|
|
30369
|
+
slug: row.slug,
|
|
30370
|
+
sessionId: row.session_id,
|
|
30371
|
+
fenceId,
|
|
30372
|
+
hintKey: `${SHELL_FENCE_PREFIX}:${shell.id}`,
|
|
30373
|
+
message: shellDoneMessage(shell),
|
|
30374
|
+
reason: `background shell finished (${shell.taskId ?? shell.label})`
|
|
30375
|
+
}, nowMs).delivery;
|
|
30376
|
+
log3(`waker: queued ${row.slug} \u2014 ${item.reason}`);
|
|
30377
|
+
checkpoint("after-enqueue", item);
|
|
30378
|
+
return;
|
|
30379
|
+
}
|
|
30380
|
+
}
|
|
29504
30381
|
}
|
|
29505
30382
|
function evalTimers(nowMs) {
|
|
29506
30383
|
for (const timer2 of deps.storage.dueThreadTimers(nowMs)) {
|
|
@@ -29534,7 +30411,6 @@ function createScheduler(deps) {
|
|
|
29534
30411
|
if (!armed || armed.dueAtMs > nowMs) continue;
|
|
29535
30412
|
const beatTele = deps.tailer.get(row.slug);
|
|
29536
30413
|
if (beatTele && saidDone(beatTele)) continue;
|
|
29537
|
-
if (heldByQuestion(row, beatTele)) continue;
|
|
29538
30414
|
if (openSchedulePrompt(row.slug, row.session_id)) continue;
|
|
29539
30415
|
const beat = beatIndex(row.recurring_schedule_fired_at ?? null, armed);
|
|
29540
30416
|
const fenceId = heartbeatFenceId(armed.armedAt, beat);
|
|
@@ -29583,8 +30459,7 @@ function createScheduler(deps) {
|
|
|
29583
30459
|
if (!restedAt) continue;
|
|
29584
30460
|
if (tele.lastUserAt && Date.parse(tele.lastUserAt) >= Date.parse(restedAt)) continue;
|
|
29585
30461
|
if (tele.authFault) continue;
|
|
29586
|
-
if (restMessageIsSignedOff(tele)) continue;
|
|
29587
|
-
if (heldByQuestion(row, tele)) continue;
|
|
30462
|
+
if (restMessageIsSignedOff(tele, registeredPrWatchesOf(deps.storage, row.slug), armedTimerIdsOf(deps.storage, row.slug))) continue;
|
|
29588
30463
|
const fenceId = stopHookFenceId(armed.armedAt, restedAt);
|
|
29589
30464
|
const deliveryId = wakeDeliveryId(row.slug, row.session_id, fenceId);
|
|
29590
30465
|
if (outbox.get(deliveryId)) continue;
|
|
@@ -29594,7 +30469,7 @@ function createScheduler(deps) {
|
|
|
29594
30469
|
sessionId: row.session_id,
|
|
29595
30470
|
fenceId,
|
|
29596
30471
|
hintKey: STOP_HOOK_HINT_KEY,
|
|
29597
|
-
message: restPromptMessage(armed.prompt),
|
|
30472
|
+
message: restPromptMessage(armed.prompt, { overQuestion: tele.pendingQuestion === true }),
|
|
29598
30473
|
reason: "recurring prompt at rest"
|
|
29599
30474
|
}, nowMs).delivery;
|
|
29600
30475
|
log3(`waker: queued ${row.slug} \u2014 ${item.reason}`);
|
|
@@ -29609,7 +30484,6 @@ function createScheduler(deps) {
|
|
|
29609
30484
|
const tele = deps.tailer.get(row.slug);
|
|
29610
30485
|
if (!tele?.lastCompactionAt) continue;
|
|
29611
30486
|
if (tele.lastCompactionAt <= armed.armedAt) continue;
|
|
29612
|
-
if (heldByQuestion(row, tele)) continue;
|
|
29613
30487
|
const fenceId = compactFenceId(armed.armedAt, tele.lastCompactionAt);
|
|
29614
30488
|
const deliveryId = wakeDeliveryId(row.slug, row.session_id, fenceId);
|
|
29615
30489
|
if (outbox.get(deliveryId)) continue;
|
|
@@ -29659,7 +30533,6 @@ function createScheduler(deps) {
|
|
|
29659
30533
|
settleRestPrompt(item);
|
|
29660
30534
|
settleCompactPrompt(item);
|
|
29661
30535
|
settleTimer(item);
|
|
29662
|
-
settleWatcher(item);
|
|
29663
30536
|
continue;
|
|
29664
30537
|
}
|
|
29665
30538
|
if (context === "superseded") {
|
|
@@ -29679,7 +30552,6 @@ function createScheduler(deps) {
|
|
|
29679
30552
|
if (recovered?.state === "exhausted") {
|
|
29680
30553
|
settleSnooze(item);
|
|
29681
30554
|
settleTimer(item);
|
|
29682
|
-
settleWatcher(item);
|
|
29683
30555
|
log3(`waker: delivery EXHAUSTED for ${item.slug} after ${recovered.attempts} attempts \u2014 ${recovered.lastError ?? "unknown error"}`);
|
|
29684
30556
|
}
|
|
29685
30557
|
}
|
|
@@ -29698,7 +30570,6 @@ function createScheduler(deps) {
|
|
|
29698
30570
|
settleRestPrompt(item);
|
|
29699
30571
|
settleCompactPrompt(item);
|
|
29700
30572
|
settleTimer(item);
|
|
29701
|
-
settleWatcher(item);
|
|
29702
30573
|
continue;
|
|
29703
30574
|
}
|
|
29704
30575
|
if (context === "superseded") {
|
|
@@ -29726,7 +30597,6 @@ function createScheduler(deps) {
|
|
|
29726
30597
|
outbox.supersede(item.id, failedAt, message);
|
|
29727
30598
|
settleSnooze(item);
|
|
29728
30599
|
settleTimer(item);
|
|
29729
|
-
settleWatcher(item);
|
|
29730
30600
|
log3(`waker: delivery ABANDONED for ${item.slug} (terminal, no retry): ${message}`);
|
|
29731
30601
|
continue;
|
|
29732
30602
|
}
|
|
@@ -29752,7 +30622,6 @@ function createScheduler(deps) {
|
|
|
29752
30622
|
settleRestPrompt(item);
|
|
29753
30623
|
settleCompactPrompt(item);
|
|
29754
30624
|
settleTimer(item);
|
|
29755
|
-
settleWatcher(item);
|
|
29756
30625
|
settleSignoffNudge(item);
|
|
29757
30626
|
const acknowledged = outbox.get(item.id);
|
|
29758
30627
|
if (acknowledged) checkpoint("after-ack", acknowledged);
|
|
@@ -29821,15 +30690,22 @@ function createScheduler(deps) {
|
|
|
29821
30690
|
}
|
|
29822
30691
|
try {
|
|
29823
30692
|
evalSignoffNudges(now());
|
|
30693
|
+
evalParkIntegrity(now());
|
|
29824
30694
|
} catch (err) {
|
|
29825
30695
|
if (err instanceof InjectedSchedulerCrash) throw err;
|
|
29826
30696
|
log3(`waker: sign-off nudge pass failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
29827
30697
|
}
|
|
29828
30698
|
try {
|
|
29829
|
-
|
|
30699
|
+
await evalPrWatches(now());
|
|
30700
|
+
} catch (err) {
|
|
30701
|
+
if (err instanceof InjectedSchedulerCrash) throw err;
|
|
30702
|
+
log3(`waker: pr-watch registry pass failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
30703
|
+
}
|
|
30704
|
+
try {
|
|
30705
|
+
evalShellCompletions(now());
|
|
29830
30706
|
} catch (err) {
|
|
29831
30707
|
if (err instanceof InjectedSchedulerCrash) throw err;
|
|
29832
|
-
log3(`waker:
|
|
30708
|
+
log3(`waker: shell-completion pass failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
29833
30709
|
}
|
|
29834
30710
|
try {
|
|
29835
30711
|
evalTimers(now());
|
|
@@ -29881,11 +30757,12 @@ function createScheduler(deps) {
|
|
|
29881
30757
|
tick
|
|
29882
30758
|
};
|
|
29883
30759
|
}
|
|
29884
|
-
var execFileAsync4, PR_REF_RE2, LIMIT_FENCE_PREFIX, LIMIT_HINT_PREFIX, LIMIT_RESUME_GRACE_MS, LIMIT_RESUME_HEADROOM_PERCENT, LIMIT_HEADROOM_MIN_FAULT_AGE_MS, REPORT_FENCE_PREFIX, REPORT_HINT_PREFIX, SNOOZE_FENCE_PREFIX, SNOOZE_HINT_PREFIX, HEARTBEAT_FENCE_PREFIX, HEARTBEAT_HINT_PREFIX, STOP_HOOK_FENCE_PREFIX, STOP_HOOK_HINT_KEY, COMPACT_FENCE_PREFIX, COMPACT_HINT_KEY, TIMER_FENCE_PREFIX, TIMER_HINT_PREFIX, SIGNOFF_FENCE_PREFIX, SIGNOFF_HINT_KEY, SIGNOFF_NUDGE_MAX,
|
|
30760
|
+
var execFileAsync4, PR_REF_RE2, LIMIT_FENCE_PREFIX, LIMIT_HINT_PREFIX, LIMIT_RESUME_GRACE_MS, LIMIT_RESUME_HEADROOM_PERCENT, LIMIT_HEADROOM_MIN_FAULT_AGE_MS, REPORT_FENCE_PREFIX, REPORT_HINT_PREFIX, SNOOZE_FENCE_PREFIX, SNOOZE_HINT_PREFIX, HEARTBEAT_FENCE_PREFIX, HEARTBEAT_HINT_PREFIX, STOP_HOOK_FENCE_PREFIX, STOP_HOOK_HINT_KEY, COMPACT_FENCE_PREFIX, COMPACT_HINT_KEY, TIMER_FENCE_PREFIX, TIMER_HINT_PREFIX, SHELL_FENCE_PREFIX, PR_WATCH_FENCE_PREFIX, PR_WATCH_EXPIRED_FENCE_PREFIX, PR_WATCH_POLL_MS, SIGNOFF_FENCE_PREFIX, SIGNOFF_HINT_KEY, SIGNOFF_NUDGE_MAX, PARK_BUMP_MAX, SIGNOFF_NUDGE_SETTING, PARK_FENCE_PREFIX, FIRED_CAP, GITHUB_STATUS_CAP, REGISTRATION_CAP, REVIEW_SEEN_CAP, INTRODUCED_CAP, REVIEW_STEER_CAP;
|
|
29885
30761
|
var init_scheduler = __esm({
|
|
29886
30762
|
"packages/server/src/scheduler.ts"() {
|
|
29887
30763
|
"use strict";
|
|
29888
30764
|
init_src();
|
|
30765
|
+
init_awaiting();
|
|
29889
30766
|
init_usage_limit();
|
|
29890
30767
|
init_wake_store();
|
|
29891
30768
|
init_shutdown();
|
|
@@ -29912,14 +30789,18 @@ var init_scheduler = __esm({
|
|
|
29912
30789
|
COMPACT_HINT_KEY = "recurring:compaction";
|
|
29913
30790
|
TIMER_FENCE_PREFIX = "timer";
|
|
29914
30791
|
TIMER_HINT_PREFIX = "timer:";
|
|
30792
|
+
SHELL_FENCE_PREFIX = "shell";
|
|
30793
|
+
PR_WATCH_FENCE_PREFIX = "prwatch";
|
|
30794
|
+
PR_WATCH_EXPIRED_FENCE_PREFIX = "prwatch-expired";
|
|
30795
|
+
PR_WATCH_POLL_MS = 6e4;
|
|
29915
30796
|
SIGNOFF_FENCE_PREFIX = "signoff";
|
|
29916
30797
|
SIGNOFF_HINT_KEY = "signoff:rest";
|
|
29917
30798
|
SIGNOFF_NUDGE_MAX = 2;
|
|
30799
|
+
PARK_BUMP_MAX = 3;
|
|
29918
30800
|
SIGNOFF_NUDGE_SETTING = "signoffNudge";
|
|
29919
|
-
|
|
29920
|
-
WATCH_HINT_PREFIX = "watch:";
|
|
29921
|
-
SHELL_SEEN = "seen";
|
|
30801
|
+
PARK_FENCE_PREFIX = "park";
|
|
29922
30802
|
FIRED_CAP = 500;
|
|
30803
|
+
GITHUB_STATUS_CAP = 200;
|
|
29923
30804
|
REGISTRATION_CAP = 500;
|
|
29924
30805
|
REVIEW_SEEN_CAP = 300;
|
|
29925
30806
|
INTRODUCED_CAP = 500;
|
|
@@ -30628,7 +31509,7 @@ var init_orphan_reaper = __esm({
|
|
|
30628
31509
|
// packages/server/src/context.ts
|
|
30629
31510
|
import { join as join29 } from "node:path";
|
|
30630
31511
|
import { randomUUID as randomUUID15 } from "node:crypto";
|
|
30631
|
-
import { homedir as
|
|
31512
|
+
import { homedir as homedir13 } from "node:os";
|
|
30632
31513
|
function reconcileSessions(storage) {
|
|
30633
31514
|
for (const row of storage.allSessions()) {
|
|
30634
31515
|
if (isHeadlessRow(row)) continue;
|
|
@@ -30782,7 +31663,7 @@ async function createContext(opts = {}) {
|
|
|
30782
31663
|
}
|
|
30783
31664
|
}
|
|
30784
31665
|
function createContextUnchecked(opts, resources) {
|
|
30785
|
-
const home = opts.home ??
|
|
31666
|
+
const home = opts.home ?? homedir13();
|
|
30786
31667
|
const project = opts.project ?? resolveProject();
|
|
30787
31668
|
const dbPath = join29(project.stateDir, "ui.db");
|
|
30788
31669
|
const storage = createStorage(dbPath);
|
|
@@ -31356,11 +32237,11 @@ var init_url = __esm({
|
|
|
31356
32237
|
}
|
|
31357
32238
|
return null;
|
|
31358
32239
|
};
|
|
31359
|
-
tryDecode = (
|
|
32240
|
+
tryDecode = (str2, decoder) => {
|
|
31360
32241
|
try {
|
|
31361
|
-
return decoder(
|
|
32242
|
+
return decoder(str2);
|
|
31362
32243
|
} catch {
|
|
31363
|
-
return
|
|
32244
|
+
return str2.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match2) => {
|
|
31364
32245
|
try {
|
|
31365
32246
|
return decoder(match2);
|
|
31366
32247
|
} catch {
|
|
@@ -31369,7 +32250,7 @@ var init_url = __esm({
|
|
|
31369
32250
|
});
|
|
31370
32251
|
}
|
|
31371
32252
|
};
|
|
31372
|
-
tryDecodeURI = (
|
|
32253
|
+
tryDecodeURI = (str2) => tryDecode(str2, decodeURI);
|
|
31373
32254
|
getPath = (request) => {
|
|
31374
32255
|
const url = request.url;
|
|
31375
32256
|
const start = url.indexOf("/", url.indexOf(":") + 4);
|
|
@@ -31517,7 +32398,7 @@ var init_request = __esm({
|
|
|
31517
32398
|
init_constants();
|
|
31518
32399
|
init_body();
|
|
31519
32400
|
init_url();
|
|
31520
|
-
tryDecodeURIComponent = (
|
|
32401
|
+
tryDecodeURIComponent = (str2) => tryDecode(str2, decodeURIComponent_);
|
|
31521
32402
|
HonoRequest = class {
|
|
31522
32403
|
/**
|
|
31523
32404
|
* `.raw` can get the raw Request object.
|
|
@@ -31816,27 +32697,27 @@ var init_html = __esm({
|
|
|
31816
32697
|
escapedString.callbacks = callbacks;
|
|
31817
32698
|
return escapedString;
|
|
31818
32699
|
};
|
|
31819
|
-
resolveCallback = async (
|
|
31820
|
-
if (typeof
|
|
31821
|
-
if (!(
|
|
31822
|
-
|
|
32700
|
+
resolveCallback = async (str2, phase, preserveCallbacks, context, buffer) => {
|
|
32701
|
+
if (typeof str2 === "object" && !(str2 instanceof String)) {
|
|
32702
|
+
if (!(str2 instanceof Promise)) {
|
|
32703
|
+
str2 = str2.toString();
|
|
31823
32704
|
}
|
|
31824
|
-
if (
|
|
31825
|
-
|
|
32705
|
+
if (str2 instanceof Promise) {
|
|
32706
|
+
str2 = await str2;
|
|
31826
32707
|
}
|
|
31827
32708
|
}
|
|
31828
|
-
const callbacks =
|
|
32709
|
+
const callbacks = str2.callbacks;
|
|
31829
32710
|
if (!callbacks?.length) {
|
|
31830
|
-
return Promise.resolve(
|
|
32711
|
+
return Promise.resolve(str2);
|
|
31831
32712
|
}
|
|
31832
32713
|
if (buffer) {
|
|
31833
|
-
buffer[0] +=
|
|
32714
|
+
buffer[0] += str2;
|
|
31834
32715
|
} else {
|
|
31835
|
-
buffer = [
|
|
32716
|
+
buffer = [str2];
|
|
31836
32717
|
}
|
|
31837
32718
|
const resStr = Promise.all(callbacks.map((c) => c({ phase, buffer, context }))).then(
|
|
31838
32719
|
(res) => Promise.all(
|
|
31839
|
-
res.filter(Boolean).map((
|
|
32720
|
+
res.filter(Boolean).map((str22) => resolveCallback(str22, phase, false, context, buffer))
|
|
31840
32721
|
).then(() => buffer[0])
|
|
31841
32722
|
);
|
|
31842
32723
|
if (preserveCallbacks) {
|
|
@@ -34006,11 +34887,14 @@ import { closeSync as closeSync13, existsSync as existsSync11, fstatSync as fsta
|
|
|
34006
34887
|
import { createHash as createHash15 } from "node:crypto";
|
|
34007
34888
|
import { StringDecoder as StringDecoder4 } from "node:string_decoder";
|
|
34008
34889
|
import { join as join30 } from "node:path";
|
|
34009
|
-
import { homedir as
|
|
34890
|
+
import { homedir as homedir14 } from "node:os";
|
|
34010
34891
|
function isInjectedNoise(text) {
|
|
34011
34892
|
const t = text.trimStart();
|
|
34012
34893
|
return NOISE_PREFIXES.some((p) => t.startsWith(p));
|
|
34013
34894
|
}
|
|
34895
|
+
function isInterruptMarker(text) {
|
|
34896
|
+
return INTERRUPT_MARKERS.includes(text.trim());
|
|
34897
|
+
}
|
|
34014
34898
|
function formatTokens(n) {
|
|
34015
34899
|
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
34016
34900
|
if (n >= 1e3) return `${Math.round(n / 1e3)}k`;
|
|
@@ -34141,6 +35025,7 @@ function createTranscriptFold(identityPrefix = "claude") {
|
|
|
34141
35025
|
const pendingTools = /* @__PURE__ */ new Map();
|
|
34142
35026
|
const agentDispatches = /* @__PURE__ */ new Map();
|
|
34143
35027
|
const backgroundShells = /* @__PURE__ */ new Map();
|
|
35028
|
+
const consumedShells = /* @__PURE__ */ new Map();
|
|
34144
35029
|
const backgroundTaskIds = /* @__PURE__ */ new Map();
|
|
34145
35030
|
const relayedWakesDrawn = /* @__PURE__ */ new Set();
|
|
34146
35031
|
const childDispatchIds = /* @__PURE__ */ new Map();
|
|
@@ -34148,9 +35033,11 @@ function createTranscriptFold(identityPrefix = "claude") {
|
|
|
34148
35033
|
const queuedPending = [];
|
|
34149
35034
|
let deliveredDedupe = null;
|
|
34150
35035
|
let pendingRest = null;
|
|
35036
|
+
const restEpoch = { n: 0 };
|
|
34151
35037
|
function flushRest() {
|
|
34152
35038
|
if (!pendingRest) return;
|
|
34153
35039
|
out.push(restMessage(pendingRest.sourceId, pendingRest.at));
|
|
35040
|
+
restEpoch.n++;
|
|
34154
35041
|
pendingRest = null;
|
|
34155
35042
|
lastAssistantId = null;
|
|
34156
35043
|
}
|
|
@@ -34194,7 +35081,7 @@ function createTranscriptFold(identityPrefix = "claude") {
|
|
|
34194
35081
|
if (pendingRest && !(rec.type === "assistant" && typeof rec.message?.id === "string" && rec.message.id === pendingRest.msgId)) {
|
|
34195
35082
|
flushRest();
|
|
34196
35083
|
}
|
|
34197
|
-
const evs = completionEvents(rec, agentDispatches, backgroundShells, backgroundTaskIds, relayedWakesDrawn);
|
|
35084
|
+
const evs = completionEvents(rec, agentDispatches, backgroundShells, backgroundTaskIds, relayedWakesDrawn, consumedShells, restEpoch);
|
|
34198
35085
|
let drewEvents = false;
|
|
34199
35086
|
if (evs.length > 0) {
|
|
34200
35087
|
attachToolResults(rec, pendingTools, backgroundShells, backgroundTaskIds, childDispatchIds);
|
|
@@ -34246,8 +35133,8 @@ function createTranscriptFold(identityPrefix = "claude") {
|
|
|
34246
35133
|
}
|
|
34247
35134
|
const peerOrigin = att.origin?.kind === "peer" ? att.origin : void 0;
|
|
34248
35135
|
if (peerOrigin && att.commandMode === "prompt") {
|
|
34249
|
-
const
|
|
34250
|
-
const senderTaskId =
|
|
35136
|
+
const str2 = (v) => typeof v === "string" ? v.trim() : "";
|
|
35137
|
+
const senderTaskId = str2(peerOrigin.senderTaskId);
|
|
34251
35138
|
const dispatchId = senderTaskId ? childDispatchIds.get(senderTaskId) : void 0;
|
|
34252
35139
|
const entry = prompt.trim() ? findQueued(prompt) : void 0;
|
|
34253
35140
|
if (entry) {
|
|
@@ -34263,7 +35150,7 @@ function createTranscriptFold(identityPrefix = "claude") {
|
|
|
34263
35150
|
} else {
|
|
34264
35151
|
const parsed = prompt.trim() ? parseAgentMessage(prompt) : void 0;
|
|
34265
35152
|
const describedHere = dispatchId ? dispatchLabels.get(dispatchId) : void 0;
|
|
34266
|
-
const from = describedHere ||
|
|
35153
|
+
const from = describedHere || str2(peerOrigin.from) || str2(peerOrigin.name) || parsed?.from || "";
|
|
34267
35154
|
const body = parsed?.body ?? (typeof peerOrigin.body === "string" ? peerOrigin.body : "");
|
|
34268
35155
|
if (from && body.trim()) {
|
|
34269
35156
|
out.push({
|
|
@@ -34307,6 +35194,7 @@ function createTranscriptFold(identityPrefix = "claude") {
|
|
|
34307
35194
|
}
|
|
34308
35195
|
let text = userText(rec);
|
|
34309
35196
|
if (text && isAllInjectedNoise(text)) return;
|
|
35197
|
+
if (text && isInterruptMarker(text)) return;
|
|
34310
35198
|
if (text) {
|
|
34311
35199
|
const queued = findQueued(text)?.message;
|
|
34312
35200
|
if (queued) {
|
|
@@ -34442,9 +35330,14 @@ ${block.text}` : block.text;
|
|
|
34442
35330
|
return {
|
|
34443
35331
|
ingest,
|
|
34444
35332
|
finalize,
|
|
35333
|
+
// The SAME window the paged reader returns (latestWindowStart), because this is the OTHER producer:
|
|
35334
|
+
// the /ws push renders from here while the RPC page renders from there, and a push whose head sat
|
|
35335
|
+
// further forward than the page's would splice the human's ask straight back out of a card that had
|
|
35336
|
+
// just anchored on it (reconcileLiveMessages replaces the window wholesale).
|
|
34445
35337
|
messages: () => {
|
|
34446
35338
|
const all = projected();
|
|
34447
|
-
|
|
35339
|
+
const start = latestWindowStart(all);
|
|
35340
|
+
return start === 0 ? all : all.slice(start);
|
|
34448
35341
|
},
|
|
34449
35342
|
allMessages: projected,
|
|
34450
35343
|
consumedBytes: () => offset + Buffer.byteLength(buffer)
|
|
@@ -34874,7 +35767,7 @@ function notificationCarrierText(rec) {
|
|
|
34874
35767
|
}
|
|
34875
35768
|
return void 0;
|
|
34876
35769
|
}
|
|
34877
|
-
function completionEvents(rec, dispatches, backgroundShells, backgroundTaskIds, relayedWakesDrawn) {
|
|
35770
|
+
function completionEvents(rec, dispatches, backgroundShells, backgroundTaskIds, relayedWakesDrawn, consumedShells = /* @__PURE__ */ new Map(), restEpoch = { n: 0 }) {
|
|
34878
35771
|
const carrier = notificationCarrierText(rec);
|
|
34879
35772
|
const raw2 = carrier === void 0 ? void 0 : splitWakeDeliveries(carrier).map((s) => relayNotificationBlock(s) ?? s).join("\n");
|
|
34880
35773
|
if (!raw2 || !raw2.includes("<task-notification>")) return [];
|
|
@@ -34908,10 +35801,17 @@ function completionEvents(rec, dispatches, backgroundShells, backgroundTaskIds,
|
|
|
34908
35801
|
if (!d) {
|
|
34909
35802
|
const shell = backgroundShells.get(id);
|
|
34910
35803
|
if (!shell) {
|
|
35804
|
+
const drawn = consumedShells.get(id);
|
|
35805
|
+
if (drawn && restEpoch.n > drawn.epoch) {
|
|
35806
|
+
consumedShells.set(id, { epoch: restEpoch.n, call: drawn.call });
|
|
35807
|
+
out.push({ role: "assistant", kind: "event", boundary: "wake", text: backgroundWakeLabel(drawn.call, status, block), tools: [], parts: [], at });
|
|
35808
|
+
continue;
|
|
35809
|
+
}
|
|
34911
35810
|
relayedFallback();
|
|
34912
35811
|
continue;
|
|
34913
35812
|
}
|
|
34914
35813
|
backgroundShells.delete(id);
|
|
35814
|
+
consumedShells.set(id, { epoch: restEpoch.n, call: shell.call });
|
|
34915
35815
|
const elapsedMs2 = elapsedBetween(shell.at, rec.timestamp);
|
|
34916
35816
|
shell.call.status = status === "completed" ? "completed" : status === "killed" ? "cancelled" : "failed";
|
|
34917
35817
|
if (elapsedMs2 !== void 0) shell.call.durationMs = elapsedMs2;
|
|
@@ -35015,7 +35915,7 @@ function verifyIncrementalParse(path, identityPrefix, incremental) {
|
|
|
35015
35915
|
}
|
|
35016
35916
|
}
|
|
35017
35917
|
function logDirOf(project) {
|
|
35018
|
-
return join30(
|
|
35918
|
+
return join30(homedir14(), ".claude", "projects", project.cwdSlug);
|
|
35019
35919
|
}
|
|
35020
35920
|
function resolveTranscriptPath(project, sessionId) {
|
|
35021
35921
|
const path = join30(logDirOf(project), `${sessionId}.jsonl`);
|
|
@@ -35267,8 +36167,30 @@ function parseCodexTranscript(raw2, identityPrefix = "codex") {
|
|
|
35267
36167
|
const out = projectCodexTranscript(raw2, identityPrefix);
|
|
35268
36168
|
return latestTranscriptWindow(out);
|
|
35269
36169
|
}
|
|
36170
|
+
function latestWindowStart(messages) {
|
|
36171
|
+
const tail = Math.max(0, messages.length - MAX_MESSAGES);
|
|
36172
|
+
if (tail === 0) return 0;
|
|
36173
|
+
let boundary = -1;
|
|
36174
|
+
for (let i = tail - 1; i >= 0; i--) {
|
|
36175
|
+
const m = messages[i];
|
|
36176
|
+
if (m.role === "user" && !m.wake && !m.queued) {
|
|
36177
|
+
boundary = i;
|
|
36178
|
+
break;
|
|
36179
|
+
}
|
|
36180
|
+
}
|
|
36181
|
+
if (boundary < 0) return tail;
|
|
36182
|
+
let start = tail;
|
|
36183
|
+
let bytes = 0;
|
|
36184
|
+
while (start > boundary && tail - start < LATEST_WINDOW_ASK_REACH_ITEMS) {
|
|
36185
|
+
const nextBytes = messageBytes(messages[start - 1]);
|
|
36186
|
+
if (bytes + nextBytes > LATEST_WINDOW_ASK_REACH_BYTES) break;
|
|
36187
|
+
start--;
|
|
36188
|
+
bytes += nextBytes;
|
|
36189
|
+
}
|
|
36190
|
+
return start === boundary ? start : tail;
|
|
36191
|
+
}
|
|
35270
36192
|
function latestTranscriptWindow(messages) {
|
|
35271
|
-
const start =
|
|
36193
|
+
const start = latestWindowStart(messages);
|
|
35272
36194
|
if (start === 0) return [...messages];
|
|
35273
36195
|
const pinned = [];
|
|
35274
36196
|
for (let i = 0; i < start; i++) {
|
|
@@ -36161,7 +37083,7 @@ function readLatestThreadTranscriptPage(project, storage, slug, backendFor) {
|
|
|
36161
37083
|
}
|
|
36162
37084
|
}
|
|
36163
37085
|
const latest = latestTranscriptWindow(projected);
|
|
36164
|
-
const canonicalStart =
|
|
37086
|
+
const canonicalStart = latestWindowStart(projected);
|
|
36165
37087
|
const row = storage.getSession(slug);
|
|
36166
37088
|
const pageMessages = row ? projectDeliveryLedger(latest, parseDeliveryLedger(row.delivery_ledger)) : latest;
|
|
36167
37089
|
return {
|
|
@@ -36302,7 +37224,7 @@ function readTranscriptFile(absPath) {
|
|
|
36302
37224
|
return [];
|
|
36303
37225
|
}
|
|
36304
37226
|
}
|
|
36305
|
-
var MAX_MESSAGES, NOISE_PREFIXES, REST_LABEL, LEGACY_TASK_MARKER, LEGACY_BANNER_PREAMBLE, COMMAND_NAME_RE, COMMAND_ARGS_RE, PEER_SESSION_PREAMBLE, QUEUED_STALE_MS, EDIT_CAP, TRUNC_MARKER, ENCRYPTED_PAYLOAD, AGENT_PROMPT_CAP, SEND_BODY_CAP, COMMAND_CAP, TOOL_INPUT_CAP, READ_LINE_CAP, READ_BYTE_CAP, IMAGE_MEDIA_EXT, SCREENSHOT_CACHE_DIR, SCREENSHOT_CACHE_MAX, SCREENSHOT_MAX_BASE64, screenshotTmpSeq, SENT_IMAGE_MAX_BYTES, SENT_FILES_MAX, TODO_ROWS_MAX, TODO_TEXT_CAP, TRANSCRIPT_CACHE_CAP, transcriptCache, PARSE_VERIFY_SAMPLE, TRANSCRIPT_DECODE_WINDOW, FOREIGN_SESSION_ID_RE, MAX_PINNED_BACKGROUND_OPERATIONS, CAPTION_MAX, _defaultCodexBackend, TRANSCRIPT_EARLIER_MAX_ITEMS, TRANSCRIPT_EARLIER_MAX_BYTES, fullDigestCache, FULL_DIGEST_CACHE_CAP, EMPTY_RETIRED_OPS;
|
|
37227
|
+
var MAX_MESSAGES, LATEST_WINDOW_ASK_REACH_ITEMS, LATEST_WINDOW_ASK_REACH_BYTES, NOISE_PREFIXES, INTERRUPT_MARKERS, REST_LABEL, LEGACY_TASK_MARKER, LEGACY_BANNER_PREAMBLE, COMMAND_NAME_RE, COMMAND_ARGS_RE, PEER_SESSION_PREAMBLE, QUEUED_STALE_MS, EDIT_CAP, TRUNC_MARKER, ENCRYPTED_PAYLOAD, AGENT_PROMPT_CAP, SEND_BODY_CAP, COMMAND_CAP, TOOL_INPUT_CAP, READ_LINE_CAP, READ_BYTE_CAP, IMAGE_MEDIA_EXT, SCREENSHOT_CACHE_DIR, SCREENSHOT_CACHE_MAX, SCREENSHOT_MAX_BASE64, screenshotTmpSeq, SENT_IMAGE_MAX_BYTES, SENT_FILES_MAX, TODO_ROWS_MAX, TODO_TEXT_CAP, TRANSCRIPT_CACHE_CAP, transcriptCache, PARSE_VERIFY_SAMPLE, TRANSCRIPT_DECODE_WINDOW, FOREIGN_SESSION_ID_RE, MAX_PINNED_BACKGROUND_OPERATIONS, CAPTION_MAX, _defaultCodexBackend, TRANSCRIPT_EARLIER_MAX_ITEMS, TRANSCRIPT_EARLIER_MAX_BYTES, fullDigestCache, FULL_DIGEST_CACHE_CAP, EMPTY_RETIRED_OPS;
|
|
36306
37228
|
var init_transcript = __esm({
|
|
36307
37229
|
"packages/server/src/transcript.ts"() {
|
|
36308
37230
|
"use strict";
|
|
@@ -36317,7 +37239,10 @@ var init_transcript = __esm({
|
|
|
36317
37239
|
init_bash_background();
|
|
36318
37240
|
init_frizz_paths();
|
|
36319
37241
|
MAX_MESSAGES = 300;
|
|
37242
|
+
LATEST_WINDOW_ASK_REACH_ITEMS = 100;
|
|
37243
|
+
LATEST_WINDOW_ASK_REACH_BYTES = 512 * 1024;
|
|
36320
37244
|
NOISE_PREFIXES = ["<task-notification>", "[SYSTEM NOTIFICATION", "<system-reminder>", "<frizz-", "[frizz]"];
|
|
37245
|
+
INTERRUPT_MARKERS = ["[Request interrupted by user]", "[Request interrupted by user for tool use]"];
|
|
36321
37246
|
REST_LABEL = "Agent rested";
|
|
36322
37247
|
LEGACY_TASK_MARKER = "\nTASK:\n";
|
|
36323
37248
|
LEGACY_BANNER_PREAMBLE = "Everything ABOVE this line is frizz system orientation. Everything BELOW the `TASK:` marker is the human operator's own prompt, verbatim.";
|
|
@@ -36394,7 +37319,7 @@ var init_open_external = __esm({
|
|
|
36394
37319
|
// packages/server/src/local-file.ts
|
|
36395
37320
|
import { spawn as spawn6 } from "node:child_process";
|
|
36396
37321
|
import { readFileSync as readFileSync20, realpathSync as realpathSync8, statSync as statSync15 } from "node:fs";
|
|
36397
|
-
import { homedir as
|
|
37322
|
+
import { homedir as homedir15 } from "node:os";
|
|
36398
37323
|
import { isAbsolute as isAbsolute3, join as join31, resolve as resolve7, sep } from "node:path";
|
|
36399
37324
|
function isUnder(real, root) {
|
|
36400
37325
|
let rootReal;
|
|
@@ -36422,7 +37347,7 @@ function resolveLocalFile(rawPath, roots) {
|
|
|
36422
37347
|
}
|
|
36423
37348
|
return real;
|
|
36424
37349
|
}
|
|
36425
|
-
function resolveOpenableFile(raw2, projectDir, roots, home =
|
|
37350
|
+
function resolveOpenableFile(raw2, projectDir, roots, home = homedir15()) {
|
|
36426
37351
|
const trimmed = raw2.trim().replace(/:\d+(?::\d+)?$/, "");
|
|
36427
37352
|
if (!trimmed) return null;
|
|
36428
37353
|
const abs = trimmed === "~" ? home : trimmed.startsWith("~/") ? join31(home, trimmed.slice(2)) : isAbsolute3(trimmed) ? trimmed : resolve7(projectDir, trimmed);
|
|
@@ -36462,6 +37387,286 @@ var init_local_file = __esm({
|
|
|
36462
37387
|
}
|
|
36463
37388
|
});
|
|
36464
37389
|
|
|
37390
|
+
// packages/server/src/github-hovercard.ts
|
|
37391
|
+
function parseRef(ref) {
|
|
37392
|
+
const match2 = REF_PATTERN.exec(ref);
|
|
37393
|
+
if (!match2) return null;
|
|
37394
|
+
const [, owner, name, number, sha] = match2;
|
|
37395
|
+
if (number) {
|
|
37396
|
+
const n = Number(number);
|
|
37397
|
+
if (!Number.isSafeInteger(n) || n <= 0 || n > 2147483647) return null;
|
|
37398
|
+
return { key: ref, owner, name, number: n };
|
|
37399
|
+
}
|
|
37400
|
+
return { key: ref, owner, name, sha };
|
|
37401
|
+
}
|
|
37402
|
+
function isImmutableRef(ref) {
|
|
37403
|
+
return ref.sha !== void 0;
|
|
37404
|
+
}
|
|
37405
|
+
function buildRefQuery(refs) {
|
|
37406
|
+
const variables = {};
|
|
37407
|
+
const declarations = [];
|
|
37408
|
+
const selections = [];
|
|
37409
|
+
refs.forEach((ref, index) => {
|
|
37410
|
+
declarations.push(`$owner${index}: String!`, `$name${index}: String!`);
|
|
37411
|
+
variables[`owner${index}`] = ref.owner;
|
|
37412
|
+
variables[`name${index}`] = ref.name;
|
|
37413
|
+
if (ref.number !== void 0) {
|
|
37414
|
+
declarations.push(`$number${index}: Int!`);
|
|
37415
|
+
variables[`number${index}`] = ref.number;
|
|
37416
|
+
selections.push(` a${index}: repository(owner: $owner${index}, name: $name${index}) {
|
|
37417
|
+
target: issueOrPullRequest(number: $number${index}) {${ISSUE_FIELDS}
|
|
37418
|
+
}
|
|
37419
|
+
}`);
|
|
37420
|
+
} else {
|
|
37421
|
+
declarations.push(`$rev${index}: String!`);
|
|
37422
|
+
variables[`rev${index}`] = ref.sha;
|
|
37423
|
+
selections.push(` a${index}: repository(owner: $owner${index}, name: $name${index}) {
|
|
37424
|
+
target: object(expression: $rev${index}) {${COMMIT_FIELDS}
|
|
37425
|
+
}
|
|
37426
|
+
}`);
|
|
37427
|
+
}
|
|
37428
|
+
});
|
|
37429
|
+
return { query: `query(${declarations.join(", ")}) {
|
|
37430
|
+
${selections.join("\n")}
|
|
37431
|
+
}`, variables };
|
|
37432
|
+
}
|
|
37433
|
+
function str(v) {
|
|
37434
|
+
return typeof v === "string" && v ? v : void 0;
|
|
37435
|
+
}
|
|
37436
|
+
function int(v) {
|
|
37437
|
+
return typeof v === "number" && Number.isFinite(v) && v >= 0 ? Math.trunc(v) : void 0;
|
|
37438
|
+
}
|
|
37439
|
+
function excerptBody(body) {
|
|
37440
|
+
if (!body) return "";
|
|
37441
|
+
const cleaned = body.replace(/<!--[\s\S]*?-->/g, "").trim();
|
|
37442
|
+
if (cleaned.length <= BODY_BUDGET) return cleaned;
|
|
37443
|
+
return `${cleaned.slice(0, BODY_BUDGET).trimEnd()}\u2026`;
|
|
37444
|
+
}
|
|
37445
|
+
function labelsOf(v) {
|
|
37446
|
+
const nodes = v?.nodes;
|
|
37447
|
+
if (!Array.isArray(nodes)) return [];
|
|
37448
|
+
const out = [];
|
|
37449
|
+
for (const node of nodes) {
|
|
37450
|
+
const name = str(node?.name);
|
|
37451
|
+
if (name) out.push({ name, color: str(node?.color) ?? "" });
|
|
37452
|
+
}
|
|
37453
|
+
return out;
|
|
37454
|
+
}
|
|
37455
|
+
function cardFromNode(ref, node, fetchedAt) {
|
|
37456
|
+
if (!node || typeof node !== "object") return null;
|
|
37457
|
+
const n = node;
|
|
37458
|
+
const repo = `${ref.owner}/${ref.name}`;
|
|
37459
|
+
const typename = str(n.__typename);
|
|
37460
|
+
if (typename === "Commit") {
|
|
37461
|
+
const headline = str(n.messageHeadline) ?? "";
|
|
37462
|
+
if (!headline) return null;
|
|
37463
|
+
const author2 = n.author ?? {};
|
|
37464
|
+
const user = author2.user ?? {};
|
|
37465
|
+
return {
|
|
37466
|
+
ref: ref.key,
|
|
37467
|
+
kind: "commit",
|
|
37468
|
+
repo,
|
|
37469
|
+
url: str(n.url) ?? `https://github.com/${repo}/commit/${str(n.oid) ?? ref.sha}`,
|
|
37470
|
+
title: headline,
|
|
37471
|
+
body: excerptBody(str(n.messageBody)),
|
|
37472
|
+
state: "",
|
|
37473
|
+
at: str(n.committedDate),
|
|
37474
|
+
...str(user.login) ? { authorLogin: str(user.login) } : {},
|
|
37475
|
+
...str(author2.name) ? { authorName: str(author2.name) } : {},
|
|
37476
|
+
// The LINKED ACCOUNT's avatar first. A commit's `GitActor.avatarUrl` is derived from the
|
|
37477
|
+
// committer email, so for an author with no matching account it is a Gravatar behind GitHub's
|
|
37478
|
+
// camo image proxy — a different host, and a generic silhouette rather than the person.
|
|
37479
|
+
...str(user.avatarUrl) || str(author2.avatarUrl) ? { authorAvatar: str(user.avatarUrl) ?? str(author2.avatarUrl) } : {},
|
|
37480
|
+
labels: [],
|
|
37481
|
+
...int(n.additions) !== void 0 ? { additions: int(n.additions) } : {},
|
|
37482
|
+
...int(n.deletions) !== void 0 ? { deletions: int(n.deletions) } : {},
|
|
37483
|
+
...int(n.changedFilesIfAvailable) !== void 0 ? { changedFiles: int(n.changedFilesIfAvailable) } : {},
|
|
37484
|
+
fetchedAt
|
|
37485
|
+
};
|
|
37486
|
+
}
|
|
37487
|
+
if (typename !== "Issue" && typename !== "PullRequest") return null;
|
|
37488
|
+
const title = str(n.title);
|
|
37489
|
+
if (!title) return null;
|
|
37490
|
+
const author = n.author ?? {};
|
|
37491
|
+
const state = n.isDraft === true && str(n.state)?.toUpperCase() === "OPEN" ? "DRAFT" : str(n.state)?.toUpperCase() ?? "";
|
|
37492
|
+
return {
|
|
37493
|
+
ref: ref.key,
|
|
37494
|
+
kind: typename === "Issue" ? "issue" : "pr",
|
|
37495
|
+
repo,
|
|
37496
|
+
url: str(n.url) ?? `https://github.com/${repo}/issues/${ref.number}`,
|
|
37497
|
+
title,
|
|
37498
|
+
body: excerptBody(str(n.body)),
|
|
37499
|
+
state,
|
|
37500
|
+
...str(n.stateReason) ? { stateReason: str(n.stateReason).toUpperCase() } : {},
|
|
37501
|
+
at: str(n.createdAt),
|
|
37502
|
+
...str(author.login) ? { authorLogin: str(author.login) } : {},
|
|
37503
|
+
...str(author.avatarUrl) ? { authorAvatar: str(author.avatarUrl) } : {},
|
|
37504
|
+
labels: labelsOf(n.labels),
|
|
37505
|
+
...int(n.additions) !== void 0 ? { additions: int(n.additions) } : {},
|
|
37506
|
+
...int(n.deletions) !== void 0 ? { deletions: int(n.deletions) } : {},
|
|
37507
|
+
...int(n.changedFiles) !== void 0 ? { changedFiles: int(n.changedFiles) } : {},
|
|
37508
|
+
...int(n.comments?.totalCount) !== void 0 ? { comments: int(n.comments.totalCount) } : {},
|
|
37509
|
+
fetchedAt
|
|
37510
|
+
};
|
|
37511
|
+
}
|
|
37512
|
+
function parseRefResponse(raw2, refs, fetchedAt) {
|
|
37513
|
+
const data = raw2?.data;
|
|
37514
|
+
const cards = [];
|
|
37515
|
+
const missing = [];
|
|
37516
|
+
refs.forEach((ref, index) => {
|
|
37517
|
+
const alias = data?.[`a${index}`] ?? null;
|
|
37518
|
+
const card = cardFromNode(ref, alias?.target, fetchedAt);
|
|
37519
|
+
if (card) cards.push(card);
|
|
37520
|
+
else missing.push(ref.key);
|
|
37521
|
+
});
|
|
37522
|
+
return { cards, missing };
|
|
37523
|
+
}
|
|
37524
|
+
function conciseError2(error) {
|
|
37525
|
+
const raw2 = error instanceof Error ? error.message : String(error);
|
|
37526
|
+
return raw2.trim().split(/\r?\n/, 1)[0]?.slice(0, 300) || "unknown error";
|
|
37527
|
+
}
|
|
37528
|
+
function createGithubHovercardService(deps = {}) {
|
|
37529
|
+
const getToken = deps.getToken ?? defaultGetToken;
|
|
37530
|
+
const request = deps.request ?? globalThis.fetch;
|
|
37531
|
+
const now = deps.now ?? Date.now;
|
|
37532
|
+
const cache = /* @__PURE__ */ new Map();
|
|
37533
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
37534
|
+
let tokenPromise;
|
|
37535
|
+
const token = async () => {
|
|
37536
|
+
if (!tokenPromise) {
|
|
37537
|
+
tokenPromise = getToken();
|
|
37538
|
+
tokenPromise.catch(() => {
|
|
37539
|
+
tokenPromise = void 0;
|
|
37540
|
+
});
|
|
37541
|
+
}
|
|
37542
|
+
return tokenPromise;
|
|
37543
|
+
};
|
|
37544
|
+
const isFresh = (ref, entry, refresh) => {
|
|
37545
|
+
if (isImmutableRef(ref) && entry.card) return true;
|
|
37546
|
+
if (refresh) return false;
|
|
37547
|
+
return now() - entry.at < MUTABLE_TTL_MS;
|
|
37548
|
+
};
|
|
37549
|
+
const fetchChunk = async (refs) => {
|
|
37550
|
+
const authToken = await token();
|
|
37551
|
+
const { query: query2, variables } = buildRefQuery(refs);
|
|
37552
|
+
const controller = new AbortController();
|
|
37553
|
+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS2);
|
|
37554
|
+
timeout.unref?.();
|
|
37555
|
+
try {
|
|
37556
|
+
const response = await request("https://api.github.com/graphql", {
|
|
37557
|
+
method: "POST",
|
|
37558
|
+
headers: {
|
|
37559
|
+
accept: "application/vnd.github+json",
|
|
37560
|
+
authorization: `Bearer ${authToken}`,
|
|
37561
|
+
"content-type": "application/json",
|
|
37562
|
+
"user-agent": "frizz-hovercard",
|
|
37563
|
+
"x-github-api-version": "2022-11-28"
|
|
37564
|
+
},
|
|
37565
|
+
body: JSON.stringify({ query: query2, variables }),
|
|
37566
|
+
signal: controller.signal
|
|
37567
|
+
});
|
|
37568
|
+
const text = await response.text();
|
|
37569
|
+
if (!response.ok) {
|
|
37570
|
+
if (response.status === 401 || response.status === 403) tokenPromise = void 0;
|
|
37571
|
+
throw new Error(`GitHub GraphQL returned HTTP ${response.status}`);
|
|
37572
|
+
}
|
|
37573
|
+
let body;
|
|
37574
|
+
try {
|
|
37575
|
+
body = JSON.parse(text);
|
|
37576
|
+
} catch {
|
|
37577
|
+
throw new Error("GitHub GraphQL returned malformed JSON");
|
|
37578
|
+
}
|
|
37579
|
+
const at = now();
|
|
37580
|
+
const { cards, missing } = parseRefResponse(body, refs, at);
|
|
37581
|
+
for (const card of cards) cache.set(card.ref, { card, at });
|
|
37582
|
+
for (const key of missing) cache.set(key, { missing: true, at });
|
|
37583
|
+
} finally {
|
|
37584
|
+
clearTimeout(timeout);
|
|
37585
|
+
}
|
|
37586
|
+
};
|
|
37587
|
+
return {
|
|
37588
|
+
size: () => cache.size,
|
|
37589
|
+
async preview(refs, opts = {}) {
|
|
37590
|
+
const refresh = opts.refresh === true;
|
|
37591
|
+
const parsed = /* @__PURE__ */ new Map();
|
|
37592
|
+
const missing = [];
|
|
37593
|
+
for (const raw2 of refs) {
|
|
37594
|
+
if (parsed.has(raw2)) continue;
|
|
37595
|
+
const ref = parseRef(raw2);
|
|
37596
|
+
if (ref) parsed.set(raw2, ref);
|
|
37597
|
+
else missing.push(raw2);
|
|
37598
|
+
}
|
|
37599
|
+
const wanted = [];
|
|
37600
|
+
const joins = [];
|
|
37601
|
+
for (const ref of parsed.values()) {
|
|
37602
|
+
const entry = cache.get(ref.key);
|
|
37603
|
+
if (entry && isFresh(ref, entry, refresh)) continue;
|
|
37604
|
+
const pending = inflight.get(ref.key);
|
|
37605
|
+
if (pending && !refresh) joins.push(pending);
|
|
37606
|
+
else wanted.push(ref);
|
|
37607
|
+
}
|
|
37608
|
+
let error;
|
|
37609
|
+
if (wanted.length > 0) {
|
|
37610
|
+
const chunks = [];
|
|
37611
|
+
for (let i = 0; i < wanted.length; i += MAX_REFS_PER_QUERY) chunks.push(wanted.slice(i, i + MAX_REFS_PER_QUERY));
|
|
37612
|
+
const runs = chunks.map((chunk) => {
|
|
37613
|
+
const run3 = fetchChunk(chunk);
|
|
37614
|
+
for (const ref of chunk) inflight.set(ref.key, run3);
|
|
37615
|
+
return run3.finally(() => {
|
|
37616
|
+
for (const ref of chunk) if (inflight.get(ref.key) === run3) inflight.delete(ref.key);
|
|
37617
|
+
});
|
|
37618
|
+
});
|
|
37619
|
+
const settled = await Promise.allSettled([...runs, ...joins]);
|
|
37620
|
+
const failure = settled.find((r) => r.status === "rejected");
|
|
37621
|
+
if (failure && failure.status === "rejected") error = conciseError2(failure.reason);
|
|
37622
|
+
} else if (joins.length > 0) {
|
|
37623
|
+
const settled = await Promise.allSettled(joins);
|
|
37624
|
+
const failure = settled.find((r) => r.status === "rejected");
|
|
37625
|
+
if (failure && failure.status === "rejected") error = conciseError2(failure.reason);
|
|
37626
|
+
}
|
|
37627
|
+
const cards = [];
|
|
37628
|
+
for (const ref of parsed.values()) {
|
|
37629
|
+
const entry = cache.get(ref.key);
|
|
37630
|
+
if (entry?.card) cards.push(entry.card);
|
|
37631
|
+
else if (entry?.missing) missing.push(ref.key);
|
|
37632
|
+
}
|
|
37633
|
+
return { cards, missing, ...error ? { error } : {} };
|
|
37634
|
+
}
|
|
37635
|
+
};
|
|
37636
|
+
}
|
|
37637
|
+
var REQUEST_TIMEOUT_MS2, MAX_REFS_PER_QUERY, BODY_BUDGET, MUTABLE_TTL_MS, REF_SEG, REF_PATTERN, ISSUE_FIELDS, COMMIT_FIELDS;
|
|
37638
|
+
var init_github_hovercard = __esm({
|
|
37639
|
+
"packages/server/src/github-hovercard.ts"() {
|
|
37640
|
+
"use strict";
|
|
37641
|
+
init_github_review();
|
|
37642
|
+
REQUEST_TIMEOUT_MS2 = 15e3;
|
|
37643
|
+
MAX_REFS_PER_QUERY = 25;
|
|
37644
|
+
BODY_BUDGET = 400;
|
|
37645
|
+
MUTABLE_TTL_MS = 5 * 6e4;
|
|
37646
|
+
REF_SEG = "[A-Za-z0-9][A-Za-z0-9._-]*";
|
|
37647
|
+
REF_PATTERN = new RegExp(`^(${REF_SEG})/(${REF_SEG})(?:#([1-9]\\d{0,9})|@([0-9a-f]{7,40}))$`);
|
|
37648
|
+
ISSUE_FIELDS = `
|
|
37649
|
+
... on Issue {
|
|
37650
|
+
__typename number title body state stateReason url createdAt
|
|
37651
|
+
author { login avatarUrl }
|
|
37652
|
+
labels(first: 6) { nodes { name color } }
|
|
37653
|
+
comments { totalCount }
|
|
37654
|
+
}
|
|
37655
|
+
... on PullRequest {
|
|
37656
|
+
__typename number title body state isDraft url createdAt additions deletions changedFiles
|
|
37657
|
+
author { login avatarUrl }
|
|
37658
|
+
labels(first: 6) { nodes { name color } }
|
|
37659
|
+
comments { totalCount }
|
|
37660
|
+
}`;
|
|
37661
|
+
COMMIT_FIELDS = `
|
|
37662
|
+
... on Commit {
|
|
37663
|
+
__typename oid abbreviatedOid url messageHeadline messageBody committedDate
|
|
37664
|
+
additions deletions changedFilesIfAvailable
|
|
37665
|
+
author { name avatarUrl user { login avatarUrl } }
|
|
37666
|
+
}`;
|
|
37667
|
+
}
|
|
37668
|
+
});
|
|
37669
|
+
|
|
36465
37670
|
// packages/server/src/backend/account-actions.ts
|
|
36466
37671
|
import { execFile as execFile8 } from "node:child_process";
|
|
36467
37672
|
import { promisify as promisify7 } from "node:util";
|
|
@@ -36897,7 +38102,7 @@ import { join as join32, resolve as resolve8 } from "node:path";
|
|
|
36897
38102
|
import { randomUUID as randomUUID16 } from "node:crypto";
|
|
36898
38103
|
import { basename as basename10, dirname as dirname14 } from "node:path";
|
|
36899
38104
|
import { existsSync as existsSync12, mkdirSync as mkdirSync16, rmSync as rmSync9, writeFileSync as writeFileSync12 } from "node:fs";
|
|
36900
|
-
import { homedir as
|
|
38105
|
+
import { homedir as homedir16 } from "node:os";
|
|
36901
38106
|
function validateGithubDispatchProfile(input) {
|
|
36902
38107
|
validateThreadProfile(input.backend, input.model, input.effort);
|
|
36903
38108
|
}
|
|
@@ -37056,7 +38261,7 @@ function projectCard(entry, stale) {
|
|
|
37056
38261
|
function addProjectAtPath(input) {
|
|
37057
38262
|
const typed = input.trim();
|
|
37058
38263
|
if (!typed) throw new Error("Enter a folder path.");
|
|
37059
|
-
const expanded = typed === "~" || typed.startsWith("~/") ? join32(
|
|
38264
|
+
const expanded = typed === "~" || typed.startsWith("~/") ? join32(homedir16(), typed.slice(1)) : typed;
|
|
37060
38265
|
const absolute = resolve8(expanded);
|
|
37061
38266
|
let stats;
|
|
37062
38267
|
try {
|
|
@@ -37066,7 +38271,7 @@ function addProjectAtPath(input) {
|
|
|
37066
38271
|
}
|
|
37067
38272
|
if (!stats.isDirectory()) throw new Error(`That is a file, not a folder: ${absolute}`);
|
|
37068
38273
|
const root = discoverProjectRoot(absolute);
|
|
37069
|
-
const id = ensureProjectIdFile(root,
|
|
38274
|
+
const id = ensureProjectIdFile(root, homedir16(), existingProjectId(root));
|
|
37070
38275
|
const remoteOwner = resolveProjectLabel(root)?.split("/")[0];
|
|
37071
38276
|
let registered = registerProject({ dir: root, id, remoteOwner });
|
|
37072
38277
|
if (registered.action === "duplicate") {
|
|
@@ -37139,14 +38344,18 @@ function createRouter(ctx) {
|
|
|
37139
38344
|
if (input.prompt === null || input.intervalSeconds === void 0) return null;
|
|
37140
38345
|
return input.intervalSeconds * 1e3;
|
|
37141
38346
|
}
|
|
37142
|
-
function
|
|
37143
|
-
|
|
37144
|
-
|
|
37145
|
-
|
|
37146
|
-
|
|
37147
|
-
|
|
37148
|
-
|
|
37149
|
-
|
|
38347
|
+
function armedPrWatchViews(slug) {
|
|
38348
|
+
const github = readGithubStatusBook(ctx.storage.getSetting(GITHUB_STATUS_SETTING));
|
|
38349
|
+
return ctx.storage.listPrWatches(slug, { armedOnly: true }).map((w) => {
|
|
38350
|
+
const target = `${w.owner}/${w.repo}#${w.number}`;
|
|
38351
|
+
return {
|
|
38352
|
+
id: w.id,
|
|
38353
|
+
target,
|
|
38354
|
+
state: w.state,
|
|
38355
|
+
createdAt: new Date(w.created_at).toISOString(),
|
|
38356
|
+
...github[target] ? { github: github[target] } : {}
|
|
38357
|
+
};
|
|
38358
|
+
});
|
|
37150
38359
|
}
|
|
37151
38360
|
function armedTimerViews(slug) {
|
|
37152
38361
|
return ctx.storage.listThreadTimers(slug, { armedOnly: true }).map((t) => ({
|
|
@@ -37162,16 +38371,14 @@ function createRouter(ctx) {
|
|
|
37162
38371
|
if (!row) throw new Error(`thread ${slug} is not registered`);
|
|
37163
38372
|
const otherOn = trigger === "rest" ? row.recurring_on_schedule === 1 : row.recurring_on_rest === 1;
|
|
37164
38373
|
const compactOn = row.recurring_on_compact === 1;
|
|
37165
|
-
const holdOn = row.recurring_pause_on_questions === 1;
|
|
37166
38374
|
const storedSeconds = row.recurring_interval_ms ? Math.round(row.recurring_interval_ms / 1e3) : void 0;
|
|
37167
38375
|
const on = write.enabled && write.prompt !== null;
|
|
37168
38376
|
const prompt = on ? write.prompt : otherOn || compactOn ? row.recurring_prompt ?? null : null;
|
|
37169
|
-
const next = prompt === null ? { prompt: null, stopHook: false, heartbeat: false, postCompaction: false
|
|
38377
|
+
const next = prompt === null ? { prompt: null, stopHook: false, heartbeat: false, postCompaction: false } : {
|
|
37170
38378
|
prompt,
|
|
37171
38379
|
stopHook: trigger === "rest" ? on : otherOn,
|
|
37172
38380
|
heartbeat: trigger === "schedule" ? on : otherOn,
|
|
37173
38381
|
postCompaction: compactOn,
|
|
37174
|
-
pauseOnQuestions: holdOn,
|
|
37175
38382
|
intervalSeconds: trigger === "schedule" && on ? write.intervalSeconds : storedSeconds
|
|
37176
38383
|
};
|
|
37177
38384
|
assertRecurringPromptArmable(next, row);
|
|
@@ -37180,7 +38387,6 @@ function createRouter(ctx) {
|
|
|
37180
38387
|
stopHook: next.stopHook,
|
|
37181
38388
|
heartbeat: next.heartbeat,
|
|
37182
38389
|
postCompaction: next.postCompaction,
|
|
37183
|
-
pauseOnQuestions: next.pauseOnQuestions,
|
|
37184
38390
|
intervalMs: recurringIntervalMs(next),
|
|
37185
38391
|
armedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
37186
38392
|
})) {
|
|
@@ -38004,7 +39210,6 @@ function createRouter(ctx) {
|
|
|
38004
39210
|
stopHook: input.stopHook,
|
|
38005
39211
|
heartbeat: input.heartbeat,
|
|
38006
39212
|
postCompaction: input.postCompaction,
|
|
38007
|
-
pauseOnQuestions: input.pauseOnQuestions,
|
|
38008
39213
|
intervalMs: recurringIntervalMs(input),
|
|
38009
39214
|
armedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
38010
39215
|
})) {
|
|
@@ -38034,7 +39239,6 @@ function createRouter(ctx) {
|
|
|
38034
39239
|
stopHook: input.stopHook,
|
|
38035
39240
|
heartbeat: input.heartbeat,
|
|
38036
39241
|
postCompaction: input.postCompaction,
|
|
38037
|
-
pauseOnQuestions: input.pauseOnQuestions,
|
|
38038
39242
|
intervalMs: recurringIntervalMs(input),
|
|
38039
39243
|
armedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
38040
39244
|
})) {
|
|
@@ -38111,56 +39315,106 @@ function createRouter(ctx) {
|
|
|
38111
39315
|
output: OwnThreadTimersResult,
|
|
38112
39316
|
handler: async ({ input }) => ({ timers: armedTimerViews(input.slug) })
|
|
38113
39317
|
}),
|
|
38114
|
-
//
|
|
38115
|
-
//
|
|
38116
|
-
//
|
|
38117
|
-
//
|
|
38118
|
-
|
|
38119
|
-
|
|
38120
|
-
|
|
39318
|
+
// EVERY kind of background work this thread has out, with the id its awaiting fence names it by.
|
|
39319
|
+
//
|
|
39320
|
+
// The fence is structural — it references live things by id — so a worker that no longer has those
|
|
39321
|
+
// ids to hand cannot write a correct fence at all, and the failure is silent: it names something
|
|
39322
|
+
// wrong, frizz refuses the park, and the thread queues. That is the exact stall the grammar exists
|
|
39323
|
+
// to prevent, so the ids have to be RETRIEVABLE rather than remembered. Same list the sign-off nudge
|
|
39324
|
+
// prints, from the same source, so the two can never tell a worker different things.
|
|
39325
|
+
//
|
|
39326
|
+
// A shell is keyed by its RUNTIME task id ("Command running in background with ID: bzvtnt3ig"),
|
|
39327
|
+
// never its launch tool_use id: that is the string the worker was actually shown and the one it will
|
|
39328
|
+
// reach for. Falls back to the launch id only when the ack has not landed yet.
|
|
39329
|
+
listOwnThreadActivity: mutation({
|
|
39330
|
+
input: ListOwnThreadActivityInput,
|
|
39331
|
+
output: OwnThreadActivityResult,
|
|
39332
|
+
handler: async ({ input }) => {
|
|
39333
|
+
const tele = ctx.tailer.get(input.slug);
|
|
39334
|
+
const activity = [];
|
|
39335
|
+
for (const sh of tele?.bgShells ?? []) {
|
|
39336
|
+
if (sh.state !== "running") continue;
|
|
39337
|
+
const id = sh.taskId ?? sh.id;
|
|
39338
|
+
if (id) activity.push({ kind: "shell", id, label: sh.label, since: sh.startedAt });
|
|
39339
|
+
}
|
|
39340
|
+
for (const a of tele?.subAgents ?? []) {
|
|
39341
|
+
if (a.state !== "running") continue;
|
|
39342
|
+
if (a.id) activity.push({ kind: "agent", id: a.id, label: a.label, since: a.startedAt });
|
|
39343
|
+
}
|
|
39344
|
+
for (const t of ctx.storage.listThreadTimers(input.slug, { armedOnly: true })) {
|
|
39345
|
+
activity.push({
|
|
39346
|
+
kind: "timer",
|
|
39347
|
+
id: t.id,
|
|
39348
|
+
label: t.prompt.trim().replace(/\s+/g, " ").slice(0, 120),
|
|
39349
|
+
since: new Date(t.created_at).toISOString(),
|
|
39350
|
+
until: new Date(t.fire_at).toISOString()
|
|
39351
|
+
});
|
|
39352
|
+
}
|
|
39353
|
+
for (const w of ctx.storage.listPrWatches(input.slug, { armedOnly: true })) {
|
|
39354
|
+
activity.push({
|
|
39355
|
+
kind: "pr",
|
|
39356
|
+
id: `${w.owner}/${w.repo}#${w.number}`,
|
|
39357
|
+
label: `${w.owner}/${w.repo}#${w.number}`,
|
|
39358
|
+
since: new Date(w.created_at).toISOString()
|
|
39359
|
+
});
|
|
39360
|
+
}
|
|
39361
|
+
return { activity };
|
|
39362
|
+
}
|
|
39363
|
+
}),
|
|
39364
|
+
// THE WATCHER REGISTRY IS GONE (2026-08-14). `mcp__frizz__watch` and its four procedures are
|
|
39365
|
+
// deleted rather than shimmed: a wait is now a `watch:` line in the worker's own ```awaiting fence,
|
|
39366
|
+
// which is BOTH the park and the wake (scheduler `watchVerdict`), so there is nothing to register and
|
|
39367
|
+
// nothing an alias could usefully do. A session dispatched before the change still holds the old MCP
|
|
39368
|
+
// binary and will get a 404 from it — which is the honest answer, and the tool it should not be
|
|
39369
|
+
// calling is the only casualty.
|
|
39370
|
+
// ---- REGISTERED PR WATCHERS (add / drop / list) ---------------------------------------------
|
|
39371
|
+
// The worker's own PR watchers, from `mcp__frizz__watch_pr`. Same caller and therefore the same rules
|
|
39372
|
+
// as the timers above: slug-only (the MCP server outlives the session ids underneath it), and no
|
|
39373
|
+
// thread parameter a model could aim elsewhere.
|
|
39374
|
+
addOwnPrWatch: mutation({
|
|
39375
|
+
input: AddOwnPrWatchInput,
|
|
39376
|
+
output: AddOwnPrWatchResult,
|
|
38121
39377
|
handler: async ({ input }) => {
|
|
38122
39378
|
const row = ctx.storage.getSession(input.slug);
|
|
38123
39379
|
if (!row) throw new Error(`thread ${input.slug} is not registered`);
|
|
38124
39380
|
if (row.state === "archived" || row.archived === 1) {
|
|
38125
39381
|
throw new Error("Reopen this thread before registering a watcher on it");
|
|
38126
39382
|
}
|
|
38127
|
-
const
|
|
38128
|
-
|
|
38129
|
-
|
|
38130
|
-
|
|
38131
|
-
|
|
39383
|
+
const ref = parsePrRef(input.target);
|
|
39384
|
+
if (!ref) {
|
|
39385
|
+
throw new Error(`\`${input.target}\` is not a pull request I can watch \u2014 give owner/repo#123 or a PR URL`);
|
|
39386
|
+
}
|
|
39387
|
+
const armed = ctx.storage.listPrWatches(input.slug, { armedOnly: true });
|
|
39388
|
+
const existing = armed.find((w) => w.owner === ref.owner && w.repo === ref.repo && w.number === ref.number);
|
|
39389
|
+
const target = `${ref.owner}/${ref.repo}#${ref.number}`;
|
|
39390
|
+
if (existing) return { id: existing.id, target, alreadyArmed: true, watches: armedPrWatchViews(input.slug) };
|
|
39391
|
+
if (armed.length >= PR_WATCH_MAX_ARMED) {
|
|
39392
|
+
throw new Error(`this thread already watches ${armed.length} pull requests (the limit is ${PR_WATCH_MAX_ARMED}) \u2014 drop one first`);
|
|
39393
|
+
}
|
|
39394
|
+
const forMs = input.for === void 0 ? PR_WATCH_DEFAULT_FOR_MS : parseAwaitingDuration(input.for);
|
|
39395
|
+
if (forMs === null) {
|
|
39396
|
+
throw new Error(`\`for: ${input.for}\` is not a duration \u2014 give one like \`30m\`, \`2h\` or \`3d\` (max 24h)`);
|
|
38132
39397
|
}
|
|
38133
|
-
const
|
|
38134
|
-
|
|
39398
|
+
const now = Date.now();
|
|
39399
|
+
const id = `prw_${randomUUID16().replace(/-/g, "").slice(0, 12)}`;
|
|
39400
|
+
ctx.storage.armPrWatch({ id, slug: input.slug, owner: ref.owner, repo: ref.repo, number: ref.number, createdAtMs: now, expiresAtMs: now + forMs });
|
|
38135
39401
|
ctx.board.refresh();
|
|
38136
|
-
return { id, alreadyArmed: false, watches:
|
|
39402
|
+
return { id, target, alreadyArmed: false, watches: armedPrWatchViews(input.slug) };
|
|
38137
39403
|
}
|
|
38138
39404
|
}),
|
|
38139
|
-
|
|
38140
|
-
input:
|
|
38141
|
-
output:
|
|
39405
|
+
dropOwnPrWatch: mutation({
|
|
39406
|
+
input: DropOwnPrWatchInput,
|
|
39407
|
+
output: DropOwnPrWatchResult,
|
|
38142
39408
|
handler: async ({ input }) => {
|
|
38143
|
-
const dropped = ctx.storage.
|
|
39409
|
+
const dropped = ctx.storage.dropPrWatch(input.slug, input.id, Date.now());
|
|
38144
39410
|
if (dropped) ctx.board.refresh();
|
|
38145
|
-
return { dropped, watches:
|
|
39411
|
+
return { dropped, watches: armedPrWatchViews(input.slug) };
|
|
38146
39412
|
}
|
|
38147
39413
|
}),
|
|
38148
|
-
|
|
38149
|
-
|
|
38150
|
-
|
|
38151
|
-
|
|
38152
|
-
input: PromoteOwnThreadWatchInput,
|
|
38153
|
-
output: PromoteOwnThreadWatchResult,
|
|
38154
|
-
handler: async ({ input }) => {
|
|
38155
|
-
const promoted = ctx.storage.promoteThreadWatch(input.slug, input.id);
|
|
38156
|
-
if (promoted) ctx.board.refresh();
|
|
38157
|
-
return { promoted, watches: armedWatchViews(input.slug) };
|
|
38158
|
-
}
|
|
38159
|
-
}),
|
|
38160
|
-
listOwnThreadWatches: mutation({
|
|
38161
|
-
input: ListOwnThreadWatchesInput,
|
|
38162
|
-
output: OwnThreadWatchesResult,
|
|
38163
|
-
handler: async ({ input }) => ({ watches: armedWatchViews(input.slug) })
|
|
39414
|
+
listOwnPrWatches: mutation({
|
|
39415
|
+
input: ListOwnPrWatchesInput,
|
|
39416
|
+
output: OwnPrWatchesResult,
|
|
39417
|
+
handler: async ({ input }) => ({ watches: armedPrWatchViews(input.slug) })
|
|
38164
39418
|
}),
|
|
38165
39419
|
// The SUPERSEDED worker procedures, aliased onto the row above — see SetOwnThreadStopHookInput for
|
|
38166
39420
|
// why they cannot simply be deleted. A worker's MCP server outlives every server restart, so these
|
|
@@ -38238,10 +39492,7 @@ function createRouter(ctx) {
|
|
|
38238
39492
|
if (!fenceAt || !Number.isFinite(Date.parse(fenceAt)) || fenceAt !== input.fenceAt) {
|
|
38239
39493
|
throw new Error("This awaiting proposal changed before it could be confirmed");
|
|
38240
39494
|
}
|
|
38241
|
-
const snoozedUntil =
|
|
38242
|
-
if (hint.kind === "timer" && !snoozedUntil) {
|
|
38243
|
-
throw new Error("This awaiting proposal changed before it could be confirmed");
|
|
38244
|
-
}
|
|
39495
|
+
const snoozedUntil = null;
|
|
38245
39496
|
if (snoozedUntil && Date.parse(snoozedUntil) <= Date.now()) {
|
|
38246
39497
|
throw new Error("This scheduled time has already passed");
|
|
38247
39498
|
}
|
|
@@ -38738,12 +39989,12 @@ function createRouter(ctx) {
|
|
|
38738
39989
|
output: DispatchPreferences,
|
|
38739
39990
|
handler: async ({ input }) => setDispatchPreference(ctx.storage, ctx.getSettings(), input, readCodexModels())
|
|
38740
39991
|
}),
|
|
38741
|
-
// The shipped GitHub batch-dispatch prompt
|
|
38742
|
-
// The Settings UI reads
|
|
38743
|
-
//
|
|
39992
|
+
// The shipped GitHub batch-dispatch prompt template (single source of truth: server/github.ts).
|
|
39993
|
+
// The Settings UI reads it to prefill the editor and to power "reset to default"; an empty/unset
|
|
39994
|
+
// githubPrompt setting means the server uses exactly this. One template serves issues AND PRs.
|
|
38744
39995
|
githubPromptDefaults: query({
|
|
38745
|
-
output: external_exports.object({
|
|
38746
|
-
handler: async () => ({
|
|
39996
|
+
output: external_exports.object({ prompt: external_exports.string() }),
|
|
39997
|
+
handler: async () => ({ prompt: DEFAULT_GITHUB_PROMPT })
|
|
38747
39998
|
}),
|
|
38748
39999
|
// ---- GitHub-first batch dispatch ----
|
|
38749
40000
|
// gh availability: installed (cache-warmed resolveInstalled) + inRepo/nameWithOwner (cache-warmed
|
|
@@ -38775,6 +40026,18 @@ function createRouter(ctx) {
|
|
|
38775
40026
|
return await listItems(repo, input.kind, input.sort, input.page, input.perPage);
|
|
38776
40027
|
}
|
|
38777
40028
|
}),
|
|
40029
|
+
// Hovercard data for every GitHub reference the client autolinked into the prose on screen —
|
|
40030
|
+
// ONE request for a whole page of refs, answered from a process-lifetime cache (see
|
|
40031
|
+
// github-hovercard.ts). The client asks as the prose renders, so the hover itself never waits on
|
|
40032
|
+
// the network; `refresh` is its revalidation of the handful it is actually pointing at.
|
|
40033
|
+
//
|
|
40034
|
+
// Never throws: a missing gh, an unauthenticated one, a rate limit and a network stall all come
|
|
40035
|
+
// back as `error` with whatever cards the cache already holds, and the anchor stays a plain link.
|
|
40036
|
+
githubRefPreview: query({
|
|
40037
|
+
input: GithubRefPreviewInput,
|
|
40038
|
+
output: GithubRefPreviewResult,
|
|
40039
|
+
handler: async ({ input }) => await githubHovercards.preview(input.refs, { refresh: input.refresh })
|
|
40040
|
+
}),
|
|
38778
40041
|
// Spin up one frizz thread per checked item: hydrate each fresh from gh, template a server-side
|
|
38779
40042
|
// prompt (single source of truth, unit-tested), then REUSE ctx.dispatcher.dispatch (no new spawn
|
|
38780
40043
|
// logic). SEQUENTIAL — a burst of 20 concurrent tmux spawns would hammer the box (risk 5). A
|
|
@@ -38786,14 +40049,13 @@ function createRouter(ctx) {
|
|
|
38786
40049
|
validateGithubDispatchProfile(input);
|
|
38787
40050
|
const repo = await resolveRepo();
|
|
38788
40051
|
if (!repo) throw new Error("not a GitHub repo");
|
|
38789
|
-
const
|
|
40052
|
+
const template = effectiveTemplate(ctx.getSettings().githubPrompt);
|
|
38790
40053
|
const dispatched = [];
|
|
38791
40054
|
const failed = [];
|
|
38792
40055
|
for (const it of input.items) {
|
|
38793
40056
|
try {
|
|
38794
40057
|
const title = `${it.kind === "issue" ? "Investigate" : "Review"} ${repo}#${it.number}`;
|
|
38795
40058
|
const slug = resolveSlug(frizzDir, slugify(title), (s) => ctx.storage.getSession(s) !== void 0);
|
|
38796
|
-
const template = effectiveTemplate(it.kind, it.kind === "issue" ? settings.githubIssuePrompt : settings.githubPrPrompt);
|
|
38797
40059
|
const hydrated = it.kind === "issue" ? await hydrateIssue(repo, it.number) : await hydratePr(repo, it.number);
|
|
38798
40060
|
const prompt = renderGithubPrompt(template, repo, hydrated, slug, it.kind);
|
|
38799
40061
|
const request = githubDispatcherRequest(input, { prompt, title, slug });
|
|
@@ -38808,7 +40070,7 @@ function createRouter(ctx) {
|
|
|
38808
40070
|
})
|
|
38809
40071
|
};
|
|
38810
40072
|
}
|
|
38811
|
-
var SlugInput, cachedLivenessTerminator, HOLD_OPS_MAX, HOLD_LABEL_MAX;
|
|
40073
|
+
var SlugInput, cachedLivenessTerminator, HOLD_OPS_MAX, HOLD_LABEL_MAX, githubHovercards;
|
|
38812
40074
|
var init_router5 = __esm({
|
|
38813
40075
|
"packages/server/src/router.ts"() {
|
|
38814
40076
|
"use strict";
|
|
@@ -38826,6 +40088,7 @@ var init_router5 = __esm({
|
|
|
38826
40088
|
init_local_file();
|
|
38827
40089
|
init_project();
|
|
38828
40090
|
init_github();
|
|
40091
|
+
init_github_hovercard();
|
|
38829
40092
|
init_dispatch();
|
|
38830
40093
|
init_codex_models();
|
|
38831
40094
|
init_codex();
|
|
@@ -38860,6 +40123,7 @@ var init_router5 = __esm({
|
|
|
38860
40123
|
};
|
|
38861
40124
|
HOLD_OPS_MAX = 8;
|
|
38862
40125
|
HOLD_LABEL_MAX = 100;
|
|
40126
|
+
githubHovercards = createGithubHovercardService();
|
|
38863
40127
|
}
|
|
38864
40128
|
});
|
|
38865
40129
|
|
|
@@ -38999,6 +40263,52 @@ var init_local_origin = __esm({
|
|
|
38999
40263
|
}
|
|
39000
40264
|
});
|
|
39001
40265
|
|
|
40266
|
+
// packages/server/src/compression.ts
|
|
40267
|
+
import { brotliCompressSync, gzipSync, constants as constants3 } from "node:zlib";
|
|
40268
|
+
function negotiateEncoding(acceptEncoding) {
|
|
40269
|
+
if (!acceptEncoding) return null;
|
|
40270
|
+
const offers = /* @__PURE__ */ new Map();
|
|
40271
|
+
for (const part of acceptEncoding.split(",")) {
|
|
40272
|
+
const [name, ...params] = part.trim().split(";");
|
|
40273
|
+
if (!name) continue;
|
|
40274
|
+
const q = params.map((p) => /^\s*q=([0-9.]+)\s*$/i.exec(p)).find(Boolean);
|
|
40275
|
+
offers.set(name.trim().toLowerCase(), q ? Number(q[1]) : 1);
|
|
40276
|
+
}
|
|
40277
|
+
const wildcard = offers.get("*");
|
|
40278
|
+
const accepted = (name) => {
|
|
40279
|
+
const explicit = offers.get(name);
|
|
40280
|
+
if (explicit !== void 0) return explicit > 0;
|
|
40281
|
+
return wildcard !== void 0 && wildcard > 0;
|
|
40282
|
+
};
|
|
40283
|
+
if (accepted("br")) return "br";
|
|
40284
|
+
if (accepted("gzip")) return "gzip";
|
|
40285
|
+
return null;
|
|
40286
|
+
}
|
|
40287
|
+
function compress(body, encoding) {
|
|
40288
|
+
const out = encoding === "br" ? brotliCompressSync(body, {
|
|
40289
|
+
params: {
|
|
40290
|
+
[constants3.BROTLI_PARAM_QUALITY]: 4,
|
|
40291
|
+
// Telling brotli the input size lets it size its window instead of guessing.
|
|
40292
|
+
[constants3.BROTLI_PARAM_SIZE_HINT]: body.byteLength
|
|
40293
|
+
}
|
|
40294
|
+
}) : gzipSync(body, { level: 6 });
|
|
40295
|
+
return Uint8Array.from(out);
|
|
40296
|
+
}
|
|
40297
|
+
function shouldCompress(headers, byteLength2) {
|
|
40298
|
+
if (byteLength2 < MIN_COMPRESS_BYTES) return false;
|
|
40299
|
+
if (headers.has("content-encoding")) return false;
|
|
40300
|
+
const type = headers.get("content-type") ?? "";
|
|
40301
|
+
if (type.includes("text/event-stream")) return false;
|
|
40302
|
+
return /json|text\/|javascript|xml|svg/i.test(type);
|
|
40303
|
+
}
|
|
40304
|
+
var MIN_COMPRESS_BYTES;
|
|
40305
|
+
var init_compression = __esm({
|
|
40306
|
+
"packages/server/src/compression.ts"() {
|
|
40307
|
+
"use strict";
|
|
40308
|
+
MIN_COMPRESS_BYTES = 1024;
|
|
40309
|
+
}
|
|
40310
|
+
});
|
|
40311
|
+
|
|
39002
40312
|
// packages/server/src/local-image.ts
|
|
39003
40313
|
import { readFileSync as readFileSync22, realpathSync as realpathSync9, statSync as statSync18 } from "node:fs";
|
|
39004
40314
|
import { extname as extname2, isAbsolute as isAbsolute4 } from "node:path";
|
|
@@ -39035,7 +40345,7 @@ var init_local_image = __esm({
|
|
|
39035
40345
|
|
|
39036
40346
|
// packages/server/src/project-icon.ts
|
|
39037
40347
|
import { readFileSync as readFileSync23, readdirSync as readdirSync11, realpathSync as realpathSync10, statSync as statSync19 } from "node:fs";
|
|
39038
|
-
import { homedir as
|
|
40348
|
+
import { homedir as homedir17 } from "node:os";
|
|
39039
40349
|
import { basename as basename11, dirname as dirname15, extname as extname3, join as join33, relative, sep as sep2 } from "node:path";
|
|
39040
40350
|
function normalizeStem(stem) {
|
|
39041
40351
|
const sized = stem.toLowerCase().replace(/[@-]\d+(\.\d+)?x(\d+)?$/u, "").replace(/[-_]\d{2,4}$/u, "");
|
|
@@ -39174,7 +40484,7 @@ function iconMediaType(path) {
|
|
|
39174
40484
|
}
|
|
39175
40485
|
function resolveProjectIconResponse(id, options = {}) {
|
|
39176
40486
|
if (!id) return { status: 400 };
|
|
39177
|
-
const home = options.home ??
|
|
40487
|
+
const home = options.home ?? homedir17();
|
|
39178
40488
|
const entry = readRegistry(home).projects.find((project) => project.id === id);
|
|
39179
40489
|
if (!entry) return { status: 404 };
|
|
39180
40490
|
const path = resolveProjectIcon(id, (root) => detectProjectIcon(root)?.path, { home });
|
|
@@ -39502,6 +40812,21 @@ function createApp(ctx, options = {}) {
|
|
|
39502
40812
|
c.header("x-frizz-boot", ctx.bootId);
|
|
39503
40813
|
await next();
|
|
39504
40814
|
});
|
|
40815
|
+
app.use(`${frizzRoute("/rpc")}/*`, async (c, next) => {
|
|
40816
|
+
await next();
|
|
40817
|
+
const encoding = negotiateEncoding(c.req.header("accept-encoding"));
|
|
40818
|
+
if (!encoding || !c.res.body) return;
|
|
40819
|
+
const body = new Uint8Array(await c.res.arrayBuffer());
|
|
40820
|
+
if (!shouldCompress(c.res.headers, body.byteLength)) {
|
|
40821
|
+
c.res = new Response(body, { status: c.res.status, headers: c.res.headers });
|
|
40822
|
+
return;
|
|
40823
|
+
}
|
|
40824
|
+
const headers = new Headers(c.res.headers);
|
|
40825
|
+
headers.set("content-encoding", encoding);
|
|
40826
|
+
headers.set("vary", "Accept-Encoding");
|
|
40827
|
+
headers.delete("content-length");
|
|
40828
|
+
c.res = new Response(compress(body, encoding), { status: c.res.status, headers });
|
|
40829
|
+
});
|
|
39505
40830
|
app.get(frizzRoute("/health"), (c) => c.json({
|
|
39506
40831
|
ok: true,
|
|
39507
40832
|
projectId: ctx.project.id,
|
|
@@ -39608,6 +40933,7 @@ var init_app = __esm({
|
|
|
39608
40933
|
init_src();
|
|
39609
40934
|
init_router5();
|
|
39610
40935
|
init_local_origin();
|
|
40936
|
+
init_compression();
|
|
39611
40937
|
init_local_image();
|
|
39612
40938
|
init_project_icon();
|
|
39613
40939
|
init_local_visualization();
|
|
@@ -40071,6 +41397,11 @@ function createAppSocketServer(deps) {
|
|
|
40071
41397
|
const bufferedAmount = deps.bufferedAmount ?? ((ws) => ws.bufferedAmount);
|
|
40072
41398
|
const now = deps.now ?? Date.now;
|
|
40073
41399
|
const serializeMessage = deps.serializeMessage ?? JSON.stringify;
|
|
41400
|
+
const scheduleSubscriptionFlush = deps.scheduleSubscriptionFlush ?? ((flush) => {
|
|
41401
|
+
const immediate = setImmediate(flush);
|
|
41402
|
+
immediate.unref?.();
|
|
41403
|
+
return () => clearImmediate(immediate);
|
|
41404
|
+
});
|
|
40074
41405
|
const terminationTimers = /* @__PURE__ */ new WeakMap();
|
|
40075
41406
|
const keyframes = /* @__PURE__ */ new Set();
|
|
40076
41407
|
let closing = false;
|
|
@@ -40307,7 +41638,33 @@ function createAppSocketServer(deps) {
|
|
|
40307
41638
|
scheduleTranscriptFlush(BACKPRESSURE_RETRY_MS);
|
|
40308
41639
|
}
|
|
40309
41640
|
const pendingSubscriptionPushes = /* @__PURE__ */ new Map();
|
|
40310
|
-
let
|
|
41641
|
+
let subscriptionFlushArmed = false;
|
|
41642
|
+
let cancelSubscriptionFlush = null;
|
|
41643
|
+
function flushSubscriptionPushes() {
|
|
41644
|
+
subscriptionFlushArmed = false;
|
|
41645
|
+
cancelSubscriptionFlush = null;
|
|
41646
|
+
if (closing) {
|
|
41647
|
+
pendingSubscriptionPushes.clear();
|
|
41648
|
+
return;
|
|
41649
|
+
}
|
|
41650
|
+
const batch = [...pendingSubscriptionPushes];
|
|
41651
|
+
pendingSubscriptionPushes.clear();
|
|
41652
|
+
const batchResults = /* @__PURE__ */ new Map();
|
|
41653
|
+
for (const [candidate, candidateSlugs] of batch) {
|
|
41654
|
+
if (candidate.readyState !== candidate.OPEN) continue;
|
|
41655
|
+
for (const candidateSlug of candidateSlugs) {
|
|
41656
|
+
if (!registry.isSubscribed(candidate, candidateSlug)) continue;
|
|
41657
|
+
let result = batchResults.get(candidateSlug);
|
|
41658
|
+
if (!result || result.kind === "limited" && result.scope === "origin") {
|
|
41659
|
+
result = snapshotFor(candidateSlug, socketOrigins.get(candidate));
|
|
41660
|
+
if (!(result.kind === "limited" && result.scope === "origin")) {
|
|
41661
|
+
batchResults.set(candidateSlug, result);
|
|
41662
|
+
}
|
|
41663
|
+
}
|
|
41664
|
+
pushSnapshotResult(candidate, candidateSlug, result);
|
|
41665
|
+
}
|
|
41666
|
+
}
|
|
41667
|
+
}
|
|
40311
41668
|
function enqueueSubscriptionPush(ws, slug) {
|
|
40312
41669
|
deleteTranscriptSnapshot(slug);
|
|
40313
41670
|
let slugs = pendingSubscriptionPushes.get(ws);
|
|
@@ -40316,32 +41673,10 @@ function createAppSocketServer(deps) {
|
|
|
40316
41673
|
pendingSubscriptionPushes.set(ws, slugs);
|
|
40317
41674
|
}
|
|
40318
41675
|
slugs.add(slug);
|
|
40319
|
-
if (
|
|
40320
|
-
|
|
40321
|
-
|
|
40322
|
-
|
|
40323
|
-
pendingSubscriptionPushes.clear();
|
|
40324
|
-
return;
|
|
40325
|
-
}
|
|
40326
|
-
const batch = [...pendingSubscriptionPushes];
|
|
40327
|
-
pendingSubscriptionPushes.clear();
|
|
40328
|
-
const batchResults = /* @__PURE__ */ new Map();
|
|
40329
|
-
for (const [candidate, candidateSlugs] of batch) {
|
|
40330
|
-
if (candidate.readyState !== candidate.OPEN) continue;
|
|
40331
|
-
for (const candidateSlug of candidateSlugs) {
|
|
40332
|
-
if (!registry.isSubscribed(candidate, candidateSlug)) continue;
|
|
40333
|
-
let result = batchResults.get(candidateSlug);
|
|
40334
|
-
if (!result || result.kind === "limited" && result.scope === "origin") {
|
|
40335
|
-
result = snapshotFor(candidateSlug, socketOrigins.get(candidate));
|
|
40336
|
-
if (!(result.kind === "limited" && result.scope === "origin")) {
|
|
40337
|
-
batchResults.set(candidateSlug, result);
|
|
40338
|
-
}
|
|
40339
|
-
}
|
|
40340
|
-
pushSnapshotResult(candidate, candidateSlug, result);
|
|
40341
|
-
}
|
|
40342
|
-
}
|
|
40343
|
-
});
|
|
40344
|
-
subscriptionFlushImmediate.unref?.();
|
|
41676
|
+
if (subscriptionFlushArmed) return;
|
|
41677
|
+
subscriptionFlushArmed = true;
|
|
41678
|
+
const cancel = scheduleSubscriptionFlush(flushSubscriptionPushes);
|
|
41679
|
+
if (subscriptionFlushArmed) cancelSubscriptionFlush = cancel ?? null;
|
|
40345
41680
|
}
|
|
40346
41681
|
function cancelPendingSubscriptionPush(ws, slug) {
|
|
40347
41682
|
if (slug === void 0) {
|
|
@@ -40643,8 +41978,12 @@ function createAppSocketServer(deps) {
|
|
|
40643
41978
|
offTranscript();
|
|
40644
41979
|
} catch {
|
|
40645
41980
|
}
|
|
40646
|
-
|
|
40647
|
-
|
|
41981
|
+
try {
|
|
41982
|
+
cancelSubscriptionFlush?.();
|
|
41983
|
+
} catch {
|
|
41984
|
+
}
|
|
41985
|
+
cancelSubscriptionFlush = null;
|
|
41986
|
+
subscriptionFlushArmed = false;
|
|
40648
41987
|
pendingSubscriptionPushes.clear();
|
|
40649
41988
|
if (transcriptRefreshTimer) clearTimeout(transcriptRefreshTimer);
|
|
40650
41989
|
transcriptRefreshTimer = null;
|