frizz 0.3.0 → 0.4.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 +9 -4
- package/dist/claude-agent-broker.js +9 -4
- package/dist/dev-child.js +1783 -527
- package/dist/frizz.js +709 -233
- package/package.json +5 -3
- package/runtime/cc-worker/DECISIONS.md +17 -0
- package/runtime/cc-worker/bin/frizz-mcp.mjs +151 -174
- package/runtime/cc-worker/hooks/session-seed.mjs +1 -1
- package/web-dist/assets/{TerminalPane-CTdetDJJ.js → TerminalPane-sd3XXjx5.js} +1 -1
- package/web-dist/assets/{abnfDiagram-VRR7QNED-BDKrjCMs.js → abnfDiagram-VRR7QNED-Bry_FIqg.js} +1 -1
- package/web-dist/assets/architecture-TIHT7OUA-CoMJQ5Ya.js +1 -0
- package/web-dist/assets/{architectureDiagram-ZJ3FMSHR-BJebpUUM.js → architectureDiagram-ZJ3FMSHR-Baok4OeI.js} +1 -1
- package/web-dist/assets/{blockDiagram-677ZJIJ3-BNbuk25k.js → blockDiagram-677ZJIJ3-BgHIO9lI.js} +1 -1
- package/web-dist/assets/{c4Diagram-LMCZKHZV-lCfyotdU.js → c4Diagram-LMCZKHZV-rfj9Z17Z.js} +1 -1
- package/web-dist/assets/channel-D6uS1y7I.js +1 -0
- package/web-dist/assets/{chunk-32BRIVSS-DP72SEkr.js → chunk-32BRIVSS-BwaHfK1Y.js} +1 -1
- package/web-dist/assets/{chunk-52WLFC77-DxO3gN-p.js → chunk-52WLFC77-BmdQDZOA.js} +1 -1
- package/web-dist/assets/{chunk-C7G6YPKG-BXFx9Vlr.js → chunk-C7G6YPKG-ByQFjoZe.js} +1 -1
- package/web-dist/assets/{chunk-EX3LRPZG-CR3sHpPf.js → chunk-EX3LRPZG-DQFudBKr.js} +1 -1
- package/web-dist/assets/{chunk-FWX5IMBZ-CJ8L__oG.js → chunk-FWX5IMBZ-OW9qEPiq.js} +2 -2
- package/web-dist/assets/{chunk-HOUHSVGY-BCl3JSWr.js → chunk-HOUHSVGY-BdKLF8nO.js} +1 -1
- package/web-dist/assets/{chunk-ICXQ74PX-CZxFMci1.js → chunk-ICXQ74PX-C1roqKR1.js} +1 -1
- package/web-dist/assets/{chunk-MOJQB5TN-Ck3_47dB.js → chunk-MOJQB5TN-Cx0u1ZDK.js} +1 -1
- package/web-dist/assets/{chunk-OGEWGWER-CVXjES-F.js → chunk-OGEWGWER-rxv9xtNR.js} +1 -1
- package/web-dist/assets/{chunk-PUDLZKDR-JjrVgaR7.js → chunk-PUDLZKDR-BXWPPPPi.js} +1 -1
- package/web-dist/assets/{chunk-Q4XR5HBZ-C5YMRAQ0.js → chunk-Q4XR5HBZ-CpqbD101.js} +1 -1
- package/web-dist/assets/{chunk-V7JOEXUC-DK0mfUHU.js → chunk-V7JOEXUC-CP8_3qVt.js} +1 -1
- package/web-dist/assets/{chunk-VAUOI2AC-BKBntpid.js → chunk-VAUOI2AC-Dl7xahpk.js} +1 -1
- package/web-dist/assets/{chunk-VR4S4FIN-zgE1dG6U.js → chunk-VR4S4FIN-BKtMX64l.js} +1 -1
- package/web-dist/assets/{chunk-WYO6CB5R-CZhh1IBq.js → chunk-WYO6CB5R-DmLOdJcu.js} +1 -1
- package/web-dist/assets/{chunk-ZGVPDNZ5-CrNjIEem.js → chunk-ZGVPDNZ5-5wN7eXvS.js} +1 -1
- package/web-dist/assets/classDiagram-OUVF2IWQ-BW5YiBf4.js +1 -0
- package/web-dist/assets/classDiagram-v2-EOCWNBFH-BW5YiBf4.js +1 -0
- package/web-dist/assets/{cynefin-VYW2F7L2-Ca_BPfTG.js → cynefin-VYW2F7L2-DUYahBXb.js} +1 -1
- package/web-dist/assets/{cynefinDiagram-TSTJHNR4-CsINQf6g.js → cynefinDiagram-TSTJHNR4-Rm0i30nE.js} +1 -1
- package/web-dist/assets/{dagre-VKFMJZFB-CE0DYMFy.js → dagre-VKFMJZFB-3uhsB-W3.js} +1 -1
- package/web-dist/assets/{diagram-FQU43EPY-BqTduEYE.js → diagram-FQU43EPY-BUvflNRQ.js} +1 -1
- package/web-dist/assets/{diagram-G47NLZAW-Cho718s6.js → diagram-G47NLZAW-DUluIZel.js} +1 -1
- package/web-dist/assets/{diagram-NH7WQ7WH-D0Z8t9UC.js → diagram-NH7WQ7WH-BBrwsbed.js} +1 -1
- package/web-dist/assets/{diagram-OA4YK3LP-CBacyeDx.js → diagram-OA4YK3LP-DWnTQBw4.js} +1 -1
- package/web-dist/assets/{diagram-WEI45ONY-CozpXa4j.js → diagram-WEI45ONY-BjEavoC0.js} +1 -1
- package/web-dist/assets/{ebnfDiagram-CCIWWBDH-ba4NbQQo.js → ebnfDiagram-CCIWWBDH-CW4xZAZH.js} +1 -1
- package/web-dist/assets/{erDiagram-Q63AITRT-B3BciOYa.js → erDiagram-Q63AITRT-BemQ1R4B.js} +1 -1
- package/web-dist/assets/eventmodeling-45OFAUF4-BF5KB7ja.js +1 -0
- package/web-dist/assets/flowDiagram-23GEKE2U-BYo4Eytx.js +1 -0
- package/web-dist/assets/{ganttDiagram-NO4QXBWP-lPmrRit4.js → ganttDiagram-NO4QXBWP-lDdG5I_z.js} +1 -1
- package/web-dist/assets/{gitGraph-TEB2WS4Q-BjyclOq0.js → gitGraph-TEB2WS4Q-Bw87a_Se.js} +1 -1
- package/web-dist/assets/{gitGraphDiagram-IHSO6WYX-pzeb4Yrw.js → gitGraphDiagram-IHSO6WYX-B762r0yi.js} +1 -1
- package/web-dist/assets/index-B3QArXu2.css +1 -0
- package/web-dist/assets/index-CAapfhMv.js +365 -0
- package/web-dist/assets/{info-DKCQHKI2-Bwycegvf.js → info-DKCQHKI2-DGOv6Gx0.js} +1 -1
- package/web-dist/assets/{infoDiagram-FWYZ7A6U-BCvRGj_5.js → infoDiagram-FWYZ7A6U-DL92kAzf.js} +1 -1
- package/web-dist/assets/{ishikawaDiagram-FXEZZL3T-Ofw1RMj3.js → ishikawaDiagram-FXEZZL3T-CFYd4OtU.js} +1 -1
- package/web-dist/assets/{journeyDiagram-5HDEW3XC-C5ROwFio.js → journeyDiagram-5HDEW3XC-QHuYL5zq.js} +1 -1
- package/web-dist/assets/{kanban-definition-HUTT4EX6-YLPLkpeT.js → kanban-definition-HUTT4EX6-DwmMluQ8.js} +1 -1
- package/web-dist/assets/{line-KtkNqRgI.js → line-Je_PFFHm.js} +1 -1
- package/web-dist/assets/{mermaid-parser.core-D_FfqBe7.js → mermaid-parser.core-BeiGWsn3.js} +3 -3
- package/web-dist/assets/{mermaid.core-Ffv8anVf.js → mermaid.core-BP8JrW48.js} +3 -3
- package/web-dist/assets/{mindmap-definition-LN4V7U3C-B2jj4vfm.js → mindmap-definition-LN4V7U3C-BsmjCIrM.js} +1 -1
- package/web-dist/assets/{packet-7NZHBO7P-BjmWHwra.js → packet-7NZHBO7P-D6ekYYGJ.js} +1 -1
- package/web-dist/assets/{pegDiagram-2B236MQR-Dq3iJDyq.js → pegDiagram-2B236MQR-CQc2Kt0u.js} +1 -1
- package/web-dist/assets/{pie-RZYD4A2V-jBbH1lv9.js → pie-RZYD4A2V-C_45fO8J.js} +1 -1
- package/web-dist/assets/{pieDiagram-ENE6RG2P-C3ETW9lq.js → pieDiagram-ENE6RG2P-Dg5z6iWt.js} +1 -1
- package/web-dist/assets/{quadrantDiagram-ABIIQ3AL-DItSmme7.js → quadrantDiagram-ABIIQ3AL-c7gZxuCX.js} +1 -1
- package/web-dist/assets/{radar-I7S5WNFK-6ey6crgP.js → radar-I7S5WNFK-D80XNJQq.js} +1 -1
- package/web-dist/assets/{railroad-3IZDKUUU-Cii-Mn0E.js → railroad-3IZDKUUU-Brz7eZQ0.js} +1 -1
- package/web-dist/assets/railroad-abnf-AHOZXSZD-D44pcGhJ.js +1 -0
- package/web-dist/assets/railroad-ebnf-EBAXGLYW-CiWVH5SP.js +1 -0
- package/web-dist/assets/railroad-peg-LSFZ7HO6-Cy1B6-pN.js +1 -0
- package/web-dist/assets/{railroadDiagram-RFXS5EU6-M363ils_.js → railroadDiagram-RFXS5EU6-Db9K7bty.js} +1 -1
- package/web-dist/assets/{requirementDiagram-TGXJPOKE-CTs2_V6T.js → requirementDiagram-TGXJPOKE-m1ipMrDe.js} +1 -1
- package/web-dist/assets/{sankeyDiagram-HTMAVEWB-QTLLcDPD.js → sankeyDiagram-HTMAVEWB-4Zz9ry0k.js} +1 -1
- package/web-dist/assets/{sequenceDiagram-DBY2YBRQ-Bxw9Tr6e.js → sequenceDiagram-DBY2YBRQ-CZeGHw4X.js} +1 -1
- package/web-dist/assets/{stateDiagram-2N3HPSRC-BecB6roG.js → stateDiagram-2N3HPSRC-rLUVs1Pg.js} +1 -1
- package/web-dist/assets/stateDiagram-v2-6OUMAXLB-w7QDFEvx.js +1 -0
- package/web-dist/assets/{swimlanes-5IMT3BWC-COiYgS0w.js → swimlanes-5IMT3BWC-BAeBdscz.js} +1 -1
- package/web-dist/assets/swimlanesDiagram-G3AALYLV-UhdgWpNZ.js +8 -0
- package/web-dist/assets/{timeline-definition-FHXFAJF6-Ci22coeH.js → timeline-definition-FHXFAJF6-DL0eWMF-.js} +1 -1
- package/web-dist/assets/{treeView-QDETBFTQ-4DW35czh.js → treeView-QDETBFTQ-gEtzLMA8.js} +1 -1
- package/web-dist/assets/{treemap-6X3UGDF4-Dq_-Z6-Y.js → treemap-6X3UGDF4-CCym1Gz6.js} +1 -1
- package/web-dist/assets/{vennDiagram-L72KCM5P-CHQSIEYq.js → vennDiagram-L72KCM5P-cF97-nb5.js} +1 -1
- package/web-dist/assets/{wardley-OPB4EBWU-Bwlh7HCY.js → wardley-OPB4EBWU-DQJHCGbM.js} +1 -1
- package/web-dist/assets/{wardleyDiagram-EHGQE667-DctjuPYn.js → wardleyDiagram-EHGQE667-0CnHzXLX.js} +1 -1
- package/web-dist/assets/{xychartDiagram-FW5EYKEG-ZoIGosv8.js → xychartDiagram-FW5EYKEG-9YDOp4m5.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, 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,162 @@ 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.",
|
|
6000
6068
|
"",
|
|
6001
|
-
"
|
|
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.",
|
|
6073
|
+
"",
|
|
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 gets its own card first via `mcp__frizz__spawn_thread`; none of them is a `done`. And what is not",
|
|
6117
|
+
'worth a card is not worth a SENTENCE: delete the dangling "one thing to carry forward", never park it',
|
|
6118
|
+
"in the handoff.",
|
|
6008
6119
|
"",
|
|
6009
6120
|
"**DO NOT REPEAT YOURSELF.** If the message you just wrote already stands on its own, reply with the",
|
|
6010
6121
|
"fence ALONE \u2014 the human reads both together, so restating it costs them the second read for nothing.",
|
|
6122
|
+
"The same holds inside one message: the card is the ledger of what shipped, the prose is only what a",
|
|
6123
|
+
"ledger cannot hold, and a sentence that reads the same in either belongs in exactly one of them.",
|
|
6011
6124
|
"",
|
|
6012
6125
|
"Only if it does NOT stand alone, fix that first, briefly. It has to be readable cold: the human has",
|
|
6013
6126
|
"seen nothing since their own last message \u2014 the Goal, this reminder, a watcher wake all came from",
|
|
6014
6127
|
"frizz \u2014 so anything you assumed they had followed, they have not."
|
|
6015
6128
|
].join("\n");
|
|
6016
6129
|
ThreadWatchKind = external_exports.enum(["shell", "github"]);
|
|
6017
|
-
|
|
6018
|
-
|
|
6130
|
+
GithubChecksState = external_exports.enum(["none", "running", "passing", "failing"]);
|
|
6131
|
+
GithubMergeState = external_exports.enum(["mergeable", "blocked", "conflicting", "unknown"]);
|
|
6132
|
+
GithubWatchStatus = external_exports.object({
|
|
6133
|
+
checks: GithubChecksState,
|
|
6134
|
+
/** The counts behind the verdict, so the card can say "3 running, 12 passed" the way GitHub does
|
|
6135
|
+
* rather than only "checks are running". */
|
|
6136
|
+
running: external_exports.number().int().nonnegative(),
|
|
6137
|
+
passed: external_exports.number().int().nonnegative(),
|
|
6138
|
+
failed: external_exports.number().int().nonnegative(),
|
|
6139
|
+
/** The failing job NAMES, capped — what a human actually needs to decide whether to look. */
|
|
6140
|
+
failing: external_exports.array(external_exports.string()).max(8).default([]),
|
|
6141
|
+
merge: GithubMergeState,
|
|
6142
|
+
/** OPEN | CLOSED | MERGED, lowercased. A merged or closed PR ends the wait outright. */
|
|
6143
|
+
state: external_exports.enum(["open", "closed", "merged"]),
|
|
6144
|
+
/** When frizz last heard from GitHub. A poll can fail or be rate-limited, and a stale reading stated
|
|
6145
|
+
* as current is worse than no reading. */
|
|
6146
|
+
polledAt: external_exports.string(),
|
|
6147
|
+
/** The head commit this verdict was reached on. A PR watcher reports a CI verdict when it is NEWS, and
|
|
6148
|
+
* "red again on a new commit" is news that the bare word `failing` cannot express. Optional because a
|
|
6149
|
+
* reading taken before 2026-08-17 carries none. */
|
|
6150
|
+
head: external_exports.string().optional(),
|
|
6151
|
+
/** A digest of WHICH jobs are failing right now. The head commit alone cannot express a re-run of the
|
|
6152
|
+
* same job, or a slower job going red after the first — both of which are a second failure the worker
|
|
6153
|
+
* must hear about. Empty when nothing is failing. */
|
|
6154
|
+
failureSig: external_exports.string().optional()
|
|
6155
|
+
}).strict();
|
|
6019
6156
|
ThreadWatchView = external_exports.object({
|
|
6020
6157
|
id: external_exports.string(),
|
|
6021
6158
|
kind: ThreadWatchKind,
|
|
6022
6159
|
target: external_exports.string(),
|
|
6023
6160
|
state: external_exports.enum(["armed", "fired", "dropped"]),
|
|
6024
|
-
createdAt: external_exports.string()
|
|
6161
|
+
createdAt: external_exports.string(),
|
|
6162
|
+
/** `github` rows only, and absent until the first successful poll. */
|
|
6163
|
+
github: GithubWatchStatus.optional()
|
|
6164
|
+
}).strict();
|
|
6165
|
+
PR_WATCH_DEFAULT_FOR_MS = 6 * 60 * 60 * 1e3;
|
|
6166
|
+
PR_WATCH_MAX_ARMED = 32;
|
|
6167
|
+
PrWatchView = external_exports.object({
|
|
6168
|
+
id: external_exports.string(),
|
|
6169
|
+
/** `owner/repo#N`, normalized — the same string the board's row and the status book are keyed by. */
|
|
6170
|
+
target: external_exports.string(),
|
|
6171
|
+
state: external_exports.enum(["armed", "dropped", "settled"]),
|
|
6172
|
+
createdAt: external_exports.string(),
|
|
6173
|
+
/** The PR's checks/mergeability as the poller last saw it. Absent until the first successful poll. */
|
|
6174
|
+
github: GithubWatchStatus.optional()
|
|
6175
|
+
}).strict();
|
|
6176
|
+
AddOwnPrWatchInput = external_exports.object({
|
|
6177
|
+
slug: ThreadSlug,
|
|
6178
|
+
/** `owner/repo#123` or a PR URL. Parsed server-side; an unparseable ref is refused rather than stored,
|
|
6179
|
+
* because a watcher that can never fire is worse than none — the worker rests believing it is covered. */
|
|
6180
|
+
target: external_exports.string().trim().min(1).max(200),
|
|
6181
|
+
/** How long to watch, as a DURATION (`30m`, `2h`, `3d` — parseAwaitingDuration).
|
|
6182
|
+
*
|
|
6183
|
+
* A PR nobody ever reviews would otherwise be polled forever, and a thread parked on it would wait
|
|
6184
|
+
* forever with it — the same unbounded wait the awaiting fence's `for:` closes, one level down. A
|
|
6185
|
+
* duration rather than an instant for the same reason it is one there: it cannot be written in the
|
|
6186
|
+
* past (maintainer 2026-08-15, asking for it explicitly).
|
|
6187
|
+
*
|
|
6188
|
+
* REQUIRED BY THE TOOL, OPTIONAL ON THE WIRE, and the asymmetry is deliberate. A worker's MCP server
|
|
6189
|
+
* outlives every frizz restart, so a session dispatched before this existed still holds a binary that
|
|
6190
|
+
* cannot send it — and making the RPC reject that would break `watch_pr` outright for every thread
|
|
6191
|
+
* already running, with no recourse from inside those threads. Absent ⇒ PR_WATCH_DEFAULT_FOR, which
|
|
6192
|
+
* still BOUNDS the poll; the point of the field is that a worker chooses, and one that cannot choose
|
|
6193
|
+
* is better bounded than broken. */
|
|
6194
|
+
for: external_exports.string().trim().min(1).max(16).optional()
|
|
6195
|
+
}).strict();
|
|
6196
|
+
AddOwnPrWatchResult = external_exports.object({
|
|
6197
|
+
id: external_exports.string(),
|
|
6198
|
+
target: external_exports.string(),
|
|
6199
|
+
/** True when this exact PR was ALREADY watched by this thread, so the call registered nothing new.
|
|
6200
|
+
* Re-registering after a compaction is the common case, and a duplicate would double every wake. */
|
|
6201
|
+
alreadyArmed: external_exports.boolean(),
|
|
6202
|
+
watches: external_exports.array(PrWatchView)
|
|
6203
|
+
}).strict();
|
|
6204
|
+
DropOwnPrWatchInput = external_exports.object({
|
|
6205
|
+
slug: ThreadSlug,
|
|
6206
|
+
id: external_exports.string().min(1).max(64)
|
|
6207
|
+
}).strict();
|
|
6208
|
+
DropOwnPrWatchResult = external_exports.object({
|
|
6209
|
+
dropped: external_exports.boolean(),
|
|
6210
|
+
watches: external_exports.array(PrWatchView)
|
|
6025
6211
|
}).strict();
|
|
6212
|
+
ListOwnPrWatchesInput = external_exports.object({ slug: ThreadSlug }).strict();
|
|
6213
|
+
OwnPrWatchesResult = external_exports.object({ watches: external_exports.array(PrWatchView) }).strict();
|
|
6026
6214
|
ThreadTimerView = external_exports.object({
|
|
6027
6215
|
id: external_exports.string(),
|
|
6028
6216
|
prompt: external_exports.string(),
|
|
@@ -6180,6 +6368,16 @@ var init_src = __esm({
|
|
|
6180
6368
|
// AUTO-bump (the scheduler resumes the agent with exactly this text) rather than a reminder, which is
|
|
6181
6369
|
// the distinction the held row's tooltip renders. Absent ⇒ the card merely re-surfaces.
|
|
6182
6370
|
snoozePrompt: external_exports.string().optional(),
|
|
6371
|
+
/** The EVENT snooze on the resting card is armed for this exact rest — the human has said "hide this
|
|
6372
|
+
* until something reports". Distinct from `snoozedUntil`, which is a wall-clock park on the whole
|
|
6373
|
+
* thread: this one has no deadline and clears itself when the thread comes to a NEW rest.
|
|
6374
|
+
*
|
|
6375
|
+
* It travels because the chat has to honour it too (2026-08-14). Until then only the QUEUE did, on
|
|
6376
|
+
* the reasoning that the card states a FACT the drawer must keep showing or it blanks at rest and
|
|
6377
|
+
* reads as "the agent died". That reasoning holds for a thread nobody has parked; once the human has
|
|
6378
|
+
* explicitly parked THIS rest, showing them the same card with the same button one surface over is
|
|
6379
|
+
* not information, and they said so. */
|
|
6380
|
+
bgSnoozed: external_exports.boolean().optional(),
|
|
6183
6381
|
// Which Claude transport serves this thread: "broker" = a session-broker-owned Agent SDK session
|
|
6184
6382
|
// (typed control channel), "tmux" = the interactive TUI in a pane. Only the broker can be asked to
|
|
6185
6383
|
// reload its plugin closure in place, so the board needs it to decide whether to offer that verb at
|
|
@@ -6297,6 +6495,13 @@ var init_src = __esm({
|
|
|
6297
6495
|
// Optional so a pre-restart server keeps working; absent means "fall back to `/`", i.e. the old
|
|
6298
6496
|
// behaviour. Registry-derived, so it is the same slug every other surface links to.
|
|
6299
6497
|
projectSlug: external_exports.string().optional(),
|
|
6498
|
+
// The server's home directory — the expansion of a `~` a worker wrote in prose. Agents reference
|
|
6499
|
+
// files that way constantly (`~/.claude/CLAUDE.md`), and the browser has no way to derive it, so a
|
|
6500
|
+
// `~`-anchored Markdown link had no absolute path to become and stayed a same-origin anchor that
|
|
6501
|
+
// navigated out of Frizz. The client only ever uses it to build a path it then hands BACK to the
|
|
6502
|
+
// server, which realpath-gates it exactly as it gates one the author typed in full. Optional so a
|
|
6503
|
+
// pre-restart server keeps working; absent means `~` links stay unresolved, i.e. the old behaviour.
|
|
6504
|
+
homeDir: external_exports.string().optional(),
|
|
6300
6505
|
// (No `.frizz/ exists` bit here on purpose. Threads are session-first — the ui.db registry IS the
|
|
6301
6506
|
// board — so `.frizz/` presence says nothing about whether this project has one. Its only consumer
|
|
6302
6507
|
// was a shell gate that dead-ended `.frizz`-less repos; the server still probes the directory
|
|
@@ -6403,13 +6608,23 @@ var init_src = __esm({
|
|
|
6403
6608
|
// Default action for a vetted non-image local path in agent markdown. Image clicks always use the
|
|
6404
6609
|
// OS default viewer so screenshots retain their expected behavior.
|
|
6405
6610
|
localFileOpener: LocalFileOpener.optional(),
|
|
6406
|
-
// GitHub batch-dispatch prompt
|
|
6407
|
-
// unset OR blank the server falls back to its exported
|
|
6408
|
-
//
|
|
6409
|
-
//
|
|
6410
|
-
//
|
|
6411
|
-
|
|
6412
|
-
|
|
6611
|
+
// The GitHub batch-dispatch prompt template (the picker's per-item worker prompt). Optional: when
|
|
6612
|
+
// unset OR blank the server falls back to its exported DEFAULT_GITHUB_PROMPT. Substitution tokens
|
|
6613
|
+
// the server fills: {repo} {n} {title} {url} {labels} {body}. The leading `THREAD: <slug>` tag is
|
|
6614
|
+
// prepended by the server (not part of the editable template) so a custom prompt can never break the
|
|
6615
|
+
// thread↔.frizz-file binding. Optional so old settings blobs parse.
|
|
6616
|
+
//
|
|
6617
|
+
// ONE field, not one per kind. Issue and PR each had their own template until 2026-08-15, and the two
|
|
6618
|
+
// said the same thing twice: read the whole thread, be skeptical, cite what you checked, post nothing.
|
|
6619
|
+
// A person tuning "be more dubious" had to make the same edit in two boxes and keep them in step. The
|
|
6620
|
+
// ONE thing that genuinely differs per kind — which `gh` command reads the item — is a line in the
|
|
6621
|
+
// metadata block, so the merged template just carries both, and the worker picks the one that applies.
|
|
6622
|
+
//
|
|
6623
|
+
// There is no migration off the two old keys, and that is deliberate (the maintainer's call): Settings
|
|
6624
|
+
// is a non-strict z.object, so `githubIssuePrompt`/`githubPrPrompt` are STRIPPED the moment an old
|
|
6625
|
+
// blob is parsed. Any existing override is dropped and the reader gets the new shipped default —
|
|
6626
|
+
// exactly the intended backfill. Merging two customized templates into one has no correct answer.
|
|
6627
|
+
githubPrompt: external_exports.string().optional()
|
|
6413
6628
|
});
|
|
6414
6629
|
DispatchProviderPreferences = external_exports.object({
|
|
6415
6630
|
model: external_exports.string().trim().min(1).max(200).optional(),
|
|
@@ -6538,9 +6753,6 @@ var init_src = __esm({
|
|
|
6538
6753
|
// so a client that predates it — an older tab, an older MCP server — keeps writing the row correctly
|
|
6539
6754
|
// with the trigger off, which is the honest reading of a caller that has never heard of it.
|
|
6540
6755
|
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
6756
|
intervalSeconds: RecurringIntervalSeconds.optional()
|
|
6545
6757
|
}).strict();
|
|
6546
6758
|
SetOwnThreadRecurringPromptInput = external_exports.object({
|
|
@@ -6552,8 +6764,14 @@ var init_src = __esm({
|
|
|
6552
6764
|
// so a client that predates it — an older tab, an older MCP server — keeps writing the row correctly
|
|
6553
6765
|
// with the trigger off, which is the honest reading of a caller that has never heard of it.
|
|
6554
6766
|
postCompaction: external_exports.boolean().default(false),
|
|
6555
|
-
|
|
6556
|
-
|
|
6767
|
+
/** ACCEPTED AND IGNORED. The question hold was deleted 2026-08-16 (see scheduler.ts) and no caller in
|
|
6768
|
+
* this repo sends it any more — but this object is `.strict()`, and the caller on the other end is a
|
|
6769
|
+
* DETACHED worker daemon holding the `frizz-mcp.mjs` it was spawned with. Those outlive a server
|
|
6770
|
+
* restart by design, so for as long as any pre-2026-08-16 worker is alive a `start` would otherwise
|
|
6771
|
+
* come back as a validation error on a field the model cannot see it is sending. Tolerated here rather
|
|
6772
|
+
* than in the router so the shape stays one declaration; delete it once no such worker can be running.
|
|
6773
|
+
* The BROWSER input above needs no such clause — a stale tab is one reload away. */
|
|
6774
|
+
pauseOnQuestions: external_exports.boolean().optional(),
|
|
6557
6775
|
intervalSeconds: RecurringIntervalSeconds.optional()
|
|
6558
6776
|
}).strict();
|
|
6559
6777
|
SetOwnThreadRecurringPromptResult = external_exports.object({
|
|
@@ -6580,6 +6798,23 @@ var init_src = __esm({
|
|
|
6580
6798
|
OwnThreadTimersResult = external_exports.object({
|
|
6581
6799
|
timers: external_exports.array(ThreadTimerView)
|
|
6582
6800
|
}).strict();
|
|
6801
|
+
ThreadActivityItem = external_exports.object({
|
|
6802
|
+
kind: external_exports.enum(["shell", "agent", "timer", "pr"]),
|
|
6803
|
+
/** The string a `<kind>:` fence line must carry. For a shell that is the runtime task id the worker
|
|
6804
|
+
* was shown; for a PR, `owner/repo#N`; for a timer, its `tmr_…` row id. */
|
|
6805
|
+
id: external_exports.string(),
|
|
6806
|
+
label: external_exports.string(),
|
|
6807
|
+
/** ISO8601 of when it started or was armed — absent when frizz has no instant for it. */
|
|
6808
|
+
since: external_exports.string().optional(),
|
|
6809
|
+
/** A timer's fire instant, or a PR's expiry. Absent for shells and sub-agents. */
|
|
6810
|
+
until: external_exports.string().optional()
|
|
6811
|
+
}).strict();
|
|
6812
|
+
ListOwnThreadActivityInput = external_exports.object({
|
|
6813
|
+
slug: ThreadSlug
|
|
6814
|
+
}).strict();
|
|
6815
|
+
OwnThreadActivityResult = external_exports.object({
|
|
6816
|
+
activity: external_exports.array(ThreadActivityItem)
|
|
6817
|
+
}).strict();
|
|
6583
6818
|
SetOwnThreadTimerResult = external_exports.object({
|
|
6584
6819
|
id: external_exports.string(),
|
|
6585
6820
|
fireAt: external_exports.string(),
|
|
@@ -6589,49 +6824,6 @@ var init_src = __esm({
|
|
|
6589
6824
|
cancelled: external_exports.boolean(),
|
|
6590
6825
|
timers: external_exports.array(ThreadTimerView)
|
|
6591
6826
|
}).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
6827
|
SetOwnThreadStopHookInput = external_exports.object({
|
|
6636
6828
|
slug: ThreadSlug,
|
|
6637
6829
|
prompt: RecurringPromptText.nullable(),
|
|
@@ -6742,6 +6934,43 @@ ${DISPATCH_TASK_BANNER}
|
|
|
6742
6934
|
nameWithOwner: external_exports.string().nullable(),
|
|
6743
6935
|
authed: external_exports.boolean()
|
|
6744
6936
|
});
|
|
6937
|
+
GithubRefCard = external_exports.object({
|
|
6938
|
+
ref: external_exports.string(),
|
|
6939
|
+
kind: external_exports.enum(["issue", "pr", "commit"]),
|
|
6940
|
+
repo: external_exports.string(),
|
|
6941
|
+
// owner/repo, for the card's header line
|
|
6942
|
+
url: external_exports.string(),
|
|
6943
|
+
title: external_exports.string(),
|
|
6944
|
+
body: external_exports.string(),
|
|
6945
|
+
// already truncated server-side to the card's excerpt budget
|
|
6946
|
+
state: external_exports.string(),
|
|
6947
|
+
// OPEN | CLOSED | MERGED | DRAFT — empty for a commit, which has no state
|
|
6948
|
+
stateReason: external_exports.string().optional(),
|
|
6949
|
+
// COMPLETED | NOT_PLANNED | REOPENED — GitHub's closed-issue nuance
|
|
6950
|
+
at: external_exports.string().optional(),
|
|
6951
|
+
// ISO: opened-at for an issue/PR, committed-at for a commit
|
|
6952
|
+
authorLogin: external_exports.string().optional(),
|
|
6953
|
+
authorName: external_exports.string().optional(),
|
|
6954
|
+
// commits carry a git author name with no GitHub account behind it
|
|
6955
|
+
authorAvatar: external_exports.string().optional(),
|
|
6956
|
+
labels: external_exports.array(external_exports.object({ name: external_exports.string(), color: external_exports.string() })).default([]),
|
|
6957
|
+
additions: external_exports.number().int().nonnegative().optional(),
|
|
6958
|
+
deletions: external_exports.number().int().nonnegative().optional(),
|
|
6959
|
+
changedFiles: external_exports.number().int().nonnegative().optional(),
|
|
6960
|
+
comments: external_exports.number().int().nonnegative().optional(),
|
|
6961
|
+
// Epoch ms of the fetch this card came from. The CLIENT owns the freshness decision (render the
|
|
6962
|
+
// cached card instantly, then revalidate if it is old), so it has to be able to see the age.
|
|
6963
|
+
fetchedAt: external_exports.number().int().nonnegative()
|
|
6964
|
+
});
|
|
6965
|
+
GithubRefPreviewInput = external_exports.object({
|
|
6966
|
+
refs: external_exports.array(external_exports.string().min(3).max(120)).min(1).max(100),
|
|
6967
|
+
refresh: external_exports.boolean().default(false)
|
|
6968
|
+
});
|
|
6969
|
+
GithubRefPreviewResult = external_exports.object({
|
|
6970
|
+
cards: external_exports.array(GithubRefCard),
|
|
6971
|
+
missing: external_exports.array(external_exports.string()),
|
|
6972
|
+
error: external_exports.string().optional()
|
|
6973
|
+
});
|
|
6745
6974
|
GithubItem = external_exports.object({
|
|
6746
6975
|
kind: external_exports.enum(["issue", "pr"]),
|
|
6747
6976
|
number: external_exports.number().int().positive(),
|
|
@@ -7798,11 +8027,11 @@ ${GITHUB_DISPATCH_UI_BOUNDARY}
|
|
|
7798
8027
|
|
|
7799
8028
|
${filled}`;
|
|
7800
8029
|
}
|
|
7801
|
-
function effectiveTemplate(
|
|
8030
|
+
function effectiveTemplate(custom2) {
|
|
7802
8031
|
if (custom2 && custom2.trim().length > 0) return custom2;
|
|
7803
|
-
return
|
|
8032
|
+
return DEFAULT_GITHUB_PROMPT;
|
|
7804
8033
|
}
|
|
7805
|
-
var pexec, GH_TIMEOUT, GH_MAXBUF, SEARCH_SORT, SEARCH_RESULT_WINDOW, LINKED_PR_CHUNK, GITHUB_PAGE_SIZE, BODY_CAP,
|
|
8034
|
+
var pexec, GH_TIMEOUT, GH_MAXBUF, SEARCH_SORT, SEARCH_RESULT_WINDOW, LINKED_PR_CHUNK, GITHUB_PAGE_SIZE, BODY_CAP, DEFAULT_GITHUB_PROMPT;
|
|
7806
8035
|
var init_github = __esm({
|
|
7807
8036
|
"packages/server/src/github.ts"() {
|
|
7808
8037
|
"use strict";
|
|
@@ -7818,26 +8047,30 @@ var init_github = __esm({
|
|
|
7818
8047
|
LINKED_PR_CHUNK = 25;
|
|
7819
8048
|
GITHUB_PAGE_SIZE = 30;
|
|
7820
8049
|
BODY_CAP = 8 * 1024;
|
|
7821
|
-
|
|
8050
|
+
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
8051
|
|
|
7823
|
-
|
|
8052
|
+
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
8053
|
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
8054
|
+
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.
|
|
8055
|
+
|
|
8056
|
+
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.
|
|
8057
|
+
|
|
8058
|
+
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.
|
|
8059
|
+
|
|
8060
|
+
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.
|
|
8061
|
+
|
|
8062
|
+
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
8063
|
|
|
7832
8064
|
---
|
|
7833
8065
|
|
|
7834
|
-
PR #{n}: {title}
|
|
8066
|
+
Issue/PR #{n}: {title}
|
|
7835
8067
|
Repository: {repo}
|
|
7836
8068
|
URL: {url}
|
|
7837
8069
|
Labels: {labels}
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
8070
|
+
Body + thread (issues): \`gh issue view {n} -R {repo} --comments\`
|
|
8071
|
+
Body + thread (PRs): \`gh pr view {n} -R {repo} --comments\`
|
|
8072
|
+
Diff (PRs): \`gh pr diff {n} -R {repo}\`
|
|
8073
|
+
CI (PRs): \`gh pr checks {n} -R {repo}\``;
|
|
7841
8074
|
}
|
|
7842
8075
|
});
|
|
7843
8076
|
|
|
@@ -10440,41 +10673,39 @@ function createStorage(dbPath) {
|
|
|
10440
10673
|
ON thread_timer(state, fire_at);
|
|
10441
10674
|
CREATE INDEX IF NOT EXISTS thread_timer_slug
|
|
10442
10675
|
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."
|
|
10676
|
+
-- A worker's registered PR WATCHERS (2026-08-14). Registered by tool call, never by a fence: the
|
|
10677
|
+
-- fence states what a thread is waiting on, and watching is a separate, orthogonal thing that
|
|
10678
|
+
-- happens whether or not anything is written down (maintainer: "We should have a tool for this. The
|
|
10679
|
+
-- agent should have a tool to register a PR watcher").
|
|
10452
10680
|
--
|
|
10453
|
-
--
|
|
10454
|
-
--
|
|
10455
|
-
CREATE TABLE IF NOT EXISTS
|
|
10681
|
+
-- REPEATING, not one-shot. It reports CI turning green or red and every later review or comment, so
|
|
10682
|
+
-- it stays armed and carries a cursor; only a merge or a close settles it.
|
|
10683
|
+
CREATE TABLE IF NOT EXISTS pr_watch (
|
|
10456
10684
|
id TEXT PRIMARY KEY,
|
|
10457
10685
|
thread_slug TEXT NOT NULL,
|
|
10458
|
-
|
|
10459
|
-
|
|
10460
|
-
|
|
10686
|
+
owner TEXT NOT NULL,
|
|
10687
|
+
repo TEXT NOT NULL,
|
|
10688
|
+
number INTEGER NOT NULL,
|
|
10689
|
+
state TEXT NOT NULL CHECK (state IN ('armed', 'dropped', 'settled')),
|
|
10461
10690
|
created_at INTEGER NOT NULL,
|
|
10462
10691
|
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
10692
|
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
|
|
10693
|
+
-- When this watcher stops polling by itself. REQUIRED at registration (2026-08-15): a PR nobody
|
|
10694
|
+
-- ever touches would otherwise be polled forever, and the thread parked on it would wait forever
|
|
10695
|
+
-- with it. Nullable in the column only so an older row reads; the tool refuses to arm without one.
|
|
10696
|
+
expires_at INTEGER
|
|
10473
10697
|
);
|
|
10474
|
-
CREATE INDEX IF NOT EXISTS
|
|
10475
|
-
ON
|
|
10476
|
-
CREATE INDEX IF NOT EXISTS
|
|
10477
|
-
ON
|
|
10698
|
+
CREATE INDEX IF NOT EXISTS pr_watch_armed
|
|
10699
|
+
ON pr_watch(state);
|
|
10700
|
+
CREATE INDEX IF NOT EXISTS pr_watch_slug
|
|
10701
|
+
ON pr_watch(thread_slug, state, created_at);
|
|
10702
|
+
-- thread_watch WAS HERE and is retired (2026-08-14). A worker's wait is a 'watch:' line in its own
|
|
10703
|
+
-- awaiting fence again, which is BOTH the park and the wake -- the scheduler matches the name against
|
|
10704
|
+
-- the thread's live shells and its retired-shell ring, so nothing needs registering and nothing can
|
|
10705
|
+
-- outlive the fence that declares it. The registry existed because a fence had no identity to drop;
|
|
10706
|
+
-- the answer turned out to be that a park nobody has to drop needs none. An old DB keeps the orphan
|
|
10707
|
+
-- table, which costs nothing and is safer than a migration to remove it.
|
|
10708
|
+
-- (No backticks in this comment: the whole schema is a template literal.)
|
|
10478
10709
|
`);
|
|
10479
10710
|
for (const col of [
|
|
10480
10711
|
"archived INTEGER NOT NULL DEFAULT 0",
|
|
@@ -10549,16 +10780,21 @@ function createStorage(dbPath) {
|
|
|
10549
10780
|
// which is the correct default: an existing prompt described the triggers its operator chose.
|
|
10550
10781
|
"recurring_on_compact INTEGER NOT NULL DEFAULT 0",
|
|
10551
10782
|
"recurring_compact_fired_at TEXT",
|
|
10552
|
-
//
|
|
10553
|
-
//
|
|
10554
|
-
//
|
|
10555
|
-
|
|
10783
|
+
// NO `recurring_pause_on_questions` HERE ANY MORE. It held every trigger while the thread was waiting
|
|
10784
|
+
// on the human and the footer showed it inverted as "Autonomous mode"; both were deleted 2026-08-16
|
|
10785
|
+
// (see scheduler.ts, "WHAT A PENDING QUESTION DOES TO THE THREE TRIGGERS"). A database created before
|
|
10786
|
+
// that release still carries the column — it is `NOT NULL DEFAULT 0`, so nothing needs to write it,
|
|
10787
|
+
// and dropping it would cost a table rebuild to reclaim one inert integer per row.
|
|
10556
10788
|
// THE BUILT-IN SIGN-OFF NUDGE (scheduler SOURCE 9, 2026-08-12). How many times in a row frizz has
|
|
10557
10789
|
// told this thread how to sign off without a fence appearing. Cleared ONLY when the thread signs
|
|
10558
10790
|
// off — never by a user record, because frizz's own delivery is one. The second column holds the
|
|
10559
10791
|
// last-nudged delivery id, for diagnosis.
|
|
10560
10792
|
"signoff_nudges INTEGER NOT NULL DEFAULT 0",
|
|
10561
10793
|
"signoff_nudge_anchor TEXT",
|
|
10794
|
+
// Cleared by `resetParkBumps` when a park is actually HONOURED — the one event that proves the
|
|
10795
|
+
// correction landed, and the one frizz cannot cause by correcting.
|
|
10796
|
+
"park_bumps INTEGER NOT NULL DEFAULT 0",
|
|
10797
|
+
"park_bump_anchor TEXT",
|
|
10562
10798
|
// Title provenance for the CURRENT text (2026-08-07): 1 = the worker's own title signal wrote it,
|
|
10563
10799
|
// 0 = the dispatch seeded it. DEFAULT 0 is the conservative direction — an existing row is assumed
|
|
10564
10800
|
// to hold its dispatch chop until the repair below (or the next title signal) says otherwise.
|
|
@@ -10569,6 +10805,10 @@ function createStorage(dbPath) {
|
|
|
10569
10805
|
} catch {
|
|
10570
10806
|
}
|
|
10571
10807
|
}
|
|
10808
|
+
try {
|
|
10809
|
+
db.exec("ALTER TABLE pr_watch ADD COLUMN expires_at INTEGER");
|
|
10810
|
+
} catch {
|
|
10811
|
+
}
|
|
10572
10812
|
try {
|
|
10573
10813
|
db.exec("UPDATE session SET tmux_name = 'frizz-' || substr(tmux_name, 6) WHERE tmux_name LIKE 'fray-%'");
|
|
10574
10814
|
} catch {
|
|
@@ -10925,10 +11165,6 @@ function createStorage(dbPath) {
|
|
|
10925
11165
|
recurring_on_rest = ?,
|
|
10926
11166
|
recurring_on_schedule = ?,
|
|
10927
11167
|
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
11168
|
recurring_armed_at = CASE
|
|
10933
11169
|
WHEN ? IS NULL THEN NULL
|
|
10934
11170
|
WHEN recurring_armed_at IS NOT NULL AND recurring_prompt IS ? AND recurring_interval_ms IS ? THEN recurring_armed_at
|
|
@@ -10945,7 +11181,7 @@ function createStorage(dbPath) {
|
|
|
10945
11181
|
WHEN ? IS NULL THEN NULL
|
|
10946
11182
|
WHEN recurring_armed_at IS NOT NULL AND recurring_prompt IS ? THEN recurring_compact_fired_at
|
|
10947
11183
|
ELSE NULL END`;
|
|
10948
|
-
const recurringArgs = ({ prompt, stopHook, heartbeat, postCompaction,
|
|
11184
|
+
const recurringArgs = ({ prompt, stopHook, heartbeat, postCompaction, intervalMs, armedAt }) => {
|
|
10949
11185
|
const ms = prompt === null ? null : intervalMs;
|
|
10950
11186
|
const flag = (on) => prompt === null || !on ? 0 : 1;
|
|
10951
11187
|
return [
|
|
@@ -10955,7 +11191,6 @@ function createStorage(dbPath) {
|
|
|
10955
11191
|
flag(stopHook),
|
|
10956
11192
|
flag(heartbeat),
|
|
10957
11193
|
flag(postCompaction),
|
|
10958
|
-
flag(pauseOnQuestions),
|
|
10959
11194
|
prompt,
|
|
10960
11195
|
prompt,
|
|
10961
11196
|
ms,
|
|
@@ -11015,35 +11250,41 @@ function createStorage(dbPath) {
|
|
|
11015
11250
|
UPDATE session SET signoff_nudges = 0, signoff_nudge_anchor = NULL
|
|
11016
11251
|
WHERE slug = ? AND signoff_nudges > 0
|
|
11017
11252
|
`);
|
|
11018
|
-
const
|
|
11019
|
-
|
|
11020
|
-
|
|
11253
|
+
const countParkBumpStmt = db.prepare(`
|
|
11254
|
+
UPDATE session SET park_bumps = park_bumps + 1, park_bump_anchor = ?
|
|
11255
|
+
WHERE slug = ?
|
|
11021
11256
|
`);
|
|
11022
|
-
const
|
|
11023
|
-
|
|
11257
|
+
const resetParkBumpsStmt = db.prepare(`
|
|
11258
|
+
UPDATE session SET park_bumps = 0, park_bump_anchor = NULL
|
|
11259
|
+
WHERE slug = ? AND park_bumps > 0
|
|
11260
|
+
`);
|
|
11261
|
+
const armPrWatchStmt = db.prepare(`
|
|
11262
|
+
INSERT INTO pr_watch (id, thread_slug, owner, repo, number, state, created_at, settled_at, cursor, expires_at)
|
|
11263
|
+
VALUES (@id, @slug, @owner, @repo, @number, 'armed', @createdAtMs, NULL, NULL, @expiresAtMs)
|
|
11264
|
+
`);
|
|
11265
|
+
const expiredPrWatchesStmt = db.prepare(
|
|
11266
|
+
"SELECT * FROM pr_watch WHERE state = 'armed' AND expires_at IS NOT NULL AND expires_at <= ? ORDER BY expires_at, id"
|
|
11024
11267
|
);
|
|
11025
|
-
const
|
|
11026
|
-
"SELECT * FROM
|
|
11268
|
+
const prWatchesBySlugStmt = db.prepare(
|
|
11269
|
+
"SELECT * FROM pr_watch WHERE thread_slug = ? ORDER BY created_at, id"
|
|
11027
11270
|
);
|
|
11028
|
-
const
|
|
11029
|
-
|
|
11030
|
-
"SELECT * FROM thread_watch WHERE state = 'armed' ORDER BY created_at, id"
|
|
11271
|
+
const armedPrWatchesBySlugStmt = db.prepare(
|
|
11272
|
+
"SELECT * FROM pr_watch WHERE thread_slug = ? AND state = 'armed' ORDER BY created_at, id"
|
|
11031
11273
|
);
|
|
11032
|
-
const
|
|
11033
|
-
|
|
11034
|
-
|
|
11035
|
-
|
|
11036
|
-
|
|
11274
|
+
const prWatchByIdStmt = db.prepare("SELECT * FROM pr_watch WHERE id = ?");
|
|
11275
|
+
const armedPrWatchesStmt = db.prepare(
|
|
11276
|
+
"SELECT * FROM pr_watch WHERE state = 'armed' ORDER BY created_at, id"
|
|
11277
|
+
);
|
|
11278
|
+
const dropPrWatchStmt = db.prepare(`
|
|
11279
|
+
UPDATE pr_watch SET state = 'dropped', settled_at = ?
|
|
11037
11280
|
WHERE id = ? AND thread_slug = ? AND state = 'armed'
|
|
11038
11281
|
`);
|
|
11039
|
-
const
|
|
11040
|
-
UPDATE
|
|
11282
|
+
const settlePrWatchStmt = db.prepare(`
|
|
11283
|
+
UPDATE pr_watch SET state = 'settled', settled_at = ?
|
|
11041
11284
|
WHERE id = ? AND state = 'armed'
|
|
11042
11285
|
`);
|
|
11043
|
-
const
|
|
11044
|
-
|
|
11045
|
-
`);
|
|
11046
|
-
const delThreadWatches = db.prepare("DELETE FROM thread_watch WHERE thread_slug = ?");
|
|
11286
|
+
const prWatchCursorStmt = db.prepare("UPDATE pr_watch SET cursor = ? WHERE id = ? AND state = 'armed'");
|
|
11287
|
+
const delPrWatches = db.prepare("DELETE FROM pr_watch WHERE thread_slug = ?");
|
|
11047
11288
|
const confirmAwaitingWaitStmt = db.prepare(`
|
|
11048
11289
|
UPDATE session
|
|
11049
11290
|
SET awaiting_fence_id = ?, awaiting_confirmed_at = ?, snoozed_until = ?
|
|
@@ -11106,7 +11347,7 @@ function createStorage(dbPath) {
|
|
|
11106
11347
|
}
|
|
11107
11348
|
delRetiredOps.run(existing.slug);
|
|
11108
11349
|
delThreadTimers.run(existing.slug);
|
|
11109
|
-
|
|
11350
|
+
delPrWatches.run(existing.slug);
|
|
11110
11351
|
delSession.run(existing.slug);
|
|
11111
11352
|
return existing;
|
|
11112
11353
|
};
|
|
@@ -11544,14 +11785,16 @@ function createStorage(dbPath) {
|
|
|
11544
11785
|
setRecurringPromptBySlug: (slug, write) => recurringBySlugStmt.run(...recurringArgs(write), slug).changes === 1,
|
|
11545
11786
|
countSignoffNudge: (slug, anchor) => void countNudgeStmt.run(anchor, slug),
|
|
11546
11787
|
resetSignoffNudges: (slug) => void resetNudgesStmt.run(slug),
|
|
11547
|
-
|
|
11548
|
-
|
|
11549
|
-
|
|
11550
|
-
|
|
11551
|
-
|
|
11552
|
-
|
|
11553
|
-
|
|
11554
|
-
|
|
11788
|
+
countParkBump: (slug, anchor) => void countParkBumpStmt.run(anchor, slug),
|
|
11789
|
+
resetParkBumps: (slug) => void resetParkBumpsStmt.run(slug),
|
|
11790
|
+
armPrWatch: (watch) => void armPrWatchStmt.run(watch),
|
|
11791
|
+
listPrWatches: (slug, opts) => (opts?.armedOnly ? armedPrWatchesBySlugStmt : prWatchesBySlugStmt).all(slug),
|
|
11792
|
+
getPrWatch: (id) => prWatchByIdStmt.get(id),
|
|
11793
|
+
armedPrWatches: () => armedPrWatchesStmt.all(),
|
|
11794
|
+
expiredPrWatches: (nowMs) => expiredPrWatchesStmt.all(nowMs),
|
|
11795
|
+
dropPrWatch: (slug, id, settledAtMs) => dropPrWatchStmt.run(settledAtMs, id, slug).changes === 1,
|
|
11796
|
+
settlePrWatch: (id, settledAtMs) => settlePrWatchStmt.run(settledAtMs, id).changes === 1,
|
|
11797
|
+
setPrWatchCursor: (id, cursor) => prWatchCursorStmt.run(cursor, id).changes === 1,
|
|
11555
11798
|
armThreadTimer: (timer) => void armTimerStmt.run(timer),
|
|
11556
11799
|
listThreadTimers: (slug, opts) => (opts?.armedOnly ? armedTimersBySlugStmt : timersBySlugStmt).all(slug),
|
|
11557
11800
|
getThreadTimer: (id) => timerByIdStmt.get(id),
|
|
@@ -13334,15 +13577,25 @@ function matchComposedText(items, recordText, contemporaneous) {
|
|
|
13334
13577
|
}
|
|
13335
13578
|
return matched;
|
|
13336
13579
|
}
|
|
13337
|
-
function ageDeliveries(items, nowMs) {
|
|
13580
|
+
function ageDeliveries(items, nowMs, observedUserAt) {
|
|
13338
13581
|
if (!items.length) return items;
|
|
13339
13582
|
let changed = false;
|
|
13583
|
+
const userAtMs = Date.parse(observedUserAt ?? "");
|
|
13584
|
+
const supersededByUserTurn = (item) => {
|
|
13585
|
+
if (!Number.isFinite(userAtMs)) return false;
|
|
13586
|
+
const born = Date.parse(item.at);
|
|
13587
|
+
return Number.isFinite(born) && userAtMs >= born;
|
|
13588
|
+
};
|
|
13340
13589
|
const next = [];
|
|
13341
13590
|
for (const item of items) {
|
|
13342
13591
|
if (item.state === "cancelled") {
|
|
13343
13592
|
next.push(item);
|
|
13344
13593
|
continue;
|
|
13345
13594
|
}
|
|
13595
|
+
if (supersededByUserTurn(item)) {
|
|
13596
|
+
changed = true;
|
|
13597
|
+
continue;
|
|
13598
|
+
}
|
|
13346
13599
|
const born = Date.parse(item.at);
|
|
13347
13600
|
if (item.state === "pending" && Number.isFinite(born) && nowMs - born > PENDING_TIMEOUT_MS) {
|
|
13348
13601
|
next.push({ ...item, state: "unconfirmed", updatedAt: new Date(nowMs).toISOString() });
|
|
@@ -14027,6 +14280,7 @@ function newTailState(slug, sessionId, path, foreign = false, nativeSessionId =
|
|
|
14027
14280
|
paneDead: false,
|
|
14028
14281
|
noTranscript: false,
|
|
14029
14282
|
nextDiscoverMs: 0,
|
|
14283
|
+
discoverMisses: 0,
|
|
14030
14284
|
stallLogged: false,
|
|
14031
14285
|
customTitleRevision: 0
|
|
14032
14286
|
};
|
|
@@ -14086,18 +14340,23 @@ function parseSignalFence(text) {
|
|
|
14086
14340
|
if (!kind) return void 0;
|
|
14087
14341
|
if (norm.slice(end).trim() !== "") return void 0;
|
|
14088
14342
|
if (kind === "done") return { kind, body: capFenceBody(raw2.trim()), hints: [] };
|
|
14343
|
+
const lines = raw2.split("\n");
|
|
14344
|
+
const delimiter2 = lines.findIndex((l) => /^\s*---+\s*$/.test(l));
|
|
14345
|
+
const frontmatter = delimiter2 === -1 ? lines : lines.slice(0, delimiter2);
|
|
14346
|
+
const prose = delimiter2 === -1 ? [] : lines.slice(delimiter2 + 1);
|
|
14089
14347
|
const hints = [];
|
|
14090
14348
|
const rest = [];
|
|
14091
|
-
for (const line of
|
|
14349
|
+
for (const line of frontmatter) {
|
|
14092
14350
|
const hm = line.match(AWAITING_HINT_RE);
|
|
14093
14351
|
const k = hm?.[1].toLowerCase();
|
|
14094
|
-
if (hm && (k === "
|
|
14352
|
+
if (hm && (k === "shell" || k === "agent" || k === "timer" || k === "pr" || k === "for" || k === "reason")) {
|
|
14095
14353
|
const value = hm[2].trim();
|
|
14096
14354
|
hints.push({ kind: k, value: value.length > HINT_VALUE_MAX ? value.slice(0, HINT_VALUE_MAX) : value });
|
|
14097
14355
|
} else {
|
|
14098
14356
|
rest.push(line);
|
|
14099
14357
|
}
|
|
14100
14358
|
}
|
|
14359
|
+
rest.push(...prose);
|
|
14101
14360
|
return { kind, body: capFenceBody(rest.join("\n").trim()), hints: hints.slice(0, HINT_MAX) };
|
|
14102
14361
|
}
|
|
14103
14362
|
function isRealUserMessage(content) {
|
|
@@ -14202,7 +14461,7 @@ function retireLive(state, entry, finishedAt, status) {
|
|
|
14202
14461
|
state.subAgents.delete(entry.toolUseId);
|
|
14203
14462
|
if (entry.kind === "shell") {
|
|
14204
14463
|
state.retiredShells.delete(entry.toolUseId);
|
|
14205
|
-
state.retiredShells.set(entry.toolUseId, { toolUseId: entry.toolUseId, command: entry.command, outputFile: entry.outputFile, status });
|
|
14464
|
+
state.retiredShells.set(entry.toolUseId, { toolUseId: entry.toolUseId, command: entry.command, outputFile: entry.outputFile, status, taskId: entry.taskId, label: entry.label, finishedAt });
|
|
14206
14465
|
while (state.retiredShells.size > RETAINED_SHELLS_MAX) {
|
|
14207
14466
|
const oldest = state.retiredShells.keys().next().value;
|
|
14208
14467
|
if (oldest === void 0) break;
|
|
@@ -14762,6 +15021,10 @@ function defaultBrokerDaemonAlive(project, now) {
|
|
|
14762
15021
|
return alive;
|
|
14763
15022
|
};
|
|
14764
15023
|
}
|
|
15024
|
+
function rowIsArchived(row) {
|
|
15025
|
+
if (row.state === "open" || row.state === "archived") return row.state === "archived";
|
|
15026
|
+
return row.archived === 1;
|
|
15027
|
+
}
|
|
14765
15028
|
function createTailer(deps) {
|
|
14766
15029
|
const now = deps.now ?? Date.now;
|
|
14767
15030
|
const paneDead = deps.paneDead ?? (() => true);
|
|
@@ -14977,10 +15240,15 @@ function createTailer(deps) {
|
|
|
14977
15240
|
for (const e of state.subAgents.values()) {
|
|
14978
15241
|
if (e.kind !== "shell") continue;
|
|
14979
15242
|
const lastActivityAt = entryLastActivity(e);
|
|
14980
|
-
out.push({ label: e.label, startedAt: e.startedAt, state: "running", id: e.toolUseId, ...e.taskId ? { stoppable: true } : {}, ...lastActivityAt ? { lastActivityAt } : {} });
|
|
15243
|
+
out.push({ label: e.label, startedAt: e.startedAt, state: "running", id: e.toolUseId, ...e.taskId ? { stoppable: true, taskId: e.taskId } : {}, ...lastActivityAt ? { lastActivityAt } : {} });
|
|
14981
15244
|
}
|
|
14982
15245
|
return out;
|
|
14983
15246
|
}
|
|
15247
|
+
function retiredShellViews(state) {
|
|
15248
|
+
const out = [];
|
|
15249
|
+
for (const r of state.retiredShells.values()) out.push({ id: r.toolUseId, taskId: r.taskId, label: r.label, status: r.status, finishedAt: r.finishedAt });
|
|
15250
|
+
return out;
|
|
15251
|
+
}
|
|
14984
15252
|
function codexBgShellViews(state) {
|
|
14985
15253
|
if (!deps.codexBackgroundExecs || state.foreign || state.paneDead) return [];
|
|
14986
15254
|
const execs = deps.codexBackgroundExecs(state.slug, state.sessionId);
|
|
@@ -15373,7 +15641,7 @@ ${ask}`;
|
|
|
15373
15641
|
const next = items.filter((item) => item.state !== "pending" && item.state !== "enqueued");
|
|
15374
15642
|
return next.length === items.length ? items : next;
|
|
15375
15643
|
}
|
|
15376
|
-
function ledgerFold(row, nowMs) {
|
|
15644
|
+
function ledgerFold(row, nowMs, state) {
|
|
15377
15645
|
if (!row.delivery_ledger) {
|
|
15378
15646
|
return { finish: () => ({ changed: false, value: null }) };
|
|
15379
15647
|
}
|
|
@@ -15393,7 +15661,7 @@ ${ask}`;
|
|
|
15393
15661
|
},
|
|
15394
15662
|
finish: () => {
|
|
15395
15663
|
items = retireDeliveriesLostWithTheDaemon(row, items);
|
|
15396
|
-
items = ageDeliveries(items, nowMs);
|
|
15664
|
+
items = ageDeliveries(items, nowMs, state.lastUserAt);
|
|
15397
15665
|
if (items === before) return { changed: false, value: null };
|
|
15398
15666
|
const value = serializeDeliveryLedger(items);
|
|
15399
15667
|
deps.storage.setDeliveryLedger(row.slug, value);
|
|
@@ -15562,6 +15830,7 @@ ${ask}`;
|
|
|
15562
15830
|
}
|
|
15563
15831
|
function captureStall(state, row) {
|
|
15564
15832
|
if (state.stallLogged) return;
|
|
15833
|
+
if (row.exited && rowIsArchived(row)) return;
|
|
15565
15834
|
state.stallLogged = true;
|
|
15566
15835
|
let pane = "";
|
|
15567
15836
|
try {
|
|
@@ -15571,11 +15840,11 @@ ${ask}`;
|
|
|
15571
15840
|
}
|
|
15572
15841
|
const authFailure = row.backend !== "codex" && isClaudeAuthErrorText(pane);
|
|
15573
15842
|
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
|
|
15843
|
+
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)";
|
|
15844
|
+
const detail = authFailure ? "(claude authentication failure \u2014 pane content redacted; sign in and retry)" : pane.trim() || evidence;
|
|
15576
15845
|
log.error(
|
|
15577
15846
|
"tailer",
|
|
15578
|
-
`thread ${row.slug} (session ${row.session_id}): no transcript ${DISCOVERY_GRACE_MS / 1e3}s after dispatch \u2014 likely a boot failure. ${
|
|
15847
|
+
`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
15848
|
);
|
|
15580
15849
|
try {
|
|
15581
15850
|
mkdirSync10(stallLogDir, { recursive: true });
|
|
@@ -15598,6 +15867,7 @@ ${detail}
|
|
|
15598
15867
|
if (size > 0) {
|
|
15599
15868
|
state.noTranscript = false;
|
|
15600
15869
|
state.stallLogged = false;
|
|
15870
|
+
state.discoverMisses = 0;
|
|
15601
15871
|
return true;
|
|
15602
15872
|
}
|
|
15603
15873
|
const spawnedMs = Date.parse(row.spawned_at);
|
|
@@ -15612,6 +15882,7 @@ ${detail}
|
|
|
15612
15882
|
state.primed = false;
|
|
15613
15883
|
state.noTranscript = false;
|
|
15614
15884
|
state.stallLogged = false;
|
|
15885
|
+
state.discoverMisses = 0;
|
|
15615
15886
|
return true;
|
|
15616
15887
|
}
|
|
15617
15888
|
const found = discoverTranscriptId(logDir, row.session_id, { nowMs, exclude: claimedIds(row.slug) });
|
|
@@ -15634,8 +15905,14 @@ ${detail}
|
|
|
15634
15905
|
state.primed = false;
|
|
15635
15906
|
state.noTranscript = false;
|
|
15636
15907
|
state.stallLogged = false;
|
|
15908
|
+
state.discoverMisses = 0;
|
|
15637
15909
|
return true;
|
|
15638
15910
|
}
|
|
15911
|
+
state.discoverMisses++;
|
|
15912
|
+
state.nextDiscoverMs = nowMs + Math.min(
|
|
15913
|
+
DISCOVER_RETRY_MS * 2 ** (state.discoverMisses - 1),
|
|
15914
|
+
DISCOVER_RETRY_MAX_MS
|
|
15915
|
+
);
|
|
15639
15916
|
state.noTranscript = true;
|
|
15640
15917
|
captureStall(state, row);
|
|
15641
15918
|
return true;
|
|
@@ -15647,6 +15924,15 @@ ${detail}
|
|
|
15647
15924
|
const nowMs = now();
|
|
15648
15925
|
adoptionBindings = /* @__PURE__ */ new Map();
|
|
15649
15926
|
const rows = deps.storage.allSessions();
|
|
15927
|
+
let coldVisibleRow = false;
|
|
15928
|
+
for (const row of rows) {
|
|
15929
|
+
if (rowIsArchived(row)) continue;
|
|
15930
|
+
const known = states.get(row.slug);
|
|
15931
|
+
if (!known || !known.primed) {
|
|
15932
|
+
coldVisibleRow = true;
|
|
15933
|
+
break;
|
|
15934
|
+
}
|
|
15935
|
+
}
|
|
15650
15936
|
let primed = 0;
|
|
15651
15937
|
let primedRows = 0;
|
|
15652
15938
|
const tickStartedMs = performance.now();
|
|
@@ -15672,13 +15958,19 @@ ${detail}
|
|
|
15672
15958
|
const prevNoTranscript = state.noTranscript;
|
|
15673
15959
|
if (row.backend !== "codex" && !resolveTranscript(state, row, nowMs)) continue;
|
|
15674
15960
|
if (!state.primed) {
|
|
15675
|
-
if (
|
|
15961
|
+
if (
|
|
15962
|
+
// Yield the slot to a still-cold visible row — but never at the cost of the forward-progress
|
|
15963
|
+
// guarantee below. `primedRows > 0` keeps the archive advancing by at least one row per tick
|
|
15964
|
+
// even if a visible row were somehow to stay cold indefinitely, so deferral can never become
|
|
15965
|
+
// starvation. The visible row still primes on this same tick; it just is not necessarily first.
|
|
15966
|
+
coldVisibleRow && primedRows > 0 && rowIsArchived(row) || primedRows >= MAX_PRIME_ROWS_PER_TICK || primedRows > 0 && performance.now() - tickStartedMs > PRIME_BUDGET_MS
|
|
15967
|
+
) {
|
|
15676
15968
|
primeIncomplete = true;
|
|
15677
15969
|
continue;
|
|
15678
15970
|
}
|
|
15679
15971
|
primedRows++;
|
|
15680
15972
|
const primeOffset = state.offset;
|
|
15681
|
-
const primeLedger = ledgerFold(row, nowMs);
|
|
15973
|
+
const primeLedger = ledgerFold(row, nowMs, state);
|
|
15682
15974
|
consume(state, backend, chainOnLine(primeLedger.onLine, codexSubAgentOnLine(row, state)));
|
|
15683
15975
|
state.codexSubAgents?.poll(nowMs);
|
|
15684
15976
|
const primedLedger = primeLedger.finish();
|
|
@@ -15734,7 +16026,7 @@ ${detail}
|
|
|
15734
16026
|
const prevTurn = state.turn;
|
|
15735
16027
|
const rowLedger = row.delivery_ledger ?? null;
|
|
15736
16028
|
const ledgerDrifted = rowLedger !== (state.deliveryLedgerSeen ?? null);
|
|
15737
|
-
const ledger = ledgerFold(row, nowMs);
|
|
16029
|
+
const ledger = ledgerFold(row, nowMs, state);
|
|
15738
16030
|
consume(state, backend, chainOnLine(ledger.onLine, codexSubAgentOnLine(row, state)));
|
|
15739
16031
|
state.codexSubAgents?.poll(nowMs);
|
|
15740
16032
|
const ledgerResult = ledger.finish();
|
|
@@ -15949,7 +16241,7 @@ ${detail}
|
|
|
15949
16241
|
if (!s) return void 0;
|
|
15950
16242
|
const pendingQuestion = s.turn === "idle" && s.lastAssistantHasQuestion;
|
|
15951
16243
|
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 };
|
|
16244
|
+
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
16245
|
},
|
|
15954
16246
|
// The CURRENT fresh foreign session ids (mtime within FOREIGN_FRESH_MS, capped), mtime-desc. Kept
|
|
15955
16247
|
// as the last scan's result — recomputed at most every FOREIGN_SCAN_EVERY ticks.
|
|
@@ -16002,7 +16294,7 @@ function landsAfterRead(eventAt, lastReadAt) {
|
|
|
16002
16294
|
if (!Number.isFinite(e) || !Number.isFinite(r)) return true;
|
|
16003
16295
|
return e > r;
|
|
16004
16296
|
}
|
|
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;
|
|
16297
|
+
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, 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
16298
|
var init_tailer = __esm({
|
|
16007
16299
|
"packages/server/src/tailer.ts"() {
|
|
16008
16300
|
"use strict";
|
|
@@ -16037,6 +16329,7 @@ var init_tailer = __esm({
|
|
|
16037
16329
|
CACHE_FLUSH_MS = 3e4;
|
|
16038
16330
|
PRIME_PROGRESS_EVERY = 20;
|
|
16039
16331
|
DISCOVER_RETRY_MS = 15e3;
|
|
16332
|
+
DISCOVER_RETRY_MAX_MS = 15 * 6e4;
|
|
16040
16333
|
PERM_YES_OPTION = /(^|\n)\s*(❯\s*)?1\.\s+Yes\b/;
|
|
16041
16334
|
PERM_QUESTION = /\b(?:Do you want|Would you like)\b/;
|
|
16042
16335
|
PERM_FOOTER = /\bEsc to (cancel|reject)\b/;
|
|
@@ -16056,7 +16349,7 @@ var init_tailer = __esm({
|
|
|
16056
16349
|
RESUME_REPLAY_SLACK_MS = 6e4;
|
|
16057
16350
|
QUESTION_BLOCK_RE = /^```question(?:[ \t]+[A-Za-z][^\r\n]*?)?[ \t]*\r?\n[\s\S]*?\r?\n```[ \t]*$/gm;
|
|
16058
16351
|
SIGNAL_FENCE_RE = /^```(done|awaiting)[ \t]*\n([\s\S]*?)\n```[ \t]*$/gm;
|
|
16059
|
-
AWAITING_HINT_RE = /^(
|
|
16352
|
+
AWAITING_HINT_RE = /^(shell|agent|timer|pr|for|reason):\s*(\S.*)$/i;
|
|
16060
16353
|
FENCE_BODY_MAX = 500;
|
|
16061
16354
|
HINT_MAX = 8;
|
|
16062
16355
|
HINT_VALUE_MAX = 200;
|
|
@@ -16089,6 +16382,7 @@ var init_tailer = __esm({
|
|
|
16089
16382
|
"permDenies",
|
|
16090
16383
|
"noTranscript",
|
|
16091
16384
|
"nextDiscoverMs",
|
|
16385
|
+
"discoverMisses",
|
|
16092
16386
|
"stallLogged",
|
|
16093
16387
|
"deliveryLedgerSeen",
|
|
16094
16388
|
// The chase bookkeeping is compared against an IN-MEMORY, per-process counter — the ingest's `live`
|
|
@@ -16451,8 +16745,8 @@ function parseCodexSubAgentLine(line, toolNameFor) {
|
|
|
16451
16745
|
if (pt === "function_call" && p.name === "spawn_agent") {
|
|
16452
16746
|
const args = parseToolArguments(p.arguments);
|
|
16453
16747
|
const a = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
16454
|
-
const
|
|
16455
|
-
return { kind: "spawn", at, callId, taskName:
|
|
16748
|
+
const str2 = (v) => typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
16749
|
+
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
16750
|
}
|
|
16457
16751
|
if (pt !== "function_call_output") return void 0;
|
|
16458
16752
|
const name = toolNameFor(callId);
|
|
@@ -18602,7 +18896,7 @@ var require_sender = __commonJS({
|
|
|
18602
18896
|
* @param {Function} [cb] Callback
|
|
18603
18897
|
* @private
|
|
18604
18898
|
*/
|
|
18605
|
-
getBlobData(blob,
|
|
18899
|
+
getBlobData(blob, compress2, options, cb) {
|
|
18606
18900
|
this._bufferedBytes += options[kByteLength];
|
|
18607
18901
|
this._state = GET_BLOB_DATA;
|
|
18608
18902
|
blob.arrayBuffer().then((arrayBuffer) => {
|
|
@@ -18615,12 +18909,12 @@ var require_sender = __commonJS({
|
|
|
18615
18909
|
}
|
|
18616
18910
|
this._bufferedBytes -= options[kByteLength];
|
|
18617
18911
|
const data = toBuffer(arrayBuffer);
|
|
18618
|
-
if (!
|
|
18912
|
+
if (!compress2) {
|
|
18619
18913
|
this._state = DEFAULT;
|
|
18620
18914
|
this.sendFrame(_Sender.frame(data, options), cb);
|
|
18621
18915
|
this.dequeue();
|
|
18622
18916
|
} else {
|
|
18623
|
-
this.dispatch(data,
|
|
18917
|
+
this.dispatch(data, compress2, options, cb);
|
|
18624
18918
|
}
|
|
18625
18919
|
}).catch((err) => {
|
|
18626
18920
|
process.nextTick(onError, this, err, cb);
|
|
@@ -18649,8 +18943,8 @@ var require_sender = __commonJS({
|
|
|
18649
18943
|
* @param {Function} [cb] Callback
|
|
18650
18944
|
* @private
|
|
18651
18945
|
*/
|
|
18652
|
-
dispatch(data,
|
|
18653
|
-
if (!
|
|
18946
|
+
dispatch(data, compress2, options, cb) {
|
|
18947
|
+
if (!compress2) {
|
|
18654
18948
|
this.sendFrame(_Sender.frame(data, options), cb);
|
|
18655
18949
|
return;
|
|
18656
18950
|
}
|
|
@@ -24857,20 +25151,64 @@ function parsePrRef(value) {
|
|
|
24857
25151
|
return { owner: m[1], repo: m[2].replace(/\.git$/, ""), number };
|
|
24858
25152
|
}
|
|
24859
25153
|
function isActionableAwaitingHint(hint) {
|
|
24860
|
-
|
|
24861
|
-
|
|
24862
|
-
|
|
24863
|
-
|
|
25154
|
+
return hint !== void 0 && false;
|
|
25155
|
+
}
|
|
25156
|
+
function readAwaitingPark(hints) {
|
|
25157
|
+
const items = [];
|
|
25158
|
+
let forMs = null;
|
|
25159
|
+
let reason = "";
|
|
25160
|
+
for (const h of hints) {
|
|
25161
|
+
const value = h.value.trim();
|
|
25162
|
+
if (isAwaitingItemKind(h.kind)) {
|
|
25163
|
+
if (value) items.push({ kind: h.kind, value });
|
|
25164
|
+
} else if (h.kind === "for") {
|
|
25165
|
+
forMs = parseAwaitingDuration(value);
|
|
25166
|
+
} else if (h.kind === "reason" && !reason) {
|
|
25167
|
+
reason = value;
|
|
25168
|
+
}
|
|
25169
|
+
}
|
|
25170
|
+
return { items, forMs, reason };
|
|
25171
|
+
}
|
|
25172
|
+
function unaccountedItems(items, live) {
|
|
25173
|
+
return items.filter((i) => !live[LIVE_SET[i.kind]].has(i.value));
|
|
25174
|
+
}
|
|
25175
|
+
function parkIsHonoured(park, live) {
|
|
25176
|
+
if (park.items.length === 0) return false;
|
|
25177
|
+
if (park.forMs === null) return false;
|
|
25178
|
+
return unaccountedItems(park.items, live).length === 0;
|
|
25179
|
+
}
|
|
25180
|
+
function parkExpiresAt(park, fenceAtMs) {
|
|
25181
|
+
if (park.forMs === null || !Number.isFinite(fenceAtMs)) return null;
|
|
25182
|
+
return fenceAtMs + Math.min(park.forMs, AWAITING_FOR_MAX_MS);
|
|
24864
25183
|
}
|
|
24865
25184
|
function awaitingFenceIdentity(hint, fenceAt) {
|
|
24866
25185
|
return createHash11("sha256").update(fenceAt).update("\0").update(hint.kind).update("\0").update(hint.value).digest("hex");
|
|
24867
25186
|
}
|
|
24868
|
-
|
|
25187
|
+
function readGithubStatusBook(raw2) {
|
|
25188
|
+
if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) return {};
|
|
25189
|
+
const out = {};
|
|
25190
|
+
for (const [key, value] of Object.entries(raw2)) {
|
|
25191
|
+
const parsed = GithubWatchStatus.safeParse(value);
|
|
25192
|
+
if (parsed.success) out[key] = parsed.data;
|
|
25193
|
+
}
|
|
25194
|
+
return out;
|
|
25195
|
+
}
|
|
25196
|
+
function githubStatusKey(ref) {
|
|
25197
|
+
return `${ref.owner}/${ref.repo}#${ref.number}`;
|
|
25198
|
+
}
|
|
25199
|
+
var PR_REF_RE, LIVE_SET, GITHUB_STATUS_SETTING;
|
|
24869
25200
|
var init_awaiting = __esm({
|
|
24870
25201
|
"packages/server/src/awaiting.ts"() {
|
|
24871
25202
|
"use strict";
|
|
24872
25203
|
init_src();
|
|
24873
25204
|
PR_REF_RE = /(?:https?:\/\/github\.com\/)?([A-Za-z0-9][\w.-]*)\/([A-Za-z0-9][\w.-]*?)(?:\/pull\/|\/pulls\/|#)(\d+)/;
|
|
25205
|
+
LIVE_SET = {
|
|
25206
|
+
shell: "shells",
|
|
25207
|
+
agent: "agents",
|
|
25208
|
+
timer: "timers",
|
|
25209
|
+
pr: "prs"
|
|
25210
|
+
};
|
|
25211
|
+
GITHUB_STATUS_SETTING = "waker.github.status.v1";
|
|
24874
25212
|
}
|
|
24875
25213
|
});
|
|
24876
25214
|
|
|
@@ -24971,11 +25309,12 @@ triage: it says neither "answer me" nor "this is finished", so it sits in the qu
|
|
|
24971
25309
|
Frizz corrects it, twice, and then stops: rest without a fence and you will be told to pick one, which
|
|
24972
25310
|
costs you a turn each time.
|
|
24973
25311
|
|
|
24974
|
-
Pick \`question\` when you need the human, \`done\` when the work is genuinely finished, and
|
|
24975
|
-
|
|
24976
|
-
|
|
24977
|
-
|
|
24978
|
-
|
|
25312
|
+
Pick \`question\` when you need the human, \`done\` when the work is genuinely finished, and \`awaiting\`
|
|
25313
|
+
when you are WAITING \u2014 on a PR, or on your own background work \u2014 naming what you wait on, one line each.
|
|
25314
|
+
|
|
25315
|
+
\`awaiting\` REGISTERS NOTHING. It is how you come to rest without being bumped for a handoff, and how
|
|
25316
|
+
the human sees what you are waiting on. Your background shells and sub-agents are watched AUTOMATICALLY,
|
|
25317
|
+
fence or no fence: frizz wakes you when one finishes, every time.
|
|
24979
25318
|
|
|
24980
25319
|
**WRITE IT TO BE READ COLD.** The human reads your final message in a queue, hours later, with none of
|
|
24981
25320
|
your context \u2014 and they have NOT seen anything since their own last message. Every prompt you received
|
|
@@ -24986,6 +25325,16 @@ you would do next \u2014 rather than only the last thing you touched. Do not ass
|
|
|
24986
25325
|
Be concise, but do not omit. Use whatever markup makes it readable: headings, bullets, tables, code.
|
|
24987
25326
|
The one place brevity is a RULE is a \`done\` body \u2014 see its entry below.
|
|
24988
25327
|
|
|
25328
|
+
**NAME THE PULL REQUEST, EVERY TIME YOU REST.** If your effort has a PR \u2014 one you opened, one you are
|
|
25329
|
+
reviewing, one you are fixing \u2014 write its number in EVERY resting message, not just the one where it
|
|
25330
|
+
first appeared. The human reads these as a queue of cards from a dozen threads at once, and a card that
|
|
25331
|
+
says "pushed the fix, CI is green" without a number is a card they cannot place: they have to open the
|
|
25332
|
+
thread to learn which PR it was about (maintainer 2026-08-16: "I keep being unclear what PR is being
|
|
25333
|
+
implemented in a given chat"). One \`#123\` in the first line or two costs you nothing and answers it.
|
|
25334
|
+
|
|
25335
|
+
Several PRs in play \u21D2 name them all. No PR yet \u21D2 say what the work is against instead (a branch, an
|
|
25336
|
+
issue, a file) \u2014 the point is that the card identifies its own subject without being opened.
|
|
25337
|
+
|
|
24989
25338
|
Frizz turns \`#123\`, \`owner/repo#123\` and bare commit hashes into github.com links itself, in every
|
|
24990
25339
|
surface it draws your prose, for any project whose origin remote is github.com. Write the bare form;
|
|
24991
25340
|
wrapping one in a \`[link](url)\` yourself is redundant. Everything else you want clickable \u2014 a file, a
|
|
@@ -24995,8 +25344,14 @@ Use at most ONE fenced signal block, at the very END of the final message. The f
|
|
|
24995
25344
|
state; the body is the card the human reads. A \` \`\`\`question \` block and real permission prompts are
|
|
24996
25345
|
higher-priority asks than either fence below.
|
|
24997
25346
|
|
|
25347
|
+
**THE FENCE AND THE PROSE ABOVE IT ARE TWO SURFACES, NOT ONE MESSAGE WRITTEN TWICE.** The card is the
|
|
25348
|
+
LEDGER: one bullet per deliverable, what shipped and where. The prose is only what a ledger cannot hold
|
|
25349
|
+
\u2014 the reasoning, the caveat, the thing the human has to do. If a sentence would read the same in either,
|
|
25350
|
+
it belongs in exactly ONE of them. Writing the summary and then restating it as bullets costs the human
|
|
25351
|
+
a second read for nothing, and it is the commonest thing wrong with an otherwise good handoff.
|
|
25352
|
+
|
|
24998
25353
|
- \` \`\`\`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
|
|
25354
|
+
doc written, or a commissioned research/audit report finished INTO A FILE. Body: ONE TO THREE SENTENCES, then a
|
|
25000
25355
|
BULLET LIST, one \`- \` item per task, each opening with a **bolded verb phrase** and naming what
|
|
25001
25356
|
shipped and where. This is the ONE place the short shape is a rule \u2014 a dismissal card is scanned, not
|
|
25002
25357
|
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 +25366,140 @@ higher-priority asks than either fence below.
|
|
|
25011
25366
|
\`\`\`
|
|
25012
25367
|
|
|
25013
25368
|
**\`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
|
-
|
|
25369
|
+
and anything living only in the conversation goes with it. So the test is never "have I stopped
|
|
25370
|
+
working": it is **WHAT IS LOST IF NOBODY EVER OPENS THIS THREAD AGAIN?** Name one thing and it is not
|
|
25371
|
+
done. If the thread points at future work AT ALL, you are not done \u2014 see **When the work is finished
|
|
25372
|
+
but the thread found more** for the ways out of that, none of which is stretching \`done\`. Uncertain
|
|
25373
|
+
is not done.
|
|
25374
|
+
|
|
25375
|
+
**A RECOMMENDATION IS NOT A CONCLUSION, AND AN UNSENT DRAFT IS NOT A DELIVERABLE.** When the verdict
|
|
25376
|
+
is that SOMEONE SHOULD NOW DO SOMETHING \u2014 merge it, decline it, post this comment, pick one of these
|
|
25377
|
+
two designs, press the button you are not allowed to press \u2014 that someone is the human, so the effort
|
|
25378
|
+
ends on a \` \`\`\`question \` carrying your recommendation as option A. Same for anything you WROTE but
|
|
25379
|
+
did not SEND: a drafted comment, reply, issue body or release note you were right not to publish
|
|
25380
|
+
unilaterally is one click short of delivered, and \`done\` files that draft away with the thread.
|
|
25381
|
+
|
|
25382
|
+
**A \`done\` MESSAGE SAYS WHAT HAPPENED. DELETE EVERY DANGLING "WORTH DOING LATER".** "One thing to
|
|
25383
|
+
carry forward\u2026", "a follow-up could\u2026", "someone should add a changelog line before this ships" \u2014 a
|
|
25384
|
+
forward-reference parked in a dismissal card is clutter: too weak for anyone to act on, too present to
|
|
25385
|
+
ignore, and archived unread. Each one resolves four ways and there is no fifth \u2014 DO it, SPAWN it onto
|
|
25386
|
+
its own card, ASK about it, or DROP it. Something the human must do NOW ("re-pull before you restart")
|
|
25387
|
+
is not a dangling idea; that is the handoff, and it belongs in the prose.
|
|
25388
|
+
|
|
25389
|
+
What blocks \`done\` is work still OWED \u2014 by you, or by the human because of what you found \u2014 not a
|
|
25390
|
+
BACKGROUND PROCESS that happens to still be running: a watcher, dev server, or poller you have already
|
|
25391
|
+
moved on from does not hold it back \u2014 name it in the body and fence anyway. **Read that carve-out
|
|
25392
|
+
narrowly: its subject is a running process, and nothing else.** Follow-up work you DISCOVERED blocks
|
|
25393
|
+
\`done\` just as hard as work you were assigned, even when it is someone else's to do \u2014 "not mine" is
|
|
25394
|
+
not "not owed", because the card takes the finding with it either way. A \`done\` fence REPLACES the
|
|
25395
|
+
awaiting-background card with your own completion card, which is what turns a finished thread with a
|
|
25396
|
+
live watcher into a one-click dismissal instead of a card the human has to keep deciding about. Code
|
|
25397
|
+
written but not LANDED is not done (a commit, a pushed branch, an open PR is work still ahead of the
|
|
25398
|
+
merge \u2014 where the project uses PRs, \`done\` waits for the MERGE, so park the PR on \` \`\`\`awaiting \`
|
|
25399
|
+
with \`pr:\`). An investigation headed for a fix is NOT \`done\`; the fix is still owed. Two cases
|
|
25400
|
+
DO earn \`done\` on something other than landed code, and BOTH earn it the same way \u2014 by passing the
|
|
25401
|
+
test above, because the deliverable is a FILE the human opens without this thread: a commissioned
|
|
25402
|
+
research or audit EFFORT, whose finished report is written to that file, earns \`done\`; and so does a
|
|
25403
|
+
PLANNING session whose plan file is FULLY written and PERSISTED (\`.frizz/plans/<topic>.md\`), because
|
|
25404
|
+
that artifact already lives outside the thread, so dismissing the thread loses nothing. Neither
|
|
25405
|
+
exception stretches to a report that ENDS IN A DECISION the human has yet to make: write the file AND
|
|
25406
|
+
ask the question.
|
|
25407
|
+
|
|
25408
|
+
- \` \`\`\`awaiting \` \u2014 you have STOPPED, and you are waiting on work that is actually running. The
|
|
25409
|
+
fence is PURE STRUCTURE: a list of things frizz can look up, how long the park may stand, and one line
|
|
25410
|
+
of prose. Nothing else belongs in it.
|
|
25050
25411
|
|
|
25051
25412
|
\`\`\`awaiting
|
|
25052
|
-
|
|
25053
|
-
|
|
25413
|
+
shell: bzvtnt3ig
|
|
25414
|
+
agent: <the runtime agent id>
|
|
25415
|
+
timer: tmr_a1b2c3d4e5f6
|
|
25416
|
+
pr: acme/app#391
|
|
25417
|
+
for: 2h
|
|
25418
|
+
---
|
|
25419
|
+
Waiting on the three-platform run before porting the v2 drivers.
|
|
25420
|
+
|
|
25421
|
+
- the macOS leg is the one that has been flaky
|
|
25422
|
+
- if it goes red I will bisect rather than re-run
|
|
25054
25423
|
\`\`\`
|
|
25055
25424
|
|
|
25056
|
-
|
|
25057
|
-
|
|
25425
|
+
**FRONTMATTER, THEN MARKDOWN.** The structural lines come first; a \`---\` line ends them and everything
|
|
25426
|
+
after it is ORDINARY MARKDOWN \u2014 as much as you want, with paragraphs, lists and links. That prose is what
|
|
25427
|
+
the human reads on your resting card, so write it as a handoff, not as a label.
|
|
25428
|
+
|
|
25429
|
+
Omit the \`---\` and the whole fence is structure. A single \`reason: <one line>\` line also still works, but
|
|
25430
|
+
prefer the delimiter: a handoff is prose, and one line is not enough room for one.
|
|
25431
|
+
|
|
25432
|
+
|
|
25433
|
+
- \`shell:\` / \`agent:\` \u2014 the runtime id you were handed when you launched it ("Command running in
|
|
25434
|
+
background with ID: bzvtnt3ig").
|
|
25435
|
+
|
|
25436
|
+
**LOST AN ID? \`mcp__frizz__activity\` PRINTS THEM ALL** \u2014 every shell, sub-agent, timer and PR you have
|
|
25437
|
+
out, each with the exact line your fence needs. Call it rather than guessing: a guessed id names
|
|
25438
|
+
nothing, so frizz refuses the park and bumps you.
|
|
25439
|
+
- \`timer:\` \u2014 a timer id from \`mcp__frizz__timer\`. NOT an instant; the tool creates the timer.
|
|
25440
|
+
- \`pr:\` \u2014 a pull request you REGISTERED with \`mcp__frizz__watch_pr\`, written \`owner/repo#N\`. Register
|
|
25441
|
+
FIRST: this line states the wait, it does not create one. That tool wakes you on CI turning green or red,
|
|
25442
|
+
and on every later review, approval or comment, human or bot, for as long as the PR lives.
|
|
25443
|
+
- \`for:\` \u2014 **REQUIRED**, and a DURATION: \`30s\`, \`15m\`, \`2h\`, \`3d\`. Never an instant. When it runs
|
|
25444
|
+
out frizz brings you back to re-check everything; re-parking is fine and uncapped.
|
|
25445
|
+
- \`reason:\` \u2014 a one-line alternative to the \`---\` block above, kept for fences already written
|
|
25446
|
+
that way. Prefer the delimiter.
|
|
25447
|
+
|
|
25448
|
+
**REGISTERING IS NOT PARKING, AND PARKING IS NOT REGISTERING.** Launching a shell, dispatching a
|
|
25449
|
+
sub-agent, setting a timer, registering a PR watcher \u2014 those just happen, and frizz watches them
|
|
25450
|
+
whether or not you ever write a fence. This fence is only how you declare that you have STOPPED, and
|
|
25451
|
+
name which of them you stopped for.
|
|
25058
25452
|
|
|
25059
|
-
**
|
|
25060
|
-
|
|
25061
|
-
|
|
25453
|
+
**A FENCE THAT NAMES NOTHING IS NOT A PARK.** \`for:\` and \`reason:\` describe a wait; they are not one.
|
|
25454
|
+
If you are waiting on CI, REGISTER the PR and name it \u2014 do not write "waiting on the checks" and a
|
|
25455
|
+
duration, because nothing will wake you and your thread just sits in the queue. And if you are not
|
|
25456
|
+
actually waiting on anything, you are not awaiting: end with \`\`\`done, or ask a \`\`\`question.
|
|
25457
|
+
|
|
25458
|
+
**FRIZZ CHECKS EVERY NAME THE MOMENT YOU REST.** All of them live \u21D2 your thread parks in the dimmed
|
|
25459
|
+
Held band. Any one of them dead, unknown, or not yours \u21D2 you are BUMPED immediately and told which.
|
|
25460
|
+
A fence naming nothing at all is not a park either \u2014 it is a claim to be waiting with no way to be
|
|
25461
|
+
woken, which is the exact failure this grammar exists to make impossible.
|
|
25462
|
+
|
|
25463
|
+
**WAITING ON A PERSON IS A \` \`\`\`question \`.** There is no human gate and there is no prose park.
|
|
25464
|
+
If you need a human, ask them something answerable.
|
|
25465
|
+
|
|
25466
|
+
**CI, RELEASES, DEPLOYS AND MERGE PROGRESSION ARE AUTOMATABLE \u2014 never \` \`\`\`awaiting \` them
|
|
25467
|
+
BLINDLY.** For a PULL REQUEST that is what \`mcp__frizz__watch_pr\` is for: register it and you are told
|
|
25468
|
+
when CI settles, so the wait is real rather than a hope. For anything else, stay ACTIVE \u2014 dispatch a
|
|
25469
|
+
sub-agent to own the wait (its return re-invokes you), or set a timer and name it here.
|
|
25470
|
+
|
|
25471
|
+
**THE PARK SURVIVES A RESTART.** The registration and the fence are both durable, so frizz brings you
|
|
25472
|
+
back (\`{{FRIZZ_RESUME_CMD}}\`) even if its own server or your daemon is replaced while you wait.
|
|
25062
25473
|
|
|
25063
25474
|
**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
|
-
|
|
25475
|
+
is already parked and never rely on the old fence: re-check what you are waiting on, confirm it is
|
|
25476
|
+
still running, and emit a FRESH fence naming it. If it turns out to be automatable, arm the active
|
|
25477
|
+
wait instead and do not fence.
|
|
25067
25478
|
|
|
25068
25479
|
- \` \`\`\`question \` \u2014 you need the human's input; see **Questions for the human**.
|
|
25069
25480
|
|
|
25481
|
+
### When the work is finished but the thread found more
|
|
25482
|
+
|
|
25483
|
+
Your mandate is complete and you turned up something else: a bug you were not sent for, a fix nobody
|
|
25484
|
+
landed, a follow-up the change implies. \`done\` is barred \u2014 the finding dies with the card. \`awaiting\`
|
|
25485
|
+
is barred \u2014 nothing is running. \`question\` looks barred \u2014 the human asked nothing. **That deadlock is
|
|
25486
|
+
not real, and stretching \`done\` is not how you break it.** Take the first exit that fits:
|
|
25487
|
+
|
|
25488
|
+
1. **DO IT** \u2014 small, in scope, yours to make. The default from **The stop criterion**. Then \`done\` is
|
|
25489
|
+
honest with nothing left over.
|
|
25490
|
+
2. **HAND IT OFF TO ITS OWN CARD** \u2014 \`mcp__frizz__spawn_thread\` puts a separable effort on the board as
|
|
25491
|
+
its own thread. Once the follow-up has a card, THIS thread owes nothing and \`done\` is accurate; put
|
|
25492
|
+
the returned \`[title](/thread/<slug>)\` link in the body. Spawn what deserves an effort, not every
|
|
25493
|
+
stray observation \u2014 but an observation that does not deserve a card does not deserve a trailing
|
|
25494
|
+
sentence either. DROP it.
|
|
25495
|
+
3. **ASK** \u2014 the human should choose whether or how it happens. An ordinary \` \`\`\`question \`; "nothing
|
|
25496
|
+
was pending" is not an objection. Make "spawn it as its own thread" an option when it is a candidate.
|
|
25497
|
+
4. **DROP IT** \u2014 it is not worth an effort, a card, or a question. Then it is not worth a SENTENCE
|
|
25498
|
+
either: delete it. Do not park it in the handoff as "one thing to carry forward".
|
|
25499
|
+
|
|
25500
|
+
A bare rest is the residual, not a plan \u2014 legitimate only when nothing above fits, and frizz will ask
|
|
25501
|
+
you twice for a fence before it gives up.
|
|
25502
|
+
|
|
25070
25503
|
A mid-conversation turn carries NO fence. Nor is a turn on a thread that still points at future work \u2014
|
|
25071
25504
|
a live code-change discussion above all \u2014 ever \`done\`.`;
|
|
25072
25505
|
AGENT_COMPLETION = `## Agent completion invariant
|
|
@@ -25165,6 +25598,23 @@ Three specific traps, each of which has ended a turn that should have continued:
|
|
|
25165
25598
|
- **Pre-emptively worrying about context budget.** You do not get to stop early because the turn feels
|
|
25166
25599
|
long. If context genuinely runs out, the harness summarizes and you continue.
|
|
25167
25600
|
|
|
25601
|
+
**AND THE INSTRUCTION IS ALSO THE CEILING.** Everything above pushes you not to stop early; none of it
|
|
25602
|
+
is a licence to grow the job. "Keep going" has no upper bound of its own \u2014 there is always more to do
|
|
25603
|
+
in any repository \u2014 so a worker that may not stop until nothing is left can only ever stop by widening
|
|
25604
|
+
what it was asked. Finish the mandate you were given, and nothing else.
|
|
25605
|
+
|
|
25606
|
+
- **Work you notice on the way is a FINDING, not a task.** The bug beside the one you were sent for,
|
|
25607
|
+
the refactor the code obviously wants, the second issue the first one touches: name each in one line
|
|
25608
|
+
in your handoff and let the human dispatch it. Widening is not thoroughness \u2014 it buries the answer
|
|
25609
|
+
they asked for under changes they now have to review, and it spends hours they did not agree to.
|
|
25610
|
+
- **If the deliverable is a DOCUMENT, the document is the ending.** A triage, a review, an
|
|
25611
|
+
investigation, a recommendation, a plan \u2014 when that is what you were asked for, the finished
|
|
25612
|
+
write-up IS the work. Implementing what it proposes is the NEXT job, and it is not yours unless you
|
|
25613
|
+
were asked.
|
|
25614
|
+
- **An unanswered question is not permission to build the answer.** If you asked and nobody has
|
|
25615
|
+
replied, decide what the task itself needs and carry on INSIDE it. Do not treat the silence as
|
|
25616
|
+
authorization for the work the answer would have unlocked.
|
|
25617
|
+
|
|
25168
25618
|
**Your default is to DECIDE.** A reversible call costs minutes to redo; a round-trip to the human costs
|
|
25169
25619
|
hours \u2014 they may not read your question for hours, and the whole effort sits idle until they do. If it
|
|
25170
25620
|
is derivable from the code, the conventions, or ordinary engineering judgment, it is yours: make it,
|
|
@@ -25173,12 +25623,20 @@ say which way you went, and keep moving.
|
|
|
25173
25623
|
**The test that catches almost every bad question: if you are about to mark one option
|
|
25174
25624
|
\`(recommended)\`, you already know the answer \u2014 so implement it instead of asking.** Writing a
|
|
25175
25625
|
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
|
|
25626
|
+
for "want me to fix it?" on a bug you were dispatched to fix, "should I also
|
|
25177
25627
|
handle X?" where X is obviously in scope, and any question about a name, a default value, a file
|
|
25178
25628
|
location, an error message, a flag spelling, or which of two equivalent designs to use. Those are
|
|
25179
|
-
granular implementation calls.
|
|
25629
|
+
granular implementation calls. (A bug you merely NOTICED is not a question either \u2014 by the ceiling
|
|
25630
|
+
above it is a line in your handoff, not work to adopt and not permission to seek.) Make them, note the
|
|
25631
|
+
notable ones in your handoff, and move on \u2014 a
|
|
25180
25632
|
decision the human can see and reverse in a line of code is not worth an hours-long stall.
|
|
25181
25633
|
|
|
25634
|
+
**That test inverts when knowing the answer and being ABLE TO ACT ON IT come apart.** Where the act is
|
|
25635
|
+
one you are not permitted or not able to perform \u2014 a read-only boundary, a comment that goes out under
|
|
25636
|
+
the human's name, a merge, a close, a publish, a spend \u2014 your recommendation is not a decision you take
|
|
25637
|
+
silently. It becomes the QUESTION, with the recommendation as option A and the act spelled out concretely
|
|
25638
|
+
enough to approve in one word. Never resolve that fork by fencing \` \`\`\`done \` on the investigation.
|
|
25639
|
+
|
|
25182
25640
|
Stop only when a wrong guess would be BOTH costly AND hard to undo:
|
|
25183
25641
|
|
|
25184
25642
|
- destructive or irreversible actions (history rewrite, data loss, force-merge, a published release);
|
|
@@ -25471,8 +25929,9 @@ When delegation is explicitly authorized:
|
|
|
25471
25929
|
Keep automatable waits inside the active turn through the selected persistent \`exec_command\` /
|
|
25472
25930
|
\`write_stdin\` monitor session until it reaches a terminal condition. Then diagnose/fix/retry/merge as
|
|
25473
25931
|
authorized. Do not emit \`awaiting\` for CI,
|
|
25474
|
-
automated review, release, or merge progression. Those tool sessions are process-bound;
|
|
25475
|
-
|
|
25932
|
+
automated review, release, or merge progression. Those tool sessions are process-bound; set a durable
|
|
25933
|
+
timer with \`mcp__frizz__timer\` and name it in your fence only when the next check belongs at a later
|
|
25934
|
+
wall-clock time. A partial
|
|
25476
25935
|
\`gh pr checks\` rollup is not a CI-green verdict: inspect workflow runs for the exact PR head too, and
|
|
25477
25936
|
treat \`ACTION_REQUIRED\` fork gates as pending. When no valid project monitor is declared, run the
|
|
25478
25937
|
bundled \`ci-watch.mjs\` / \`review-watch.mjs\` named above instead of inventing a loop of your own.
|
|
@@ -25497,7 +25956,7 @@ So when a wait is longer than a single yield budget:
|
|
|
25497
25956
|
3. Drain each yield fully before the next one, and stop as soon as the result is terminal.
|
|
25498
25957
|
|
|
25499
25958
|
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
|
|
25959
|
+
switch to a blocking monitor, or set a timer with \`mcp__frizz__timer\` for a genuinely distant check.
|
|
25501
25960
|
|
|
25502
25961
|
When you genuinely need to work alongside a disposable local process (a dev server or long gate), use
|
|
25503
25962
|
the managed unified-exec handoff: create the \`tools.exec_command(...)\` promise, call
|
|
@@ -25512,8 +25971,9 @@ the cell can wake a rested turn.
|
|
|
25512
25971
|
|
|
25513
25972
|
Never implement a recurring wake as a shell \`sleep\` loop. Shell output cannot create a new Codex turn,
|
|
25514
25973
|
so an unattended loop is not a wake mechanism. When a later wall-clock check genuinely belongs after a
|
|
25515
|
-
rest,
|
|
25516
|
-
new turn
|
|
25974
|
+
rest, set one with \`mcp__frizz__timer\` and name the id it returns on a \`timer:\` line in your
|
|
25975
|
+
\`\`\`awaiting fence; Frizz's durable scheduler starts the new turn then. Each timer is one-shot \u2014 set
|
|
25976
|
+
several if you need several, and do not try to make one recur.
|
|
25517
25977
|
|
|
25518
25978
|
## Your model and reasoning effort
|
|
25519
25979
|
|
|
@@ -25538,7 +25998,13 @@ effort that deserves its own card and whose output you do not need.
|
|
|
25538
25998
|
|
|
25539
25999
|
Give it a self-contained \`prompt\` and choose \`model\` + \`effort\` by the new task's complexity (both
|
|
25540
26000
|
required). It returns a \`[title](/thread/<slug>)\` link \u2014 put that in your handoff so the human can open
|
|
25541
|
-
it
|
|
26001
|
+
it.
|
|
26002
|
+
|
|
26003
|
+
**Its most valuable use is the one that unblocks \` \`\`\`done \`.** When your own mandate is complete but
|
|
26004
|
+
you turned up separable follow-up work, that finding is exactly what a \`done\` card would bury. Spawn it,
|
|
26005
|
+
and it lives on its own card instead of inside a thread nobody will reopen \u2014 which is what makes \`done\`
|
|
26006
|
+
on THIS thread honest rather than a stretch. See **When the work is finished but the thread found
|
|
26007
|
+
more**.`;
|
|
25542
26008
|
THREAD_EXECUTION = {
|
|
25543
26009
|
claude: `## Thread types
|
|
25544
26010
|
|
|
@@ -26967,17 +27433,6 @@ function buildClaudeResumeCommand(opts) {
|
|
|
26967
27433
|
if (opts.message) argv.push(opts.message);
|
|
26968
27434
|
return argv;
|
|
26969
27435
|
}
|
|
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
27436
|
function createDispatcher(deps) {
|
|
26982
27437
|
const readBoardSource = deps.readBoard ?? readBoard;
|
|
26983
27438
|
const frizzDir = join23(deps.project.dir, ".frizz");
|
|
@@ -27063,7 +27518,6 @@ function createDispatcher(deps) {
|
|
|
27063
27518
|
effort: effort ?? null,
|
|
27064
27519
|
permission_mode: permissionMode
|
|
27065
27520
|
});
|
|
27066
|
-
armDefaultGoal(deps.storage, slug);
|
|
27067
27521
|
deps.storage.setBackend(slug, "codex");
|
|
27068
27522
|
deps.storage.setAgentSession(slug, spawned.binding.codexSessionId);
|
|
27069
27523
|
deps.storage.setCodexRuntime(slug, "app-server");
|
|
@@ -27124,7 +27578,6 @@ function createDispatcher(deps) {
|
|
|
27124
27578
|
effort: effort ?? null,
|
|
27125
27579
|
permission_mode: permissionMode
|
|
27126
27580
|
});
|
|
27127
|
-
armDefaultGoal(deps.storage, slug);
|
|
27128
27581
|
deps.storage.setBackend(slug, "claude");
|
|
27129
27582
|
deps.storage.setClaudeRuntime(slug, "broker");
|
|
27130
27583
|
void deps.board.rebuild().catch(() => {
|
|
@@ -27501,6 +27954,7 @@ var init_plan_files = __esm({
|
|
|
27501
27954
|
import {
|
|
27502
27955
|
watch as fsWatch
|
|
27503
27956
|
} from "node:fs";
|
|
27957
|
+
import { homedir as homedir12 } from "node:os";
|
|
27504
27958
|
import { join as join25 } from "node:path";
|
|
27505
27959
|
import watcher from "@parcel/watcher";
|
|
27506
27960
|
function appServerTurnStalled(liveness, lastActivityAt, nowMs) {
|
|
@@ -27564,6 +28018,53 @@ function stampStoppableShells(shells, row) {
|
|
|
27564
28018
|
if (isHeadlessRow(row)) return shells;
|
|
27565
28019
|
return shells.map((shell) => shell.stoppable ? { ...shell, stoppable: false } : shell);
|
|
27566
28020
|
}
|
|
28021
|
+
function declaredWaitIds(tele) {
|
|
28022
|
+
if (tele?.lastFence?.kind !== "awaiting") return [];
|
|
28023
|
+
return readAwaitingPark(tele.lastFence.hints).items.filter((i) => i.kind === "shell" || i.kind === "agent").map((i) => i.value);
|
|
28024
|
+
}
|
|
28025
|
+
function liveWaitHandles(tele) {
|
|
28026
|
+
const handles = /* @__PURE__ */ new Set();
|
|
28027
|
+
for (const shell of tele?.bgShells ?? []) {
|
|
28028
|
+
if (shell.state !== "running") continue;
|
|
28029
|
+
if (shell.id) handles.add(shell.id);
|
|
28030
|
+
if (shell.taskId) handles.add(shell.taskId);
|
|
28031
|
+
if (shell.label) handles.add(shell.label);
|
|
28032
|
+
}
|
|
28033
|
+
for (const agent of tele?.subAgents ?? []) {
|
|
28034
|
+
if (!isDirectSubAgent(agent) || agent.state !== "running") continue;
|
|
28035
|
+
handles.add(agent.id);
|
|
28036
|
+
if (agent.label) handles.add(agent.label);
|
|
28037
|
+
}
|
|
28038
|
+
return handles;
|
|
28039
|
+
}
|
|
28040
|
+
function hasDeclaredBackgroundPark(tele, nowMs) {
|
|
28041
|
+
const named = declaredWaitIds(tele);
|
|
28042
|
+
if (named.length === 0) return false;
|
|
28043
|
+
const live = liveWaitHandles(tele);
|
|
28044
|
+
if (!named.every((id) => live.has(id))) return false;
|
|
28045
|
+
const restedMs = Date.parse(tele?.lastAssistantAt ?? "");
|
|
28046
|
+
if (Number.isFinite(restedMs) && nowMs - restedMs > DECLARED_PARK_MAX_MS) return false;
|
|
28047
|
+
return true;
|
|
28048
|
+
}
|
|
28049
|
+
function hasDeclaredWait(tele, nowMs) {
|
|
28050
|
+
if (hasDeclaredBackgroundPark(tele, nowMs)) return true;
|
|
28051
|
+
if (tele?.lastFence?.kind === "awaiting" && hasParkedPrWatch(tele)) return true;
|
|
28052
|
+
return (tele?.subAgents ?? []).some((a) => isDirectSubAgent(a) && a.state === "running");
|
|
28053
|
+
}
|
|
28054
|
+
function heldByRunningChecks(tele, github, registered) {
|
|
28055
|
+
if (tele?.lastFence?.kind !== "awaiting") return false;
|
|
28056
|
+
const refs = [];
|
|
28057
|
+
for (const hint of tele.lastFence.hints) {
|
|
28058
|
+
if (hint.kind !== "pr") continue;
|
|
28059
|
+
const ref = parsePrRef(hint.value);
|
|
28060
|
+
if (ref && registered.has(githubStatusKey(ref))) refs.push(githubStatusKey(ref));
|
|
28061
|
+
}
|
|
28062
|
+
if (refs.length === 0) return false;
|
|
28063
|
+
return refs.every((key) => {
|
|
28064
|
+
const status = github[key];
|
|
28065
|
+
return status?.checks === "running" && status.state === "open";
|
|
28066
|
+
});
|
|
28067
|
+
}
|
|
27567
28068
|
function hasLiveOwnWork(tele) {
|
|
27568
28069
|
return Boolean(
|
|
27569
28070
|
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 +28078,47 @@ function hasLiveOwnWork(tele) {
|
|
|
27577
28078
|
}
|
|
27578
28079
|
function hasParkedPrWatch(tele) {
|
|
27579
28080
|
if (tele?.lastFence?.kind !== "awaiting") return false;
|
|
27580
|
-
return tele.lastFence.hints.some((hint) => hint.kind === "pr
|
|
28081
|
+
return tele.lastFence.hints.some((hint) => hint.kind === "pr" && parsePrRef(hint.value) !== void 0);
|
|
27581
28082
|
}
|
|
27582
28083
|
function bgSnoozeArmed(row) {
|
|
27583
28084
|
return row.bg_snooze_rested_at != null && row.rested_at != null && row.bg_snooze_rested_at === row.rested_at;
|
|
27584
28085
|
}
|
|
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
|
-
);
|
|
28086
|
+
function hasParkedExternalWait(tele, _nowMs) {
|
|
28087
|
+
return false;
|
|
27590
28088
|
}
|
|
27591
|
-
function
|
|
27592
|
-
if (tele?.lastFence?.kind !== "awaiting") return [];
|
|
28089
|
+
function fenceWatchViews(slug, tele, fenceAt, github = {}, registered = []) {
|
|
27593
28090
|
const seen = /* @__PURE__ */ new Set();
|
|
27594
28091
|
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);
|
|
28092
|
+
for (const w of registered) {
|
|
28093
|
+
if (seen.has(`github:${w.target}`)) continue;
|
|
28094
|
+
seen.add(`github:${w.target}`);
|
|
27602
28095
|
out.push({
|
|
27603
|
-
id: `github:${slug}:${target}`,
|
|
28096
|
+
id: `github:${slug}:${w.target}`,
|
|
27604
28097
|
kind: "github",
|
|
27605
|
-
target,
|
|
28098
|
+
target: w.target,
|
|
27606
28099
|
state: "armed",
|
|
27607
|
-
|
|
27608
|
-
//
|
|
27609
|
-
//
|
|
27610
|
-
|
|
28100
|
+
createdAt: w.createdAt,
|
|
28101
|
+
// Absent until the poller's first successful read. The card says "checking…" then, rather than
|
|
28102
|
+
// inventing a verdict — an unpolled PR and a PR with no CI are different facts.
|
|
28103
|
+
...github[w.target] ? { github: github[w.target] } : {}
|
|
27611
28104
|
});
|
|
27612
28105
|
}
|
|
28106
|
+
if (tele?.lastFence?.kind !== "awaiting") return out;
|
|
28107
|
+
const createdAt = fenceAt ?? tele.lastAssistantAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
28108
|
+
for (const hint of tele.lastFence.hints) {
|
|
28109
|
+
if (hint.kind !== "shell" && hint.kind !== "agent") continue;
|
|
28110
|
+
const target = hint.value.trim();
|
|
28111
|
+
if (!target || seen.has(`shell:${target}`) || !liveWaitHandles(tele).has(target)) continue;
|
|
28112
|
+
seen.add(`shell:${target}`);
|
|
28113
|
+
out.push({ id: `shell:${slug}:${target}`, kind: "shell", target, state: "armed", createdAt });
|
|
28114
|
+
}
|
|
27613
28115
|
return out;
|
|
27614
28116
|
}
|
|
27615
28117
|
function hasFreshDelivery(row, processGone) {
|
|
27616
28118
|
if (processGone) return false;
|
|
27617
28119
|
return parseDeliveryLedger(row.delivery_ledger).some((d) => d.state === "pending" || d.state === "enqueued");
|
|
27618
28120
|
}
|
|
27619
|
-
function deriveNeedsYou(row, tele, runtime, hasActionableInteraction = false, nowMs = Date.now(), limitPause = void 0, excuseLiveOwnWork = true, deliveryProcessGone = false) {
|
|
28121
|
+
function deriveNeedsYou(row, tele, runtime, hasActionableInteraction = false, nowMs = Date.now(), limitPause = void 0, excuseLiveOwnWork = true, deliveryProcessGone = false, github = {}, registeredPrWatches = /* @__PURE__ */ new Set()) {
|
|
27620
28122
|
if (futureSnooze(row, nowMs)) return false;
|
|
27621
28123
|
if (hasActionableInteraction) return true;
|
|
27622
28124
|
if (runtime === "perm-prompt") return true;
|
|
@@ -27630,15 +28132,17 @@ function deriveNeedsYou(row, tele, runtime, hasActionableInteraction = false, no
|
|
|
27630
28132
|
if (hasParkedExternalWait(tele, nowMs)) return false;
|
|
27631
28133
|
if (limitPause?.autoResume) return false;
|
|
27632
28134
|
if (excuseLiveOwnWork && runtime !== "exited" && hasLiveBackgroundWork(tele) && !tele?.lastFence) return false;
|
|
28135
|
+
if (excuseLiveOwnWork && runtime !== "exited" && hasDeclaredBackgroundPark(tele, nowMs)) return false;
|
|
28136
|
+
if (excuseLiveOwnWork && runtime !== "exited" && heldByRunningChecks(tele, github, registeredPrWatches)) return false;
|
|
27633
28137
|
if (runtime !== "exited" && hasLiveOwnWork(tele) && tele?.lastFence?.kind !== "done") return !bgSnoozeArmed(row);
|
|
27634
28138
|
if (tele?.lastFence?.kind === "done") return true;
|
|
27635
28139
|
return true;
|
|
27636
28140
|
}
|
|
27637
|
-
function deriveAwaitingBackground(row, tele, runtime, hasActionableInteraction = false, nowMs = Date.now(), limitPause = void 0, deliveryProcessGone = false) {
|
|
27638
|
-
if (runtime !== "turn-idle" || !
|
|
28141
|
+
function deriveAwaitingBackground(row, tele, runtime, hasActionableInteraction = false, nowMs = Date.now(), limitPause = void 0, deliveryProcessGone = false, github = {}, registeredPrWatches = /* @__PURE__ */ new Set()) {
|
|
28142
|
+
if (runtime !== "turn-idle" || !hasDeclaredWait(tele, nowMs)) return false;
|
|
27639
28143
|
if (hasActionableInteraction || tele?.pendingAsk || tele?.nativeInputRequired || tele?.pendingQuestion) return false;
|
|
27640
28144
|
if (tele?.lastFence?.kind === "done") return false;
|
|
27641
|
-
if (tele?.lastFence?.kind === "awaiting" && !hasParkedPrWatch(tele)) return false;
|
|
28145
|
+
if (tele?.lastFence?.kind === "awaiting" && !hasParkedPrWatch(tele) && !hasDeclaredBackgroundPark(tele, nowMs)) return false;
|
|
27642
28146
|
return deriveNeedsYou({ ...row, bg_snooze_rested_at: null }, tele, runtime, hasActionableInteraction, nowMs, limitPause, false, deliveryProcessGone);
|
|
27643
28147
|
}
|
|
27644
28148
|
function resolveSessionProfile(row, tele) {
|
|
@@ -27683,7 +28187,6 @@ function resolveRecurringPrompt(row) {
|
|
|
27683
28187
|
stopHook: row.recurring_on_rest === 1,
|
|
27684
28188
|
heartbeat: row.recurring_on_schedule === 1,
|
|
27685
28189
|
postCompaction: row.recurring_on_compact === 1,
|
|
27686
|
-
pauseOnQuestions: row.recurring_pause_on_questions === 1,
|
|
27687
28190
|
// Carried whenever a cadence has ever been chosen, INCLUDING while the heartbeat is off — the
|
|
27688
28191
|
// minutes field has to read back what switching it on again would use.
|
|
27689
28192
|
intervalSeconds: row.recurring_interval_ms ? Math.round(row.recurring_interval_ms / 1e3) : void 0,
|
|
@@ -27717,7 +28220,12 @@ function resolveSessionTitle(row, tele) {
|
|
|
27717
28220
|
aiTitle: locked ? void 0 : tele?.aiTitle ?? persisted
|
|
27718
28221
|
};
|
|
27719
28222
|
}
|
|
27720
|
-
function sessionThreadView(projectDir, storage, row, tele, registeredLegacyTerminal, interactionPresence, nowMs, codexTurnLiveness, claudeBrokerDaemonAlive) {
|
|
28223
|
+
function sessionThreadView(projectDir, storage, row, tele, registeredLegacyTerminal, interactionPresence, nowMs, codexTurnLiveness, claudeBrokerDaemonAlive, github = {}) {
|
|
28224
|
+
const armedPrWatches = storage.listPrWatches(row.slug, { armedOnly: true }).map((w) => ({
|
|
28225
|
+
target: `${w.owner}/${w.repo}#${w.number}`,
|
|
28226
|
+
createdAt: new Date(w.created_at).toISOString()
|
|
28227
|
+
}));
|
|
28228
|
+
const registeredPrWatches = new Set(armedPrWatches.map((w) => w.target));
|
|
27721
28229
|
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
28230
|
const headlessLostWork = isBrokerClaudeRow(row) && headlessStalled && hasUnretiredOwnAgents(tele);
|
|
27723
28231
|
const deliveryProcessGone = isBrokerClaudeRow(row) && headlessStalled;
|
|
@@ -27728,8 +28236,8 @@ function sessionThreadView(projectDir, storage, row, tele, registeredLegacyTermi
|
|
|
27728
28236
|
const state = effectiveSessionState(row, registeredLegacyTerminal);
|
|
27729
28237
|
const archived = state === "archived";
|
|
27730
28238
|
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);
|
|
28239
|
+
const needsYou = archived ? false : deriveNeedsYou(row, tele, runtime, interactionPresence.needsUser, nowMs, limitPause, true, deliveryProcessGone, github, registeredPrWatches);
|
|
28240
|
+
const awaitingBackground = archived ? false : deriveAwaitingBackground(row, tele, runtime, interactionPresence.needsUser, nowMs, limitPause, deliveryProcessGone, github, registeredPrWatches);
|
|
27733
28241
|
const crashed = runtime === "exited" && (tele?.turn === "in-flight" || hasLiveBackgroundWork(tele) || headlessLostWork);
|
|
27734
28242
|
const snoozedUntil = futureSnooze(row, nowMs);
|
|
27735
28243
|
const profile = resolveSessionProfile(row, tele);
|
|
@@ -27761,20 +28269,11 @@ function sessionThreadView(projectDir, storage, row, tele, registeredLegacyTermi
|
|
|
27761
28269
|
lastAssistantAt: tele?.lastAssistantAt,
|
|
27762
28270
|
subAgents: stampStoppable(tele?.subAgents ?? [], row),
|
|
27763
28271
|
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
|
-
],
|
|
28272
|
+
// ONE SOURCE: the FENCE. Both kinds are derived from what the worker wrote — `pr-watch:` lines
|
|
28273
|
+
// become the github rows, `watch:` lines the shell rows — so this strip lists exactly what will
|
|
28274
|
+
// actually wake the thread, and the two cannot drift into claiming different things. There is no
|
|
28275
|
+
// registry behind either any more (`thread_watch`, retired 2026-08-14).
|
|
28276
|
+
watches: fenceWatchViews(row.slug, tele, tele?.lastAssistantAt, github, armedPrWatches),
|
|
27778
28277
|
pendingAsk: tele?.pendingAsk ? { questions: tele.pendingAsk.questions } : void 0,
|
|
27779
28278
|
nativeInputRequired: tele?.nativeInputRequired,
|
|
27780
28279
|
pendingQuestion: tele?.pendingQuestion ?? false,
|
|
@@ -27795,6 +28294,10 @@ function sessionThreadView(projectDir, storage, row, tele, registeredLegacyTermi
|
|
|
27795
28294
|
// Only meaningful while the snooze is still pending — a prompt without a live deadline is an
|
|
27796
28295
|
// already-delivered (or superseded) bump the row has not been swept clean of yet.
|
|
27797
28296
|
snoozePrompt: snoozedUntil ? row.snooze_prompt ?? void 0 : void 0,
|
|
28297
|
+
// The event-snooze, as a fact the CHAT can read. `awaitingBackground` still ignores it — that flag
|
|
28298
|
+
// states whether the thread is waiting, which the snooze does not change — so the suppression is a
|
|
28299
|
+
// presentation rule the client applies, not a second opinion about the thread's state.
|
|
28300
|
+
bgSnoozed: bgSnoozeArmed(row) || void 0,
|
|
27798
28301
|
claudeRuntime: row.claude_runtime === "broker" ? "broker" : row.claude_runtime === "tmux" ? "tmux" : void 0,
|
|
27799
28302
|
// The recurring prompt — the same projection the worker's own `action: "get"` reads back.
|
|
27800
28303
|
recurringPrompt: resolveRecurringPrompt(row),
|
|
@@ -27881,6 +28384,7 @@ function createBoard(project, storage, bus, tailer, bootId, deps = {}) {
|
|
|
27881
28384
|
let plansCache = [];
|
|
27882
28385
|
function buildSessionThreads(nowMs) {
|
|
27883
28386
|
const rows = storage.allSessions().filter((row) => ThreadSlug.safeParse(row.slug).success);
|
|
28387
|
+
const github = readGithubStatusBook(storage.getSetting(GITHUB_STATUS_SETTING));
|
|
27884
28388
|
const currentInteractionKeys = /* @__PURE__ */ new Set();
|
|
27885
28389
|
const out = [];
|
|
27886
28390
|
for (const row of rows) {
|
|
@@ -27916,7 +28420,8 @@ function createBoard(project, storage, bus, tailer, bootId, deps = {}) {
|
|
|
27916
28420
|
interactionPresence,
|
|
27917
28421
|
nowMs,
|
|
27918
28422
|
codexTurnLiveness,
|
|
27919
|
-
claudeBrokerDaemonAlive
|
|
28423
|
+
claudeBrokerDaemonAlive,
|
|
28424
|
+
github
|
|
27920
28425
|
));
|
|
27921
28426
|
}
|
|
27922
28427
|
for (const key of pendingInteractionCache.keys()) {
|
|
@@ -27932,7 +28437,9 @@ function createBoard(project, storage, bus, tailer, bootId, deps = {}) {
|
|
|
27932
28437
|
projectName: project.name,
|
|
27933
28438
|
projectLabel: project.label,
|
|
27934
28439
|
...project.githubRepo ? { githubRepo: project.githubRepo } : {},
|
|
27935
|
-
projectSlug: findByPath(project.dir)?.slug
|
|
28440
|
+
projectSlug: findByPath(project.dir)?.slug,
|
|
28441
|
+
// So the client can expand a `~` a worker wrote in prose (see BoardSnapshot.homeDir).
|
|
28442
|
+
homeDir: homedir12()
|
|
27936
28443
|
};
|
|
27937
28444
|
const sessionThreads = buildSessionThreads(assembledAtMs);
|
|
27938
28445
|
armSnoozeWake(sessionThreads, assembledAtMs);
|
|
@@ -28113,7 +28620,7 @@ function createBoard(project, storage, bus, tailer, bootId, deps = {}) {
|
|
|
28113
28620
|
}
|
|
28114
28621
|
};
|
|
28115
28622
|
}
|
|
28116
|
-
var DEBOUNCE_MS, RECONCILE_MS, STALL_GRACE_MS, LINE_CAP;
|
|
28623
|
+
var DEBOUNCE_MS, RECONCILE_MS, STALL_GRACE_MS, LINE_CAP, DECLARED_PARK_MAX_MS;
|
|
28117
28624
|
var init_board = __esm({
|
|
28118
28625
|
"packages/server/src/board.ts"() {
|
|
28119
28626
|
"use strict";
|
|
@@ -28134,6 +28641,7 @@ var init_board = __esm({
|
|
|
28134
28641
|
RECONCILE_MS = 15e3;
|
|
28135
28642
|
STALL_GRACE_MS = 3e4;
|
|
28136
28643
|
LINE_CAP = 240;
|
|
28644
|
+
DECLARED_PARK_MAX_MS = 24 * 60 * 60 * 1e3;
|
|
28137
28645
|
}
|
|
28138
28646
|
});
|
|
28139
28647
|
|
|
@@ -28641,6 +29149,17 @@ var init_github_review = __esm({
|
|
|
28641
29149
|
import { execFile as execFile6 } from "node:child_process";
|
|
28642
29150
|
import { promisify as promisify6 } from "node:util";
|
|
28643
29151
|
import { createHash as createHash14, randomUUID as randomUUID14 } from "node:crypto";
|
|
29152
|
+
function failureSignature(rollup) {
|
|
29153
|
+
const ids = [];
|
|
29154
|
+
for (const c of Array.isArray(rollup) ? rollup : []) {
|
|
29155
|
+
if (!c || typeof c !== "object") continue;
|
|
29156
|
+
if (!rollupEntryFailed(typeof c.conclusion === "string" ? c.conclusion : void 0, typeof c.state === "string" ? c.state : void 0)) continue;
|
|
29157
|
+
const label = c.name ?? c.context ?? c.workflowName ?? "?";
|
|
29158
|
+
ids.push(`${label}@${c.detailsUrl ?? c.targetUrl ?? c.completedAt ?? ""}`);
|
|
29159
|
+
}
|
|
29160
|
+
if (ids.length === 0) return "";
|
|
29161
|
+
return createHash14("sha1").update(ids.sort().join("\n")).digest("hex").slice(0, 12);
|
|
29162
|
+
}
|
|
28644
29163
|
function parsePrRef2(value) {
|
|
28645
29164
|
const m = value.trim().match(PR_REF_RE2);
|
|
28646
29165
|
if (!m) return void 0;
|
|
@@ -28652,7 +29171,7 @@ function refKey2(r) {
|
|
|
28652
29171
|
return `${r.owner}/${r.repo}#${r.number}`;
|
|
28653
29172
|
}
|
|
28654
29173
|
function ghPrViewArgs(ref) {
|
|
28655
|
-
return ["pr", "view", String(ref.number), "--repo", `${ref.owner}/${ref.repo}`, "--json", "state,mergedAt,statusCheckRollup,headRefOid"];
|
|
29174
|
+
return ["pr", "view", String(ref.number), "--repo", `${ref.owner}/${ref.repo}`, "--json", "state,mergedAt,statusCheckRollup,headRefOid,mergeable,reviewDecision"];
|
|
28656
29175
|
}
|
|
28657
29176
|
function latestWorkflowRuns(runs) {
|
|
28658
29177
|
const latest = /* @__PURE__ */ new Map();
|
|
@@ -28708,12 +29227,40 @@ function failedCheckNames(rollup, runs = [], cap = 8) {
|
|
|
28708
29227
|
}
|
|
28709
29228
|
return { names: names.slice(0, cap), omitted: Math.max(0, names.length - cap) };
|
|
28710
29229
|
}
|
|
28711
|
-
function
|
|
28712
|
-
|
|
29230
|
+
function githubWatchStatus(pr, polledAt) {
|
|
29231
|
+
const entries = Array.isArray(pr.rollup) ? pr.rollup.filter((c) => c && typeof c === "object") : [];
|
|
29232
|
+
let running = 0;
|
|
29233
|
+
let passed = 0;
|
|
29234
|
+
let failed = 0;
|
|
29235
|
+
for (const c of entries) {
|
|
29236
|
+
const status = typeof c.status === "string" ? c.status : void 0;
|
|
29237
|
+
const state2 = typeof c.state === "string" ? c.state : void 0;
|
|
29238
|
+
const conclusion = typeof c.conclusion === "string" ? c.conclusion : void 0;
|
|
29239
|
+
const terminal = status ? status === "COMPLETED" : state2 ? state2 !== "PENDING" && state2 !== "EXPECTED" : false;
|
|
29240
|
+
if (!terminal) running++;
|
|
29241
|
+
else if (rollupEntryFailed(conclusion, state2)) failed++;
|
|
29242
|
+
else passed++;
|
|
29243
|
+
}
|
|
29244
|
+
const state = pr.state === "MERGED" ? "merged" : pr.state === "CLOSED" ? "closed" : "open";
|
|
29245
|
+
const checks = entries.length === 0 ? "none" : failed > 0 ? "failing" : running > 0 ? "running" : "passing";
|
|
29246
|
+
const merge = pr.mergeable === "CONFLICTING" ? "conflicting" : pr.mergeable !== "MERGEABLE" ? "unknown" : checks === "failing" || pr.reviewDecision === "CHANGES_REQUESTED" || pr.reviewDecision === "REVIEW_REQUIRED" ? "blocked" : "mergeable";
|
|
29247
|
+
return {
|
|
29248
|
+
checks,
|
|
29249
|
+
running,
|
|
29250
|
+
passed,
|
|
29251
|
+
failed,
|
|
29252
|
+
failing: failedCheckNames(entries, pr.workflowRuns ?? []).names,
|
|
29253
|
+
merge,
|
|
29254
|
+
state,
|
|
29255
|
+
polledAt,
|
|
29256
|
+
head: pr.head,
|
|
29257
|
+
failureSig: failureSignature(entries)
|
|
29258
|
+
};
|
|
29259
|
+
}
|
|
29260
|
+
function isPrWatchHint(_kind) {
|
|
29261
|
+
return false;
|
|
28713
29262
|
}
|
|
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;
|
|
29263
|
+
function isActionable(_hint) {
|
|
28717
29264
|
return false;
|
|
28718
29265
|
}
|
|
28719
29266
|
function fenceIdentity(hints, fenceAt) {
|
|
@@ -28763,20 +29310,28 @@ function isHeartbeatFenceId(fenceId) {
|
|
|
28763
29310
|
function saidDone(tele) {
|
|
28764
29311
|
return tele.lastFence?.kind === "done" || tele.lastAssistantAllDone === true;
|
|
28765
29312
|
}
|
|
28766
|
-
function
|
|
28767
|
-
return
|
|
29313
|
+
function armedTimerIdsOf(storage, slug) {
|
|
29314
|
+
return new Set(storage.listThreadTimers(slug, { armedOnly: true }).map((t) => t.id));
|
|
28768
29315
|
}
|
|
28769
|
-
function
|
|
28770
|
-
|
|
28771
|
-
|
|
28772
|
-
return fence.hints.some((h) => isActionable(h) || h.kind === "human");
|
|
29316
|
+
function restMessageIsSignedOff(tele, _registeredPrWatches = /* @__PURE__ */ new Set(), _armedTimerIds = /* @__PURE__ */ new Set()) {
|
|
29317
|
+
if (tele.lastFence?.kind === "awaiting") return true;
|
|
29318
|
+
return saidDone(tele);
|
|
28773
29319
|
}
|
|
28774
|
-
function
|
|
28775
|
-
|
|
29320
|
+
function liveActivityOf(tele, registeredPrWatches, armedTimerIds) {
|
|
29321
|
+
const shells = /* @__PURE__ */ new Set();
|
|
29322
|
+
for (const sh of tele.bgShells ?? []) {
|
|
29323
|
+
if (sh.state !== "running") continue;
|
|
29324
|
+
for (const h of [sh.taskId, sh.id, sh.label]) if (h) shells.add(h);
|
|
29325
|
+
}
|
|
29326
|
+
const agents = /* @__PURE__ */ new Set();
|
|
29327
|
+
for (const a of tele.subAgents ?? []) {
|
|
29328
|
+
if (a.state !== "running") continue;
|
|
29329
|
+
for (const h of [a.id, a.label]) if (h) agents.add(h);
|
|
29330
|
+
}
|
|
29331
|
+
return { shells, agents, timers: armedTimerIds, prs: registeredPrWatches };
|
|
28776
29332
|
}
|
|
28777
|
-
function
|
|
28778
|
-
|
|
28779
|
-
return tele !== void 0 && blockedOnHuman(tele);
|
|
29333
|
+
function registeredPrWatchesOf(storage, slug) {
|
|
29334
|
+
return new Set(storage.listPrWatches(slug, { armedOnly: true }).map((w) => `${w.owner}/${w.repo}#${w.number}`));
|
|
28780
29335
|
}
|
|
28781
29336
|
function armedSchedule(row) {
|
|
28782
29337
|
const prompt = row.recurring_prompt?.trim();
|
|
@@ -28814,21 +29369,24 @@ function armedCompact(row) {
|
|
|
28814
29369
|
if (row.recurring_on_compact !== 1) return void 0;
|
|
28815
29370
|
return { prompt, armedAt };
|
|
28816
29371
|
}
|
|
29372
|
+
function shellFenceId(toolUseId) {
|
|
29373
|
+
return `${SHELL_FENCE_PREFIX}:${toolUseId}`;
|
|
29374
|
+
}
|
|
29375
|
+
function isShellFenceId(fenceId) {
|
|
29376
|
+
return fenceId.startsWith(`${SHELL_FENCE_PREFIX}:`);
|
|
29377
|
+
}
|
|
29378
|
+
function prWatchFenceId(watchId, report) {
|
|
29379
|
+
return `${PR_WATCH_FENCE_PREFIX}:${watchId}:${report}`;
|
|
29380
|
+
}
|
|
29381
|
+
function isPrWatchFenceId(fenceId) {
|
|
29382
|
+
return fenceId.startsWith(`${PR_WATCH_FENCE_PREFIX}:`);
|
|
29383
|
+
}
|
|
28817
29384
|
function signoffFenceId(restedAt) {
|
|
28818
29385
|
return `${SIGNOFF_FENCE_PREFIX}:${restedAt}`;
|
|
28819
29386
|
}
|
|
28820
29387
|
function isSignoffFenceId(fenceId) {
|
|
28821
29388
|
return fenceId.startsWith(`${SIGNOFF_FENCE_PREFIX}:`);
|
|
28822
29389
|
}
|
|
28823
|
-
function watchFenceId(watchId) {
|
|
28824
|
-
return `${WATCH_FENCE_PREFIX}:${watchId}`;
|
|
28825
|
-
}
|
|
28826
|
-
function isWatchFenceId(fenceId) {
|
|
28827
|
-
return fenceId.startsWith(`${WATCH_FENCE_PREFIX}:`);
|
|
28828
|
-
}
|
|
28829
|
-
function watchIdOf(fenceId) {
|
|
28830
|
-
return fenceId.slice(WATCH_FENCE_PREFIX.length + 1);
|
|
28831
|
-
}
|
|
28832
29390
|
function timerFenceId(timerId) {
|
|
28833
29391
|
return `${TIMER_FENCE_PREFIX}:${timerId}`;
|
|
28834
29392
|
}
|
|
@@ -28839,12 +29397,6 @@ function timerIdOf(fenceId) {
|
|
|
28839
29397
|
return fenceId.slice(TIMER_FENCE_PREFIX.length + 1);
|
|
28840
29398
|
}
|
|
28841
29399
|
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
29400
|
if (isPrWatchHint(hint.kind)) return void 0;
|
|
28849
29401
|
const ref = parsePrRef2(hint.value);
|
|
28850
29402
|
if (!ref) return void 0;
|
|
@@ -28895,6 +29447,9 @@ async function defaultFetchPr(ref) {
|
|
|
28895
29447
|
state: j.state,
|
|
28896
29448
|
mergedAt: typeof j.mergedAt === "string" ? j.mergedAt : null,
|
|
28897
29449
|
rollup: Array.isArray(j.statusCheckRollup) ? j.statusCheckRollup : [],
|
|
29450
|
+
head: j.headRefOid,
|
|
29451
|
+
mergeable: typeof j.mergeable === "string" ? j.mergeable : void 0,
|
|
29452
|
+
reviewDecision: typeof j.reviewDecision === "string" ? j.reviewDecision : void 0,
|
|
28898
29453
|
workflowRuns
|
|
28899
29454
|
};
|
|
28900
29455
|
} catch {
|
|
@@ -28937,6 +29492,14 @@ function createScheduler(deps) {
|
|
|
28937
29492
|
let timer = null;
|
|
28938
29493
|
let activeTick = null;
|
|
28939
29494
|
let stopped = false;
|
|
29495
|
+
function publishGithubStatus(key, pr, nowMs) {
|
|
29496
|
+
const raw2 = deps.storage.getSetting(GITHUB_STATUS_SETTING);
|
|
29497
|
+
const book = raw2 && typeof raw2 === "object" && !Array.isArray(raw2) ? { ...raw2 } : {};
|
|
29498
|
+
book[key] = githubWatchStatus(pr, new Date(nowMs).toISOString());
|
|
29499
|
+
const keys = Object.keys(book);
|
|
29500
|
+
for (const stale of keys.slice(0, Math.max(0, keys.length - GITHUB_STATUS_CAP))) delete book[stale];
|
|
29501
|
+
deps.storage.setSetting(GITHUB_STATUS_SETTING, book);
|
|
29502
|
+
}
|
|
28940
29503
|
function loadFired() {
|
|
28941
29504
|
const raw2 = deps.storage.getSetting("waker.fired");
|
|
28942
29505
|
return Array.isArray(raw2) ? raw2.filter((x) => typeof x === "string") : [];
|
|
@@ -29067,6 +29630,12 @@ function createScheduler(deps) {
|
|
|
29067
29630
|
if (!fault || limitFenceId(fault) !== item.fenceId) return "superseded";
|
|
29068
29631
|
return tele.turn === "idle" ? "current-idle" : "current-busy";
|
|
29069
29632
|
}
|
|
29633
|
+
if (isShellFenceId(item.fenceId)) {
|
|
29634
|
+
return tele.turn === "idle" ? "current-idle" : "current-busy";
|
|
29635
|
+
}
|
|
29636
|
+
if (isPrWatchFenceId(item.fenceId)) {
|
|
29637
|
+
return tele.turn === "idle" ? "current-idle" : "current-busy";
|
|
29638
|
+
}
|
|
29070
29639
|
if (isSnoozeFenceId(item.fenceId)) {
|
|
29071
29640
|
if (armedSnooze(row)?.fenceId !== item.fenceId) return "superseded";
|
|
29072
29641
|
return tele.turn === "idle" ? "current-idle" : "current-busy";
|
|
@@ -29079,7 +29648,7 @@ function createScheduler(deps) {
|
|
|
29079
29648
|
if (isStopHookFenceId(item.fenceId)) {
|
|
29080
29649
|
const armed = armedRest(row);
|
|
29081
29650
|
if (!armed || item.fenceId !== stopHookFenceId(armed.armedAt, tele.lastAssistantAt ?? "")) return "superseded";
|
|
29082
|
-
if (restMessageIsSignedOff(tele)) return "superseded";
|
|
29651
|
+
if (restMessageIsSignedOff(tele, registeredPrWatchesOf(deps.storage, item.slug), armedTimerIdsOf(deps.storage, item.slug))) return "superseded";
|
|
29083
29652
|
return tele.turn === "idle" ? "current-idle" : "current-busy";
|
|
29084
29653
|
}
|
|
29085
29654
|
if (isCompactFenceId(item.fenceId)) {
|
|
@@ -29096,10 +29665,6 @@ function createScheduler(deps) {
|
|
|
29096
29665
|
if (tele.lastFence || tele.pendingQuestion) return "superseded";
|
|
29097
29666
|
return tele.turn === "idle" ? "current-idle" : "current-busy";
|
|
29098
29667
|
}
|
|
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
29668
|
if (isReportFenceId(item.fenceId)) {
|
|
29104
29669
|
const stillMissing = tele.droppedReports?.some((r) => reportFenceId(r.taskId) === item.fenceId);
|
|
29105
29670
|
if (!stillMissing) return "superseded";
|
|
@@ -29246,26 +29811,30 @@ function createScheduler(deps) {
|
|
|
29246
29811
|
const deliveryId = wakeDeliveryId(slug, sessionId, fenceId);
|
|
29247
29812
|
if (outbox.get(deliveryId)) st.fired = true;
|
|
29248
29813
|
if (st.fired) return;
|
|
29249
|
-
const needsPr =
|
|
29814
|
+
const needsPr = false;
|
|
29250
29815
|
const needsReview = actionable.some((h) => isPrWatchHint(h.kind));
|
|
29251
29816
|
if ((needsPr || needsReview) && (st.lastPollAt === 0 || nowMs - st.lastPollAt >= pollMs)) {
|
|
29252
29817
|
st.lastPollAt = nowMs;
|
|
29253
29818
|
const refs = /* @__PURE__ */ new Map();
|
|
29254
29819
|
const reviewRefs = /* @__PURE__ */ new Map();
|
|
29820
|
+
const verdictRefs = /* @__PURE__ */ new Set();
|
|
29255
29821
|
for (const h of actionable) {
|
|
29256
29822
|
const ref = parsePrRef2(h.value);
|
|
29257
29823
|
if (!ref) continue;
|
|
29258
|
-
if (h.kind === "pr" || h.kind === "ci") refs.set(refKey2(ref), ref);
|
|
29259
29824
|
if (isPrWatchHint(h.kind)) reviewRefs.set(refKey2(ref), ref);
|
|
29260
29825
|
}
|
|
29261
29826
|
await Promise.all([
|
|
29262
29827
|
...[...refs].map(async ([k, ref]) => {
|
|
29263
29828
|
try {
|
|
29264
29829
|
const s = await fetchPr(ref);
|
|
29265
|
-
if (s)
|
|
29266
|
-
|
|
29830
|
+
if (s) {
|
|
29831
|
+
st.prCache.set(k, s);
|
|
29832
|
+
publishGithubStatus(k, s, nowMs);
|
|
29833
|
+
} else if (verdictRefs.has(k)) {
|
|
29834
|
+
log3(`waker: gh check skipped for ${k} (${slug}) \u2014 gh unavailable / not authed / rate-limited`);
|
|
29835
|
+
}
|
|
29267
29836
|
} catch (err) {
|
|
29268
|
-
log3(`waker: gh check errored for ${k} (${slug}): ${err instanceof Error ? err.message : String(err)}`);
|
|
29837
|
+
if (verdictRefs.has(k)) log3(`waker: gh check errored for ${k} (${slug}): ${err instanceof Error ? err.message : String(err)}`);
|
|
29269
29838
|
}
|
|
29270
29839
|
}),
|
|
29271
29840
|
...[...reviewRefs].map(async ([k, ref]) => {
|
|
@@ -29451,7 +30020,19 @@ function createScheduler(deps) {
|
|
|
29451
30020
|
sessionId: row.session_id,
|
|
29452
30021
|
fenceId,
|
|
29453
30022
|
hintKey: SIGNOFF_HINT_KEY,
|
|
29454
|
-
|
|
30023
|
+
// The reminder plus THIS THREAD's live ops and their ids, so the agent can write a correct
|
|
30024
|
+
// `watch:` line without going looking for an id it cannot see. Shells are named by `taskId` —
|
|
30025
|
+
// the handle the runtime actually showed the worker — because that is the string it will
|
|
30026
|
+
// naturally reach for, and the one the fence's own integrity check matches on.
|
|
30027
|
+
message: signoffNudgeMessage({
|
|
30028
|
+
shells: (tele.bgShells ?? []).filter((sh) => sh.state === "running").map((sh) => ({ id: sh.taskId ?? sh.id, label: sh.label })),
|
|
30029
|
+
subAgents: (tele.subAgents ?? []).filter((a) => a.state === "running").map((a) => ({ id: a.id, label: a.label })),
|
|
30030
|
+
// The other two registries, so the nudge lists EVERY kind an awaiting fence can name rather
|
|
30031
|
+
// than the two the fold happens to know about — a worker told about half its work writes half
|
|
30032
|
+
// a fence, and the half it left out is not what gets it bumped.
|
|
30033
|
+
timers: deps.storage.listThreadTimers(row.slug, { armedOnly: true }).map((t) => ({ id: t.id, label: t.prompt.trim().replace(/\s+/g, " ").slice(0, 80) })),
|
|
30034
|
+
prs: deps.storage.listPrWatches(row.slug, { armedOnly: true }).map((w) => ({ id: `${w.owner}/${w.repo}#${w.number}`, label: `${w.owner}/${w.repo}#${w.number}` }))
|
|
30035
|
+
}),
|
|
29455
30036
|
reason: "rested without signing off"
|
|
29456
30037
|
}, nowMs).delivery;
|
|
29457
30038
|
log3(`waker: queued ${row.slug} \u2014 ${item.reason}`);
|
|
@@ -29462,27 +30043,99 @@ function createScheduler(deps) {
|
|
|
29462
30043
|
if (!isSignoffFenceId(item.fenceId)) return;
|
|
29463
30044
|
deps.storage.countSignoffNudge(item.slug, item.fenceId);
|
|
29464
30045
|
}
|
|
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;
|
|
30046
|
+
function evalParkIntegrity(nowMs) {
|
|
30047
|
+
for (const row of deps.storage.allSessions()) {
|
|
30048
|
+
if (row.state === "archived" || row.archived === 1) continue;
|
|
29470
30049
|
const tele = deps.tailer.get(row.slug);
|
|
29471
|
-
if (!tele) continue;
|
|
29472
|
-
|
|
29473
|
-
|
|
30050
|
+
if (!tele || tele.turn !== "idle") continue;
|
|
30051
|
+
if (tele.lastFence?.kind !== "awaiting") continue;
|
|
30052
|
+
const spokeAt = tele.lastAssistantAt;
|
|
30053
|
+
if (!spokeAt) continue;
|
|
30054
|
+
if (tele.lastUserAt && Date.parse(tele.lastUserAt) >= Date.parse(spokeAt)) continue;
|
|
30055
|
+
if (tele.authFault) continue;
|
|
30056
|
+
const park = readAwaitingPark(tele.lastFence.hints);
|
|
30057
|
+
const live = liveActivityOf(
|
|
30058
|
+
tele,
|
|
30059
|
+
registeredPrWatchesOf(deps.storage, row.slug),
|
|
30060
|
+
armedTimerIdsOf(deps.storage, row.slug)
|
|
29474
30061
|
);
|
|
29475
|
-
|
|
29476
|
-
|
|
29477
|
-
|
|
29478
|
-
|
|
29479
|
-
if (
|
|
29480
|
-
if (
|
|
29481
|
-
|
|
29482
|
-
|
|
29483
|
-
|
|
29484
|
-
|
|
29485
|
-
|
|
30062
|
+
const dead = unaccountedItems(park.items, live);
|
|
30063
|
+
const expiresAt = parkExpiresAt(park, Date.parse(spokeAt));
|
|
30064
|
+
const expired = expiresAt !== null && nowMs >= expiresAt;
|
|
30065
|
+
const nameless = park.items.length === 0;
|
|
30066
|
+
if (parkIsHonoured(park, live) && (row.park_bumps ?? 0) > 0) deps.storage.resetParkBumps(row.slug);
|
|
30067
|
+
if (dead.length === 0 && !expired && !nameless) continue;
|
|
30068
|
+
if (park.forMs === null && !nameless) continue;
|
|
30069
|
+
const status = park.items.map((i) => {
|
|
30070
|
+
const gone = dead.some((d) => d.kind === i.kind && d.value === i.value);
|
|
30071
|
+
return `- \`${i.kind}: ${i.value}\` \u2014 ${gone ? "NOT RUNNING" : "still running"}`;
|
|
30072
|
+
});
|
|
30073
|
+
const retired = retiredAwaitingKindsIn(tele.lastFence.body ?? "");
|
|
30074
|
+
const head = retired.length > 0 ? [
|
|
30075
|
+
`\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`,
|
|
30076
|
+
"fence named nothing and your thread stayed in the queue.",
|
|
30077
|
+
"",
|
|
30078
|
+
...retired.map((k) => `- \`${k}:\` is GONE \u2192 ${RETIRED_AWAITING_REPLACEMENT[k]}`),
|
|
30079
|
+
"",
|
|
30080
|
+
"THE ONLY LINE KINDS NOW SUPPORTED, and every one is checked against something frizz can see:",
|
|
30081
|
+
"",
|
|
30082
|
+
" shell: <runtime task id> a background shell you launched",
|
|
30083
|
+
" agent: <runtime agent id> a sub-agent you dispatched",
|
|
30084
|
+
" timer: tmr_\u2026 a timer from `mcp__frizz__timer`",
|
|
30085
|
+
" pr: owner/repo#123 a PR registered with `mcp__frizz__watch_pr`",
|
|
30086
|
+
" for: 2h REQUIRED \u2014 a DURATION, never an instant",
|
|
30087
|
+
" reason: one line the only prose",
|
|
30088
|
+
"",
|
|
30089
|
+
"`mcp__frizz__activity` lists everything you have running with the exact id each line needs.",
|
|
30090
|
+
"If you are not waiting on anything, you are not awaiting \u2014 end with ```done, or ask a ```question."
|
|
30091
|
+
].join("\n") : nameless ? [
|
|
30092
|
+
"\u26A0\uFE0F Your ```awaiting fence names nothing to wait on, so it is not a park \u2014 your thread is still",
|
|
30093
|
+
"in the queue, and nothing will wake you.",
|
|
30094
|
+
"",
|
|
30095
|
+
"A wait has to be something frizz can SEE. Register one, then name it:",
|
|
30096
|
+
"",
|
|
30097
|
+
"- a pull request \u2192 `mcp__frizz__watch_pr` (it wakes you on CI going green or red, and on every",
|
|
30098
|
+
" later review and comment) \u2192 `pr: owner/repo#123`",
|
|
30099
|
+
"- a wall-clock check \u2192 `mcp__frizz__timer` \u2192 `timer: tmr_\u2026`",
|
|
30100
|
+
"- work you already launched \u2192 `shell: <the id your runtime gave you>` or `agent: <id>`",
|
|
30101
|
+
"",
|
|
30102
|
+
"Use `mcp__frizz__activity` to read back everything you have running, with the exact id each",
|
|
30103
|
+
"line needs.",
|
|
30104
|
+
"",
|
|
30105
|
+
"AND IF YOU ARE NOT WAITING ON ANYTHING, you are not awaiting \u2014 you are done. End with ```done,",
|
|
30106
|
+
"or ask a ```question if you need the human."
|
|
30107
|
+
].join("\n") : expired ? [
|
|
30108
|
+
"\u23F0 Your wait expired, nothing resolved. Check back in on everything.",
|
|
30109
|
+
"",
|
|
30110
|
+
...status,
|
|
30111
|
+
"",
|
|
30112
|
+
"Re-park if they are genuinely still going \u2014 there is no limit on that, and a long job is not a",
|
|
30113
|
+
"failure. If something is finished, read its result. If nothing is left, end in ```done or ask a",
|
|
30114
|
+
"```question."
|
|
30115
|
+
].join("\n") : [
|
|
30116
|
+
`\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.`,
|
|
30117
|
+
"",
|
|
30118
|
+
...status,
|
|
30119
|
+
"",
|
|
30120
|
+
"Use `mcp__frizz__activity` to read back what you actually have out, with the exact id each line",
|
|
30121
|
+
"needs, then re-fence naming only those \u2014 or end in ```done or a ```question if there is nothing",
|
|
30122
|
+
"left to wait for."
|
|
30123
|
+
].join("\n");
|
|
30124
|
+
const cause = retired.length > 0 ? "retired" : nameless ? "nameless" : expired ? "expired" : "dead";
|
|
30125
|
+
const ops = cause === "expired" ? [] : liveOpsLines({
|
|
30126
|
+
shells: (tele.bgShells ?? []).filter((sh) => sh.state === "running").map((sh) => ({ id: sh.taskId ?? sh.id, label: sh.label })),
|
|
30127
|
+
subAgents: (tele.subAgents ?? []).filter((a) => a.state === "running").map((a) => ({ id: a.id, label: a.label })),
|
|
30128
|
+
timers: deps.storage.listThreadTimers(row.slug, { armedOnly: true }).map((t) => ({ id: t.id, label: t.prompt.trim().replace(/\s+/g, " ").slice(0, 80) })),
|
|
30129
|
+
prs: deps.storage.listPrWatches(row.slug, { armedOnly: true }).map((w) => ({ id: `${w.owner}/${w.repo}#${w.number}`, label: `${w.owner}/${w.repo}#${w.number}` }))
|
|
30130
|
+
});
|
|
30131
|
+
const message = ops.length > 0 ? `${head}
|
|
30132
|
+
${ops.join("\n")}` : cause === "expired" ? head : `${head}
|
|
30133
|
+
|
|
30134
|
+
You have NOTHING running right now \u2014 no shell, no sub-agent, no timer, no registered
|
|
30135
|
+
pull request. There is nothing that could wake you, so this thread is not awaiting: finish in
|
|
30136
|
+
\`\`\`done, or ask a \`\`\`question.`;
|
|
30137
|
+
if (cause !== "expired" && (row.park_bumps ?? 0) >= PARK_BUMP_MAX) continue;
|
|
30138
|
+
const fenceId = `park:${cause}:${spokeAt}`;
|
|
29486
30139
|
const deliveryId = wakeDeliveryId(row.slug, row.session_id, fenceId);
|
|
29487
30140
|
if (outbox.get(deliveryId)) continue;
|
|
29488
30141
|
const item = outbox.enqueue({
|
|
@@ -29490,17 +30143,199 @@ function createScheduler(deps) {
|
|
|
29490
30143
|
slug: row.slug,
|
|
29491
30144
|
sessionId: row.session_id,
|
|
29492
30145
|
fenceId,
|
|
29493
|
-
hintKey:
|
|
29494
|
-
message
|
|
29495
|
-
reason: `
|
|
30146
|
+
hintKey: fenceId,
|
|
30147
|
+
message,
|
|
30148
|
+
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
30149
|
}, nowMs).delivery;
|
|
30150
|
+
if (cause !== "expired") deps.storage.countParkBump(row.slug, fenceId);
|
|
29497
30151
|
log3(`waker: queued ${row.slug} \u2014 ${item.reason}`);
|
|
29498
30152
|
checkpoint("after-enqueue", item);
|
|
29499
30153
|
}
|
|
29500
30154
|
}
|
|
29501
|
-
|
|
29502
|
-
|
|
29503
|
-
|
|
30155
|
+
const prWatchPolledAt = /* @__PURE__ */ new Map();
|
|
30156
|
+
function readPrWatchCursor(raw2) {
|
|
30157
|
+
try {
|
|
30158
|
+
const parsed = raw2 ? JSON.parse(raw2) : null;
|
|
30159
|
+
if (!parsed || typeof parsed !== "object") return { seen: [] };
|
|
30160
|
+
const seen = Array.isArray(parsed.seen) ? parsed.seen.filter((x) => typeof x === "string") : [];
|
|
30161
|
+
return { seen, checks: typeof parsed.checks === "string" ? parsed.checks : void 0, report: Number(parsed.report) || 0 };
|
|
30162
|
+
} catch {
|
|
30163
|
+
return { seen: [] };
|
|
30164
|
+
}
|
|
30165
|
+
}
|
|
30166
|
+
async function evalPrWatches(nowMs) {
|
|
30167
|
+
for (const w of deps.storage.expiredPrWatches(nowMs)) {
|
|
30168
|
+
const row = deps.storage.getSession(w.thread_slug);
|
|
30169
|
+
deps.storage.settlePrWatch(w.id, nowMs);
|
|
30170
|
+
const ref = `${w.owner}/${w.repo}#${w.number}`;
|
|
30171
|
+
log3(`waker: settled ${w.thread_slug} \u2014 pr watcher ${w.id} expired (${ref})`);
|
|
30172
|
+
if (!row || row.state === "archived" || row.archived === 1) continue;
|
|
30173
|
+
const fenceId = `prwatch-expired:${w.id}`;
|
|
30174
|
+
const deliveryId = wakeDeliveryId(row.slug, row.session_id, fenceId);
|
|
30175
|
+
if (outbox.get(deliveryId)) continue;
|
|
30176
|
+
const item = outbox.enqueue({
|
|
30177
|
+
id: deliveryId,
|
|
30178
|
+
slug: row.slug,
|
|
30179
|
+
sessionId: row.session_id,
|
|
30180
|
+
fenceId,
|
|
30181
|
+
hintKey: `prwatch-expired:${w.id}`,
|
|
30182
|
+
message: `\u23F0 Your watcher on ${ref} has expired and is no longer armed \u2014 nothing on that PR will wake you now.
|
|
30183
|
+
|
|
30184
|
+
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.`,
|
|
30185
|
+
reason: `pr watcher ${w.id} expired (${ref})`
|
|
30186
|
+
}, nowMs).delivery;
|
|
30187
|
+
log3(`waker: queued ${row.slug} \u2014 ${item.reason}`);
|
|
30188
|
+
checkpoint("after-enqueue", item);
|
|
30189
|
+
}
|
|
30190
|
+
const armed = deps.storage.armedPrWatches();
|
|
30191
|
+
if (armed.length === 0) return;
|
|
30192
|
+
const refs = /* @__PURE__ */ new Map();
|
|
30193
|
+
for (const w of armed) {
|
|
30194
|
+
const key = `${w.owner}/${w.repo}#${w.number}`;
|
|
30195
|
+
const last = prWatchPolledAt.get(key) ?? 0;
|
|
30196
|
+
if (nowMs - last < PR_WATCH_POLL_MS) continue;
|
|
30197
|
+
refs.set(key, { owner: w.owner, repo: w.repo, number: w.number });
|
|
30198
|
+
}
|
|
30199
|
+
const status = /* @__PURE__ */ new Map();
|
|
30200
|
+
const activity = /* @__PURE__ */ new Map();
|
|
30201
|
+
await Promise.all([...refs].map(async ([key, ref]) => {
|
|
30202
|
+
prWatchPolledAt.set(key, nowMs);
|
|
30203
|
+
try {
|
|
30204
|
+
const pr = await fetchPr(ref);
|
|
30205
|
+
if (pr) {
|
|
30206
|
+
status.set(key, githubWatchStatus(pr, new Date(nowMs).toISOString()));
|
|
30207
|
+
publishGithubStatus(key, pr, nowMs);
|
|
30208
|
+
}
|
|
30209
|
+
} catch (err) {
|
|
30210
|
+
log3(`waker: pr-watch status fetch failed for ${key}: ${err instanceof Error ? err.message : String(err)}`);
|
|
30211
|
+
}
|
|
30212
|
+
try {
|
|
30213
|
+
const result = normalizeReviewResult(await fetchGithubReview(ref));
|
|
30214
|
+
if (result.status === "ok") {
|
|
30215
|
+
activity.set(key, result.activity);
|
|
30216
|
+
recordReviewSuccess(key, "pr-watch registry");
|
|
30217
|
+
} else if (result.status === "error") recordReviewFailure(key, "pr-watch registry", result, nowMs);
|
|
30218
|
+
} catch (err) {
|
|
30219
|
+
recordReviewFailure(key, "pr-watch registry", {
|
|
30220
|
+
status: "error",
|
|
30221
|
+
failure: { kind: "network", message: err instanceof Error ? err.message : String(err) }
|
|
30222
|
+
}, nowMs);
|
|
30223
|
+
}
|
|
30224
|
+
}));
|
|
30225
|
+
for (const w of armed) {
|
|
30226
|
+
const row = deps.storage.getSession(w.thread_slug);
|
|
30227
|
+
if (!row || row.state === "archived" || row.archived === 1) continue;
|
|
30228
|
+
const key = `${w.owner}/${w.repo}#${w.number}`;
|
|
30229
|
+
const st = status.get(key);
|
|
30230
|
+
const acts = activity.get(key);
|
|
30231
|
+
if (!st && !acts) continue;
|
|
30232
|
+
const cursor = readPrWatchCursor(w.cursor);
|
|
30233
|
+
if (st && st.state !== "open") {
|
|
30234
|
+
deps.storage.settlePrWatch(w.id, nowMs);
|
|
30235
|
+
enqueuePrWatchWake(row, w.id, nextReport(cursor), prWatchWakeMessage({
|
|
30236
|
+
target: key,
|
|
30237
|
+
merged: st.state === "merged",
|
|
30238
|
+
closed: st.state === "closed"
|
|
30239
|
+
}), `pr-watch ${key} ${st.state}`, nowMs);
|
|
30240
|
+
continue;
|
|
30241
|
+
}
|
|
30242
|
+
const terminal = st && (st.checks === "passing" || st.checks === "failing") ? st.checks : void 0;
|
|
30243
|
+
const stamp = terminal !== void 0 ? `${st?.head ?? "?"}:${terminal}:${st?.failureSig ?? ""}` : void 0;
|
|
30244
|
+
const checksChanged = stamp !== void 0 && cursor.checks !== stamp;
|
|
30245
|
+
const seen = new Set(cursor.seen);
|
|
30246
|
+
const firstPoll = w.cursor === null;
|
|
30247
|
+
const newestFirst = [...acts ?? []].sort((a, b) => {
|
|
30248
|
+
const at = Date.parse(b.at ?? "") - Date.parse(a.at ?? "");
|
|
30249
|
+
return Number.isFinite(at) && at !== 0 ? at : b.id.localeCompare(a.id);
|
|
30250
|
+
});
|
|
30251
|
+
const fresh = newestFirst.filter((a) => {
|
|
30252
|
+
if (seen.has(a.id)) return false;
|
|
30253
|
+
if (!firstPoll) return true;
|
|
30254
|
+
const landed = Date.parse(a.at ?? "");
|
|
30255
|
+
return Number.isFinite(landed) && landed > w.created_at;
|
|
30256
|
+
});
|
|
30257
|
+
const named = fresh.slice(0, REVIEW_STEER_CAP);
|
|
30258
|
+
const reviewSteer = fresh.length > 0 ? formatGithubWakeSteer({
|
|
30259
|
+
ref: key,
|
|
30260
|
+
omitted: fresh.length - named.length,
|
|
30261
|
+
items: [...named].reverse().map((a) => ({
|
|
30262
|
+
label: activityLabel(a),
|
|
30263
|
+
actor: a.actor,
|
|
30264
|
+
bot: isBotGithubActor(a),
|
|
30265
|
+
...a.at ? { at: a.at } : {},
|
|
30266
|
+
...a.url ? { url: a.url } : {}
|
|
30267
|
+
}))
|
|
30268
|
+
}) : void 0;
|
|
30269
|
+
const nextCursor = {
|
|
30270
|
+
seen: acts ? [.../* @__PURE__ */ new Set([...cursor.seen, ...acts.map((a) => a.id)])].slice(-REVIEW_SEEN_CAP) : cursor.seen,
|
|
30271
|
+
// The STAMP, so the next poll compares against the commit as well as the colour.
|
|
30272
|
+
checks: stamp ?? cursor.checks,
|
|
30273
|
+
report: cursor.report ?? 0
|
|
30274
|
+
};
|
|
30275
|
+
if (!checksChanged && !reviewSteer) {
|
|
30276
|
+
if (JSON.stringify(nextCursor) !== JSON.stringify(cursor)) {
|
|
30277
|
+
deps.storage.setPrWatchCursor(w.id, JSON.stringify(nextCursor));
|
|
30278
|
+
}
|
|
30279
|
+
continue;
|
|
30280
|
+
}
|
|
30281
|
+
deps.storage.setSnoozedUntil(row.slug, null);
|
|
30282
|
+
const report = nextReport(cursor);
|
|
30283
|
+
enqueuePrWatchWake(row, w.id, report, prWatchWakeMessage({
|
|
30284
|
+
target: key,
|
|
30285
|
+
...checksChanged && terminal ? { checks: { verdict: terminal, passed: st.passed, failed: st.failed, failing: st.failing } } : {},
|
|
30286
|
+
...reviewSteer ? { review: reviewSteer } : {}
|
|
30287
|
+
}), `pr-watch ${key}${checksChanged ? ` CI ${terminal}` : ""}${reviewSteer ? " review" : ""}`, nowMs);
|
|
30288
|
+
deps.storage.setPrWatchCursor(w.id, JSON.stringify({ ...nextCursor, report }));
|
|
30289
|
+
}
|
|
30290
|
+
}
|
|
30291
|
+
function nextReport(cursor) {
|
|
30292
|
+
return (cursor.report ?? 0) + 1;
|
|
30293
|
+
}
|
|
30294
|
+
function enqueuePrWatchWake(row, watchId, report, message, reason, nowMs) {
|
|
30295
|
+
const fenceId = prWatchFenceId(watchId, report);
|
|
30296
|
+
const deliveryId = wakeDeliveryId(row.slug, row.session_id, fenceId);
|
|
30297
|
+
if (outbox.get(deliveryId)) return;
|
|
30298
|
+
const item = outbox.enqueue({
|
|
30299
|
+
id: deliveryId,
|
|
30300
|
+
slug: row.slug,
|
|
30301
|
+
sessionId: row.session_id,
|
|
30302
|
+
fenceId,
|
|
30303
|
+
hintKey: `${PR_WATCH_FENCE_PREFIX}:${watchId}`,
|
|
30304
|
+
message,
|
|
30305
|
+
reason
|
|
30306
|
+
}, nowMs).delivery;
|
|
30307
|
+
log3(`waker: queued ${row.slug} \u2014 ${item.reason}`);
|
|
30308
|
+
checkpoint("after-enqueue", item);
|
|
30309
|
+
}
|
|
30310
|
+
function evalShellCompletions(nowMs) {
|
|
30311
|
+
for (const row of deps.storage.allSessions()) {
|
|
30312
|
+
if (row.state === "archived" || row.archived === 1) continue;
|
|
30313
|
+
const tele = deps.tailer.get(row.slug);
|
|
30314
|
+
if (!tele) continue;
|
|
30315
|
+
if (tele.turn !== "idle") continue;
|
|
30316
|
+
if (tele.authFault) continue;
|
|
30317
|
+
const restedAt = Date.parse(tele.lastAssistantAt ?? "");
|
|
30318
|
+
if (!Number.isFinite(restedAt)) continue;
|
|
30319
|
+
for (const shell of tele.retiredShells ?? []) {
|
|
30320
|
+
const finishedAt = Date.parse(shell.finishedAt ?? "");
|
|
30321
|
+
if (!Number.isFinite(finishedAt) || finishedAt <= restedAt) continue;
|
|
30322
|
+
const fenceId = shellFenceId(shell.id);
|
|
30323
|
+
const deliveryId = wakeDeliveryId(row.slug, row.session_id, fenceId);
|
|
30324
|
+
if (outbox.get(deliveryId)) continue;
|
|
30325
|
+
const item = outbox.enqueue({
|
|
30326
|
+
id: deliveryId,
|
|
30327
|
+
slug: row.slug,
|
|
30328
|
+
sessionId: row.session_id,
|
|
30329
|
+
fenceId,
|
|
30330
|
+
hintKey: `${SHELL_FENCE_PREFIX}:${shell.id}`,
|
|
30331
|
+
message: shellDoneMessage(shell),
|
|
30332
|
+
reason: `background shell finished (${shell.taskId ?? shell.label})`
|
|
30333
|
+
}, nowMs).delivery;
|
|
30334
|
+
log3(`waker: queued ${row.slug} \u2014 ${item.reason}`);
|
|
30335
|
+
checkpoint("after-enqueue", item);
|
|
30336
|
+
return;
|
|
30337
|
+
}
|
|
30338
|
+
}
|
|
29504
30339
|
}
|
|
29505
30340
|
function evalTimers(nowMs) {
|
|
29506
30341
|
for (const timer2 of deps.storage.dueThreadTimers(nowMs)) {
|
|
@@ -29534,7 +30369,6 @@ function createScheduler(deps) {
|
|
|
29534
30369
|
if (!armed || armed.dueAtMs > nowMs) continue;
|
|
29535
30370
|
const beatTele = deps.tailer.get(row.slug);
|
|
29536
30371
|
if (beatTele && saidDone(beatTele)) continue;
|
|
29537
|
-
if (heldByQuestion(row, beatTele)) continue;
|
|
29538
30372
|
if (openSchedulePrompt(row.slug, row.session_id)) continue;
|
|
29539
30373
|
const beat = beatIndex(row.recurring_schedule_fired_at ?? null, armed);
|
|
29540
30374
|
const fenceId = heartbeatFenceId(armed.armedAt, beat);
|
|
@@ -29583,8 +30417,7 @@ function createScheduler(deps) {
|
|
|
29583
30417
|
if (!restedAt) continue;
|
|
29584
30418
|
if (tele.lastUserAt && Date.parse(tele.lastUserAt) >= Date.parse(restedAt)) continue;
|
|
29585
30419
|
if (tele.authFault) continue;
|
|
29586
|
-
if (restMessageIsSignedOff(tele)) continue;
|
|
29587
|
-
if (heldByQuestion(row, tele)) continue;
|
|
30420
|
+
if (restMessageIsSignedOff(tele, registeredPrWatchesOf(deps.storage, row.slug), armedTimerIdsOf(deps.storage, row.slug))) continue;
|
|
29588
30421
|
const fenceId = stopHookFenceId(armed.armedAt, restedAt);
|
|
29589
30422
|
const deliveryId = wakeDeliveryId(row.slug, row.session_id, fenceId);
|
|
29590
30423
|
if (outbox.get(deliveryId)) continue;
|
|
@@ -29594,7 +30427,7 @@ function createScheduler(deps) {
|
|
|
29594
30427
|
sessionId: row.session_id,
|
|
29595
30428
|
fenceId,
|
|
29596
30429
|
hintKey: STOP_HOOK_HINT_KEY,
|
|
29597
|
-
message: restPromptMessage(armed.prompt),
|
|
30430
|
+
message: restPromptMessage(armed.prompt, { overQuestion: tele.pendingQuestion === true }),
|
|
29598
30431
|
reason: "recurring prompt at rest"
|
|
29599
30432
|
}, nowMs).delivery;
|
|
29600
30433
|
log3(`waker: queued ${row.slug} \u2014 ${item.reason}`);
|
|
@@ -29609,7 +30442,6 @@ function createScheduler(deps) {
|
|
|
29609
30442
|
const tele = deps.tailer.get(row.slug);
|
|
29610
30443
|
if (!tele?.lastCompactionAt) continue;
|
|
29611
30444
|
if (tele.lastCompactionAt <= armed.armedAt) continue;
|
|
29612
|
-
if (heldByQuestion(row, tele)) continue;
|
|
29613
30445
|
const fenceId = compactFenceId(armed.armedAt, tele.lastCompactionAt);
|
|
29614
30446
|
const deliveryId = wakeDeliveryId(row.slug, row.session_id, fenceId);
|
|
29615
30447
|
if (outbox.get(deliveryId)) continue;
|
|
@@ -29659,7 +30491,6 @@ function createScheduler(deps) {
|
|
|
29659
30491
|
settleRestPrompt(item);
|
|
29660
30492
|
settleCompactPrompt(item);
|
|
29661
30493
|
settleTimer(item);
|
|
29662
|
-
settleWatcher(item);
|
|
29663
30494
|
continue;
|
|
29664
30495
|
}
|
|
29665
30496
|
if (context === "superseded") {
|
|
@@ -29679,7 +30510,6 @@ function createScheduler(deps) {
|
|
|
29679
30510
|
if (recovered?.state === "exhausted") {
|
|
29680
30511
|
settleSnooze(item);
|
|
29681
30512
|
settleTimer(item);
|
|
29682
|
-
settleWatcher(item);
|
|
29683
30513
|
log3(`waker: delivery EXHAUSTED for ${item.slug} after ${recovered.attempts} attempts \u2014 ${recovered.lastError ?? "unknown error"}`);
|
|
29684
30514
|
}
|
|
29685
30515
|
}
|
|
@@ -29698,7 +30528,6 @@ function createScheduler(deps) {
|
|
|
29698
30528
|
settleRestPrompt(item);
|
|
29699
30529
|
settleCompactPrompt(item);
|
|
29700
30530
|
settleTimer(item);
|
|
29701
|
-
settleWatcher(item);
|
|
29702
30531
|
continue;
|
|
29703
30532
|
}
|
|
29704
30533
|
if (context === "superseded") {
|
|
@@ -29726,7 +30555,6 @@ function createScheduler(deps) {
|
|
|
29726
30555
|
outbox.supersede(item.id, failedAt, message);
|
|
29727
30556
|
settleSnooze(item);
|
|
29728
30557
|
settleTimer(item);
|
|
29729
|
-
settleWatcher(item);
|
|
29730
30558
|
log3(`waker: delivery ABANDONED for ${item.slug} (terminal, no retry): ${message}`);
|
|
29731
30559
|
continue;
|
|
29732
30560
|
}
|
|
@@ -29752,7 +30580,6 @@ function createScheduler(deps) {
|
|
|
29752
30580
|
settleRestPrompt(item);
|
|
29753
30581
|
settleCompactPrompt(item);
|
|
29754
30582
|
settleTimer(item);
|
|
29755
|
-
settleWatcher(item);
|
|
29756
30583
|
settleSignoffNudge(item);
|
|
29757
30584
|
const acknowledged = outbox.get(item.id);
|
|
29758
30585
|
if (acknowledged) checkpoint("after-ack", acknowledged);
|
|
@@ -29821,15 +30648,22 @@ function createScheduler(deps) {
|
|
|
29821
30648
|
}
|
|
29822
30649
|
try {
|
|
29823
30650
|
evalSignoffNudges(now());
|
|
30651
|
+
evalParkIntegrity(now());
|
|
29824
30652
|
} catch (err) {
|
|
29825
30653
|
if (err instanceof InjectedSchedulerCrash) throw err;
|
|
29826
30654
|
log3(`waker: sign-off nudge pass failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
29827
30655
|
}
|
|
29828
30656
|
try {
|
|
29829
|
-
|
|
30657
|
+
await evalPrWatches(now());
|
|
30658
|
+
} catch (err) {
|
|
30659
|
+
if (err instanceof InjectedSchedulerCrash) throw err;
|
|
30660
|
+
log3(`waker: pr-watch registry pass failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
30661
|
+
}
|
|
30662
|
+
try {
|
|
30663
|
+
evalShellCompletions(now());
|
|
29830
30664
|
} catch (err) {
|
|
29831
30665
|
if (err instanceof InjectedSchedulerCrash) throw err;
|
|
29832
|
-
log3(`waker:
|
|
30666
|
+
log3(`waker: shell-completion pass failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
29833
30667
|
}
|
|
29834
30668
|
try {
|
|
29835
30669
|
evalTimers(now());
|
|
@@ -29881,11 +30715,12 @@ function createScheduler(deps) {
|
|
|
29881
30715
|
tick
|
|
29882
30716
|
};
|
|
29883
30717
|
}
|
|
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,
|
|
30718
|
+
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_POLL_MS, SIGNOFF_FENCE_PREFIX, SIGNOFF_HINT_KEY, SIGNOFF_NUDGE_MAX, PARK_BUMP_MAX, SIGNOFF_NUDGE_SETTING, FIRED_CAP, GITHUB_STATUS_CAP, REGISTRATION_CAP, REVIEW_SEEN_CAP, INTRODUCED_CAP, REVIEW_STEER_CAP;
|
|
29885
30719
|
var init_scheduler = __esm({
|
|
29886
30720
|
"packages/server/src/scheduler.ts"() {
|
|
29887
30721
|
"use strict";
|
|
29888
30722
|
init_src();
|
|
30723
|
+
init_awaiting();
|
|
29889
30724
|
init_usage_limit();
|
|
29890
30725
|
init_wake_store();
|
|
29891
30726
|
init_shutdown();
|
|
@@ -29912,14 +30747,16 @@ var init_scheduler = __esm({
|
|
|
29912
30747
|
COMPACT_HINT_KEY = "recurring:compaction";
|
|
29913
30748
|
TIMER_FENCE_PREFIX = "timer";
|
|
29914
30749
|
TIMER_HINT_PREFIX = "timer:";
|
|
30750
|
+
SHELL_FENCE_PREFIX = "shell";
|
|
30751
|
+
PR_WATCH_FENCE_PREFIX = "prwatch";
|
|
30752
|
+
PR_WATCH_POLL_MS = 6e4;
|
|
29915
30753
|
SIGNOFF_FENCE_PREFIX = "signoff";
|
|
29916
30754
|
SIGNOFF_HINT_KEY = "signoff:rest";
|
|
29917
30755
|
SIGNOFF_NUDGE_MAX = 2;
|
|
30756
|
+
PARK_BUMP_MAX = 3;
|
|
29918
30757
|
SIGNOFF_NUDGE_SETTING = "signoffNudge";
|
|
29919
|
-
WATCH_FENCE_PREFIX = "watch";
|
|
29920
|
-
WATCH_HINT_PREFIX = "watch:";
|
|
29921
|
-
SHELL_SEEN = "seen";
|
|
29922
30758
|
FIRED_CAP = 500;
|
|
30759
|
+
GITHUB_STATUS_CAP = 200;
|
|
29923
30760
|
REGISTRATION_CAP = 500;
|
|
29924
30761
|
REVIEW_SEEN_CAP = 300;
|
|
29925
30762
|
INTRODUCED_CAP = 500;
|
|
@@ -30628,7 +31465,7 @@ var init_orphan_reaper = __esm({
|
|
|
30628
31465
|
// packages/server/src/context.ts
|
|
30629
31466
|
import { join as join29 } from "node:path";
|
|
30630
31467
|
import { randomUUID as randomUUID15 } from "node:crypto";
|
|
30631
|
-
import { homedir as
|
|
31468
|
+
import { homedir as homedir13 } from "node:os";
|
|
30632
31469
|
function reconcileSessions(storage) {
|
|
30633
31470
|
for (const row of storage.allSessions()) {
|
|
30634
31471
|
if (isHeadlessRow(row)) continue;
|
|
@@ -30782,7 +31619,7 @@ async function createContext(opts = {}) {
|
|
|
30782
31619
|
}
|
|
30783
31620
|
}
|
|
30784
31621
|
function createContextUnchecked(opts, resources) {
|
|
30785
|
-
const home = opts.home ??
|
|
31622
|
+
const home = opts.home ?? homedir13();
|
|
30786
31623
|
const project = opts.project ?? resolveProject();
|
|
30787
31624
|
const dbPath = join29(project.stateDir, "ui.db");
|
|
30788
31625
|
const storage = createStorage(dbPath);
|
|
@@ -31356,11 +32193,11 @@ var init_url = __esm({
|
|
|
31356
32193
|
}
|
|
31357
32194
|
return null;
|
|
31358
32195
|
};
|
|
31359
|
-
tryDecode = (
|
|
32196
|
+
tryDecode = (str2, decoder) => {
|
|
31360
32197
|
try {
|
|
31361
|
-
return decoder(
|
|
32198
|
+
return decoder(str2);
|
|
31362
32199
|
} catch {
|
|
31363
|
-
return
|
|
32200
|
+
return str2.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match2) => {
|
|
31364
32201
|
try {
|
|
31365
32202
|
return decoder(match2);
|
|
31366
32203
|
} catch {
|
|
@@ -31369,7 +32206,7 @@ var init_url = __esm({
|
|
|
31369
32206
|
});
|
|
31370
32207
|
}
|
|
31371
32208
|
};
|
|
31372
|
-
tryDecodeURI = (
|
|
32209
|
+
tryDecodeURI = (str2) => tryDecode(str2, decodeURI);
|
|
31373
32210
|
getPath = (request) => {
|
|
31374
32211
|
const url = request.url;
|
|
31375
32212
|
const start = url.indexOf("/", url.indexOf(":") + 4);
|
|
@@ -31517,7 +32354,7 @@ var init_request = __esm({
|
|
|
31517
32354
|
init_constants();
|
|
31518
32355
|
init_body();
|
|
31519
32356
|
init_url();
|
|
31520
|
-
tryDecodeURIComponent = (
|
|
32357
|
+
tryDecodeURIComponent = (str2) => tryDecode(str2, decodeURIComponent_);
|
|
31521
32358
|
HonoRequest = class {
|
|
31522
32359
|
/**
|
|
31523
32360
|
* `.raw` can get the raw Request object.
|
|
@@ -31816,27 +32653,27 @@ var init_html = __esm({
|
|
|
31816
32653
|
escapedString.callbacks = callbacks;
|
|
31817
32654
|
return escapedString;
|
|
31818
32655
|
};
|
|
31819
|
-
resolveCallback = async (
|
|
31820
|
-
if (typeof
|
|
31821
|
-
if (!(
|
|
31822
|
-
|
|
32656
|
+
resolveCallback = async (str2, phase, preserveCallbacks, context, buffer) => {
|
|
32657
|
+
if (typeof str2 === "object" && !(str2 instanceof String)) {
|
|
32658
|
+
if (!(str2 instanceof Promise)) {
|
|
32659
|
+
str2 = str2.toString();
|
|
31823
32660
|
}
|
|
31824
|
-
if (
|
|
31825
|
-
|
|
32661
|
+
if (str2 instanceof Promise) {
|
|
32662
|
+
str2 = await str2;
|
|
31826
32663
|
}
|
|
31827
32664
|
}
|
|
31828
|
-
const callbacks =
|
|
32665
|
+
const callbacks = str2.callbacks;
|
|
31829
32666
|
if (!callbacks?.length) {
|
|
31830
|
-
return Promise.resolve(
|
|
32667
|
+
return Promise.resolve(str2);
|
|
31831
32668
|
}
|
|
31832
32669
|
if (buffer) {
|
|
31833
|
-
buffer[0] +=
|
|
32670
|
+
buffer[0] += str2;
|
|
31834
32671
|
} else {
|
|
31835
|
-
buffer = [
|
|
32672
|
+
buffer = [str2];
|
|
31836
32673
|
}
|
|
31837
32674
|
const resStr = Promise.all(callbacks.map((c) => c({ phase, buffer, context }))).then(
|
|
31838
32675
|
(res) => Promise.all(
|
|
31839
|
-
res.filter(Boolean).map((
|
|
32676
|
+
res.filter(Boolean).map((str22) => resolveCallback(str22, phase, false, context, buffer))
|
|
31840
32677
|
).then(() => buffer[0])
|
|
31841
32678
|
);
|
|
31842
32679
|
if (preserveCallbacks) {
|
|
@@ -34006,11 +34843,14 @@ import { closeSync as closeSync13, existsSync as existsSync11, fstatSync as fsta
|
|
|
34006
34843
|
import { createHash as createHash15 } from "node:crypto";
|
|
34007
34844
|
import { StringDecoder as StringDecoder4 } from "node:string_decoder";
|
|
34008
34845
|
import { join as join30 } from "node:path";
|
|
34009
|
-
import { homedir as
|
|
34846
|
+
import { homedir as homedir14 } from "node:os";
|
|
34010
34847
|
function isInjectedNoise(text) {
|
|
34011
34848
|
const t = text.trimStart();
|
|
34012
34849
|
return NOISE_PREFIXES.some((p) => t.startsWith(p));
|
|
34013
34850
|
}
|
|
34851
|
+
function isInterruptMarker(text) {
|
|
34852
|
+
return INTERRUPT_MARKERS.includes(text.trim());
|
|
34853
|
+
}
|
|
34014
34854
|
function formatTokens(n) {
|
|
34015
34855
|
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
34016
34856
|
if (n >= 1e3) return `${Math.round(n / 1e3)}k`;
|
|
@@ -34246,8 +35086,8 @@ function createTranscriptFold(identityPrefix = "claude") {
|
|
|
34246
35086
|
}
|
|
34247
35087
|
const peerOrigin = att.origin?.kind === "peer" ? att.origin : void 0;
|
|
34248
35088
|
if (peerOrigin && att.commandMode === "prompt") {
|
|
34249
|
-
const
|
|
34250
|
-
const senderTaskId =
|
|
35089
|
+
const str2 = (v) => typeof v === "string" ? v.trim() : "";
|
|
35090
|
+
const senderTaskId = str2(peerOrigin.senderTaskId);
|
|
34251
35091
|
const dispatchId = senderTaskId ? childDispatchIds.get(senderTaskId) : void 0;
|
|
34252
35092
|
const entry = prompt.trim() ? findQueued(prompt) : void 0;
|
|
34253
35093
|
if (entry) {
|
|
@@ -34263,7 +35103,7 @@ function createTranscriptFold(identityPrefix = "claude") {
|
|
|
34263
35103
|
} else {
|
|
34264
35104
|
const parsed = prompt.trim() ? parseAgentMessage(prompt) : void 0;
|
|
34265
35105
|
const describedHere = dispatchId ? dispatchLabels.get(dispatchId) : void 0;
|
|
34266
|
-
const from = describedHere ||
|
|
35106
|
+
const from = describedHere || str2(peerOrigin.from) || str2(peerOrigin.name) || parsed?.from || "";
|
|
34267
35107
|
const body = parsed?.body ?? (typeof peerOrigin.body === "string" ? peerOrigin.body : "");
|
|
34268
35108
|
if (from && body.trim()) {
|
|
34269
35109
|
out.push({
|
|
@@ -34307,6 +35147,7 @@ function createTranscriptFold(identityPrefix = "claude") {
|
|
|
34307
35147
|
}
|
|
34308
35148
|
let text = userText(rec);
|
|
34309
35149
|
if (text && isAllInjectedNoise(text)) return;
|
|
35150
|
+
if (text && isInterruptMarker(text)) return;
|
|
34310
35151
|
if (text) {
|
|
34311
35152
|
const queued = findQueued(text)?.message;
|
|
34312
35153
|
if (queued) {
|
|
@@ -35015,7 +35856,7 @@ function verifyIncrementalParse(path, identityPrefix, incremental) {
|
|
|
35015
35856
|
}
|
|
35016
35857
|
}
|
|
35017
35858
|
function logDirOf(project) {
|
|
35018
|
-
return join30(
|
|
35859
|
+
return join30(homedir14(), ".claude", "projects", project.cwdSlug);
|
|
35019
35860
|
}
|
|
35020
35861
|
function resolveTranscriptPath(project, sessionId) {
|
|
35021
35862
|
const path = join30(logDirOf(project), `${sessionId}.jsonl`);
|
|
@@ -36302,7 +37143,7 @@ function readTranscriptFile(absPath) {
|
|
|
36302
37143
|
return [];
|
|
36303
37144
|
}
|
|
36304
37145
|
}
|
|
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;
|
|
37146
|
+
var MAX_MESSAGES, 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
37147
|
var init_transcript = __esm({
|
|
36307
37148
|
"packages/server/src/transcript.ts"() {
|
|
36308
37149
|
"use strict";
|
|
@@ -36318,6 +37159,7 @@ var init_transcript = __esm({
|
|
|
36318
37159
|
init_frizz_paths();
|
|
36319
37160
|
MAX_MESSAGES = 300;
|
|
36320
37161
|
NOISE_PREFIXES = ["<task-notification>", "[SYSTEM NOTIFICATION", "<system-reminder>", "<frizz-", "[frizz]"];
|
|
37162
|
+
INTERRUPT_MARKERS = ["[Request interrupted by user]", "[Request interrupted by user for tool use]"];
|
|
36321
37163
|
REST_LABEL = "Agent rested";
|
|
36322
37164
|
LEGACY_TASK_MARKER = "\nTASK:\n";
|
|
36323
37165
|
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 +37236,7 @@ var init_open_external = __esm({
|
|
|
36394
37236
|
// packages/server/src/local-file.ts
|
|
36395
37237
|
import { spawn as spawn6 } from "node:child_process";
|
|
36396
37238
|
import { readFileSync as readFileSync20, realpathSync as realpathSync8, statSync as statSync15 } from "node:fs";
|
|
36397
|
-
import { homedir as
|
|
37239
|
+
import { homedir as homedir15 } from "node:os";
|
|
36398
37240
|
import { isAbsolute as isAbsolute3, join as join31, resolve as resolve7, sep } from "node:path";
|
|
36399
37241
|
function isUnder(real, root) {
|
|
36400
37242
|
let rootReal;
|
|
@@ -36422,7 +37264,7 @@ function resolveLocalFile(rawPath, roots) {
|
|
|
36422
37264
|
}
|
|
36423
37265
|
return real;
|
|
36424
37266
|
}
|
|
36425
|
-
function resolveOpenableFile(raw2, projectDir, roots, home =
|
|
37267
|
+
function resolveOpenableFile(raw2, projectDir, roots, home = homedir15()) {
|
|
36426
37268
|
const trimmed = raw2.trim().replace(/:\d+(?::\d+)?$/, "");
|
|
36427
37269
|
if (!trimmed) return null;
|
|
36428
37270
|
const abs = trimmed === "~" ? home : trimmed.startsWith("~/") ? join31(home, trimmed.slice(2)) : isAbsolute3(trimmed) ? trimmed : resolve7(projectDir, trimmed);
|
|
@@ -36462,6 +37304,286 @@ var init_local_file = __esm({
|
|
|
36462
37304
|
}
|
|
36463
37305
|
});
|
|
36464
37306
|
|
|
37307
|
+
// packages/server/src/github-hovercard.ts
|
|
37308
|
+
function parseRef(ref) {
|
|
37309
|
+
const match2 = REF_PATTERN.exec(ref);
|
|
37310
|
+
if (!match2) return null;
|
|
37311
|
+
const [, owner, name, number, sha] = match2;
|
|
37312
|
+
if (number) {
|
|
37313
|
+
const n = Number(number);
|
|
37314
|
+
if (!Number.isSafeInteger(n) || n <= 0 || n > 2147483647) return null;
|
|
37315
|
+
return { key: ref, owner, name, number: n };
|
|
37316
|
+
}
|
|
37317
|
+
return { key: ref, owner, name, sha };
|
|
37318
|
+
}
|
|
37319
|
+
function isImmutableRef(ref) {
|
|
37320
|
+
return ref.sha !== void 0;
|
|
37321
|
+
}
|
|
37322
|
+
function buildRefQuery(refs) {
|
|
37323
|
+
const variables = {};
|
|
37324
|
+
const declarations = [];
|
|
37325
|
+
const selections = [];
|
|
37326
|
+
refs.forEach((ref, index) => {
|
|
37327
|
+
declarations.push(`$owner${index}: String!`, `$name${index}: String!`);
|
|
37328
|
+
variables[`owner${index}`] = ref.owner;
|
|
37329
|
+
variables[`name${index}`] = ref.name;
|
|
37330
|
+
if (ref.number !== void 0) {
|
|
37331
|
+
declarations.push(`$number${index}: Int!`);
|
|
37332
|
+
variables[`number${index}`] = ref.number;
|
|
37333
|
+
selections.push(` a${index}: repository(owner: $owner${index}, name: $name${index}) {
|
|
37334
|
+
target: issueOrPullRequest(number: $number${index}) {${ISSUE_FIELDS}
|
|
37335
|
+
}
|
|
37336
|
+
}`);
|
|
37337
|
+
} else {
|
|
37338
|
+
declarations.push(`$rev${index}: String!`);
|
|
37339
|
+
variables[`rev${index}`] = ref.sha;
|
|
37340
|
+
selections.push(` a${index}: repository(owner: $owner${index}, name: $name${index}) {
|
|
37341
|
+
target: object(expression: $rev${index}) {${COMMIT_FIELDS}
|
|
37342
|
+
}
|
|
37343
|
+
}`);
|
|
37344
|
+
}
|
|
37345
|
+
});
|
|
37346
|
+
return { query: `query(${declarations.join(", ")}) {
|
|
37347
|
+
${selections.join("\n")}
|
|
37348
|
+
}`, variables };
|
|
37349
|
+
}
|
|
37350
|
+
function str(v) {
|
|
37351
|
+
return typeof v === "string" && v ? v : void 0;
|
|
37352
|
+
}
|
|
37353
|
+
function int(v) {
|
|
37354
|
+
return typeof v === "number" && Number.isFinite(v) && v >= 0 ? Math.trunc(v) : void 0;
|
|
37355
|
+
}
|
|
37356
|
+
function excerptBody(body) {
|
|
37357
|
+
if (!body) return "";
|
|
37358
|
+
const cleaned = body.replace(/<!--[\s\S]*?-->/g, "").trim();
|
|
37359
|
+
if (cleaned.length <= BODY_BUDGET) return cleaned;
|
|
37360
|
+
return `${cleaned.slice(0, BODY_BUDGET).trimEnd()}\u2026`;
|
|
37361
|
+
}
|
|
37362
|
+
function labelsOf(v) {
|
|
37363
|
+
const nodes = v?.nodes;
|
|
37364
|
+
if (!Array.isArray(nodes)) return [];
|
|
37365
|
+
const out = [];
|
|
37366
|
+
for (const node of nodes) {
|
|
37367
|
+
const name = str(node?.name);
|
|
37368
|
+
if (name) out.push({ name, color: str(node?.color) ?? "" });
|
|
37369
|
+
}
|
|
37370
|
+
return out;
|
|
37371
|
+
}
|
|
37372
|
+
function cardFromNode(ref, node, fetchedAt) {
|
|
37373
|
+
if (!node || typeof node !== "object") return null;
|
|
37374
|
+
const n = node;
|
|
37375
|
+
const repo = `${ref.owner}/${ref.name}`;
|
|
37376
|
+
const typename = str(n.__typename);
|
|
37377
|
+
if (typename === "Commit") {
|
|
37378
|
+
const headline = str(n.messageHeadline) ?? "";
|
|
37379
|
+
if (!headline) return null;
|
|
37380
|
+
const author2 = n.author ?? {};
|
|
37381
|
+
const user = author2.user ?? {};
|
|
37382
|
+
return {
|
|
37383
|
+
ref: ref.key,
|
|
37384
|
+
kind: "commit",
|
|
37385
|
+
repo,
|
|
37386
|
+
url: str(n.url) ?? `https://github.com/${repo}/commit/${str(n.oid) ?? ref.sha}`,
|
|
37387
|
+
title: headline,
|
|
37388
|
+
body: excerptBody(str(n.messageBody)),
|
|
37389
|
+
state: "",
|
|
37390
|
+
at: str(n.committedDate),
|
|
37391
|
+
...str(user.login) ? { authorLogin: str(user.login) } : {},
|
|
37392
|
+
...str(author2.name) ? { authorName: str(author2.name) } : {},
|
|
37393
|
+
// The LINKED ACCOUNT's avatar first. A commit's `GitActor.avatarUrl` is derived from the
|
|
37394
|
+
// committer email, so for an author with no matching account it is a Gravatar behind GitHub's
|
|
37395
|
+
// camo image proxy — a different host, and a generic silhouette rather than the person.
|
|
37396
|
+
...str(user.avatarUrl) || str(author2.avatarUrl) ? { authorAvatar: str(user.avatarUrl) ?? str(author2.avatarUrl) } : {},
|
|
37397
|
+
labels: [],
|
|
37398
|
+
...int(n.additions) !== void 0 ? { additions: int(n.additions) } : {},
|
|
37399
|
+
...int(n.deletions) !== void 0 ? { deletions: int(n.deletions) } : {},
|
|
37400
|
+
...int(n.changedFilesIfAvailable) !== void 0 ? { changedFiles: int(n.changedFilesIfAvailable) } : {},
|
|
37401
|
+
fetchedAt
|
|
37402
|
+
};
|
|
37403
|
+
}
|
|
37404
|
+
if (typename !== "Issue" && typename !== "PullRequest") return null;
|
|
37405
|
+
const title = str(n.title);
|
|
37406
|
+
if (!title) return null;
|
|
37407
|
+
const author = n.author ?? {};
|
|
37408
|
+
const state = n.isDraft === true && str(n.state)?.toUpperCase() === "OPEN" ? "DRAFT" : str(n.state)?.toUpperCase() ?? "";
|
|
37409
|
+
return {
|
|
37410
|
+
ref: ref.key,
|
|
37411
|
+
kind: typename === "Issue" ? "issue" : "pr",
|
|
37412
|
+
repo,
|
|
37413
|
+
url: str(n.url) ?? `https://github.com/${repo}/issues/${ref.number}`,
|
|
37414
|
+
title,
|
|
37415
|
+
body: excerptBody(str(n.body)),
|
|
37416
|
+
state,
|
|
37417
|
+
...str(n.stateReason) ? { stateReason: str(n.stateReason).toUpperCase() } : {},
|
|
37418
|
+
at: str(n.createdAt),
|
|
37419
|
+
...str(author.login) ? { authorLogin: str(author.login) } : {},
|
|
37420
|
+
...str(author.avatarUrl) ? { authorAvatar: str(author.avatarUrl) } : {},
|
|
37421
|
+
labels: labelsOf(n.labels),
|
|
37422
|
+
...int(n.additions) !== void 0 ? { additions: int(n.additions) } : {},
|
|
37423
|
+
...int(n.deletions) !== void 0 ? { deletions: int(n.deletions) } : {},
|
|
37424
|
+
...int(n.changedFiles) !== void 0 ? { changedFiles: int(n.changedFiles) } : {},
|
|
37425
|
+
...int(n.comments?.totalCount) !== void 0 ? { comments: int(n.comments.totalCount) } : {},
|
|
37426
|
+
fetchedAt
|
|
37427
|
+
};
|
|
37428
|
+
}
|
|
37429
|
+
function parseRefResponse(raw2, refs, fetchedAt) {
|
|
37430
|
+
const data = raw2?.data;
|
|
37431
|
+
const cards = [];
|
|
37432
|
+
const missing = [];
|
|
37433
|
+
refs.forEach((ref, index) => {
|
|
37434
|
+
const alias = data?.[`a${index}`] ?? null;
|
|
37435
|
+
const card = cardFromNode(ref, alias?.target, fetchedAt);
|
|
37436
|
+
if (card) cards.push(card);
|
|
37437
|
+
else missing.push(ref.key);
|
|
37438
|
+
});
|
|
37439
|
+
return { cards, missing };
|
|
37440
|
+
}
|
|
37441
|
+
function conciseError2(error) {
|
|
37442
|
+
const raw2 = error instanceof Error ? error.message : String(error);
|
|
37443
|
+
return raw2.trim().split(/\r?\n/, 1)[0]?.slice(0, 300) || "unknown error";
|
|
37444
|
+
}
|
|
37445
|
+
function createGithubHovercardService(deps = {}) {
|
|
37446
|
+
const getToken = deps.getToken ?? defaultGetToken;
|
|
37447
|
+
const request = deps.request ?? globalThis.fetch;
|
|
37448
|
+
const now = deps.now ?? Date.now;
|
|
37449
|
+
const cache = /* @__PURE__ */ new Map();
|
|
37450
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
37451
|
+
let tokenPromise;
|
|
37452
|
+
const token = async () => {
|
|
37453
|
+
if (!tokenPromise) {
|
|
37454
|
+
tokenPromise = getToken();
|
|
37455
|
+
tokenPromise.catch(() => {
|
|
37456
|
+
tokenPromise = void 0;
|
|
37457
|
+
});
|
|
37458
|
+
}
|
|
37459
|
+
return tokenPromise;
|
|
37460
|
+
};
|
|
37461
|
+
const isFresh = (ref, entry, refresh) => {
|
|
37462
|
+
if (isImmutableRef(ref) && entry.card) return true;
|
|
37463
|
+
if (refresh) return false;
|
|
37464
|
+
return now() - entry.at < MUTABLE_TTL_MS;
|
|
37465
|
+
};
|
|
37466
|
+
const fetchChunk = async (refs) => {
|
|
37467
|
+
const authToken = await token();
|
|
37468
|
+
const { query: query2, variables } = buildRefQuery(refs);
|
|
37469
|
+
const controller = new AbortController();
|
|
37470
|
+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS2);
|
|
37471
|
+
timeout.unref?.();
|
|
37472
|
+
try {
|
|
37473
|
+
const response = await request("https://api.github.com/graphql", {
|
|
37474
|
+
method: "POST",
|
|
37475
|
+
headers: {
|
|
37476
|
+
accept: "application/vnd.github+json",
|
|
37477
|
+
authorization: `Bearer ${authToken}`,
|
|
37478
|
+
"content-type": "application/json",
|
|
37479
|
+
"user-agent": "frizz-hovercard",
|
|
37480
|
+
"x-github-api-version": "2022-11-28"
|
|
37481
|
+
},
|
|
37482
|
+
body: JSON.stringify({ query: query2, variables }),
|
|
37483
|
+
signal: controller.signal
|
|
37484
|
+
});
|
|
37485
|
+
const text = await response.text();
|
|
37486
|
+
if (!response.ok) {
|
|
37487
|
+
if (response.status === 401 || response.status === 403) tokenPromise = void 0;
|
|
37488
|
+
throw new Error(`GitHub GraphQL returned HTTP ${response.status}`);
|
|
37489
|
+
}
|
|
37490
|
+
let body;
|
|
37491
|
+
try {
|
|
37492
|
+
body = JSON.parse(text);
|
|
37493
|
+
} catch {
|
|
37494
|
+
throw new Error("GitHub GraphQL returned malformed JSON");
|
|
37495
|
+
}
|
|
37496
|
+
const at = now();
|
|
37497
|
+
const { cards, missing } = parseRefResponse(body, refs, at);
|
|
37498
|
+
for (const card of cards) cache.set(card.ref, { card, at });
|
|
37499
|
+
for (const key of missing) cache.set(key, { missing: true, at });
|
|
37500
|
+
} finally {
|
|
37501
|
+
clearTimeout(timeout);
|
|
37502
|
+
}
|
|
37503
|
+
};
|
|
37504
|
+
return {
|
|
37505
|
+
size: () => cache.size,
|
|
37506
|
+
async preview(refs, opts = {}) {
|
|
37507
|
+
const refresh = opts.refresh === true;
|
|
37508
|
+
const parsed = /* @__PURE__ */ new Map();
|
|
37509
|
+
const missing = [];
|
|
37510
|
+
for (const raw2 of refs) {
|
|
37511
|
+
if (parsed.has(raw2)) continue;
|
|
37512
|
+
const ref = parseRef(raw2);
|
|
37513
|
+
if (ref) parsed.set(raw2, ref);
|
|
37514
|
+
else missing.push(raw2);
|
|
37515
|
+
}
|
|
37516
|
+
const wanted = [];
|
|
37517
|
+
const joins = [];
|
|
37518
|
+
for (const ref of parsed.values()) {
|
|
37519
|
+
const entry = cache.get(ref.key);
|
|
37520
|
+
if (entry && isFresh(ref, entry, refresh)) continue;
|
|
37521
|
+
const pending = inflight.get(ref.key);
|
|
37522
|
+
if (pending && !refresh) joins.push(pending);
|
|
37523
|
+
else wanted.push(ref);
|
|
37524
|
+
}
|
|
37525
|
+
let error;
|
|
37526
|
+
if (wanted.length > 0) {
|
|
37527
|
+
const chunks = [];
|
|
37528
|
+
for (let i = 0; i < wanted.length; i += MAX_REFS_PER_QUERY) chunks.push(wanted.slice(i, i + MAX_REFS_PER_QUERY));
|
|
37529
|
+
const runs = chunks.map((chunk) => {
|
|
37530
|
+
const run3 = fetchChunk(chunk);
|
|
37531
|
+
for (const ref of chunk) inflight.set(ref.key, run3);
|
|
37532
|
+
return run3.finally(() => {
|
|
37533
|
+
for (const ref of chunk) if (inflight.get(ref.key) === run3) inflight.delete(ref.key);
|
|
37534
|
+
});
|
|
37535
|
+
});
|
|
37536
|
+
const settled = await Promise.allSettled([...runs, ...joins]);
|
|
37537
|
+
const failure = settled.find((r) => r.status === "rejected");
|
|
37538
|
+
if (failure && failure.status === "rejected") error = conciseError2(failure.reason);
|
|
37539
|
+
} else if (joins.length > 0) {
|
|
37540
|
+
const settled = await Promise.allSettled(joins);
|
|
37541
|
+
const failure = settled.find((r) => r.status === "rejected");
|
|
37542
|
+
if (failure && failure.status === "rejected") error = conciseError2(failure.reason);
|
|
37543
|
+
}
|
|
37544
|
+
const cards = [];
|
|
37545
|
+
for (const ref of parsed.values()) {
|
|
37546
|
+
const entry = cache.get(ref.key);
|
|
37547
|
+
if (entry?.card) cards.push(entry.card);
|
|
37548
|
+
else if (entry?.missing) missing.push(ref.key);
|
|
37549
|
+
}
|
|
37550
|
+
return { cards, missing, ...error ? { error } : {} };
|
|
37551
|
+
}
|
|
37552
|
+
};
|
|
37553
|
+
}
|
|
37554
|
+
var REQUEST_TIMEOUT_MS2, MAX_REFS_PER_QUERY, BODY_BUDGET, MUTABLE_TTL_MS, REF_SEG, REF_PATTERN, ISSUE_FIELDS, COMMIT_FIELDS;
|
|
37555
|
+
var init_github_hovercard = __esm({
|
|
37556
|
+
"packages/server/src/github-hovercard.ts"() {
|
|
37557
|
+
"use strict";
|
|
37558
|
+
init_github_review();
|
|
37559
|
+
REQUEST_TIMEOUT_MS2 = 15e3;
|
|
37560
|
+
MAX_REFS_PER_QUERY = 25;
|
|
37561
|
+
BODY_BUDGET = 400;
|
|
37562
|
+
MUTABLE_TTL_MS = 5 * 6e4;
|
|
37563
|
+
REF_SEG = "[A-Za-z0-9][A-Za-z0-9._-]*";
|
|
37564
|
+
REF_PATTERN = new RegExp(`^(${REF_SEG})/(${REF_SEG})(?:#([1-9]\\d{0,9})|@([0-9a-f]{7,40}))$`);
|
|
37565
|
+
ISSUE_FIELDS = `
|
|
37566
|
+
... on Issue {
|
|
37567
|
+
__typename number title body state stateReason url createdAt
|
|
37568
|
+
author { login avatarUrl }
|
|
37569
|
+
labels(first: 6) { nodes { name color } }
|
|
37570
|
+
comments { totalCount }
|
|
37571
|
+
}
|
|
37572
|
+
... on PullRequest {
|
|
37573
|
+
__typename number title body state isDraft url createdAt additions deletions changedFiles
|
|
37574
|
+
author { login avatarUrl }
|
|
37575
|
+
labels(first: 6) { nodes { name color } }
|
|
37576
|
+
comments { totalCount }
|
|
37577
|
+
}`;
|
|
37578
|
+
COMMIT_FIELDS = `
|
|
37579
|
+
... on Commit {
|
|
37580
|
+
__typename oid abbreviatedOid url messageHeadline messageBody committedDate
|
|
37581
|
+
additions deletions changedFilesIfAvailable
|
|
37582
|
+
author { name avatarUrl user { login avatarUrl } }
|
|
37583
|
+
}`;
|
|
37584
|
+
}
|
|
37585
|
+
});
|
|
37586
|
+
|
|
36465
37587
|
// packages/server/src/backend/account-actions.ts
|
|
36466
37588
|
import { execFile as execFile8 } from "node:child_process";
|
|
36467
37589
|
import { promisify as promisify7 } from "node:util";
|
|
@@ -36897,7 +38019,7 @@ import { join as join32, resolve as resolve8 } from "node:path";
|
|
|
36897
38019
|
import { randomUUID as randomUUID16 } from "node:crypto";
|
|
36898
38020
|
import { basename as basename10, dirname as dirname14 } from "node:path";
|
|
36899
38021
|
import { existsSync as existsSync12, mkdirSync as mkdirSync16, rmSync as rmSync9, writeFileSync as writeFileSync12 } from "node:fs";
|
|
36900
|
-
import { homedir as
|
|
38022
|
+
import { homedir as homedir16 } from "node:os";
|
|
36901
38023
|
function validateGithubDispatchProfile(input) {
|
|
36902
38024
|
validateThreadProfile(input.backend, input.model, input.effort);
|
|
36903
38025
|
}
|
|
@@ -37056,7 +38178,7 @@ function projectCard(entry, stale) {
|
|
|
37056
38178
|
function addProjectAtPath(input) {
|
|
37057
38179
|
const typed = input.trim();
|
|
37058
38180
|
if (!typed) throw new Error("Enter a folder path.");
|
|
37059
|
-
const expanded = typed === "~" || typed.startsWith("~/") ? join32(
|
|
38181
|
+
const expanded = typed === "~" || typed.startsWith("~/") ? join32(homedir16(), typed.slice(1)) : typed;
|
|
37060
38182
|
const absolute = resolve8(expanded);
|
|
37061
38183
|
let stats;
|
|
37062
38184
|
try {
|
|
@@ -37066,7 +38188,7 @@ function addProjectAtPath(input) {
|
|
|
37066
38188
|
}
|
|
37067
38189
|
if (!stats.isDirectory()) throw new Error(`That is a file, not a folder: ${absolute}`);
|
|
37068
38190
|
const root = discoverProjectRoot(absolute);
|
|
37069
|
-
const id = ensureProjectIdFile(root,
|
|
38191
|
+
const id = ensureProjectIdFile(root, homedir16(), existingProjectId(root));
|
|
37070
38192
|
const remoteOwner = resolveProjectLabel(root)?.split("/")[0];
|
|
37071
38193
|
let registered = registerProject({ dir: root, id, remoteOwner });
|
|
37072
38194
|
if (registered.action === "duplicate") {
|
|
@@ -37139,14 +38261,18 @@ function createRouter(ctx) {
|
|
|
37139
38261
|
if (input.prompt === null || input.intervalSeconds === void 0) return null;
|
|
37140
38262
|
return input.intervalSeconds * 1e3;
|
|
37141
38263
|
}
|
|
37142
|
-
function
|
|
37143
|
-
|
|
37144
|
-
|
|
37145
|
-
|
|
37146
|
-
|
|
37147
|
-
|
|
37148
|
-
|
|
37149
|
-
|
|
38264
|
+
function armedPrWatchViews(slug) {
|
|
38265
|
+
const github = readGithubStatusBook(ctx.storage.getSetting(GITHUB_STATUS_SETTING));
|
|
38266
|
+
return ctx.storage.listPrWatches(slug, { armedOnly: true }).map((w) => {
|
|
38267
|
+
const target = `${w.owner}/${w.repo}#${w.number}`;
|
|
38268
|
+
return {
|
|
38269
|
+
id: w.id,
|
|
38270
|
+
target,
|
|
38271
|
+
state: w.state,
|
|
38272
|
+
createdAt: new Date(w.created_at).toISOString(),
|
|
38273
|
+
...github[target] ? { github: github[target] } : {}
|
|
38274
|
+
};
|
|
38275
|
+
});
|
|
37150
38276
|
}
|
|
37151
38277
|
function armedTimerViews(slug) {
|
|
37152
38278
|
return ctx.storage.listThreadTimers(slug, { armedOnly: true }).map((t) => ({
|
|
@@ -37162,16 +38288,14 @@ function createRouter(ctx) {
|
|
|
37162
38288
|
if (!row) throw new Error(`thread ${slug} is not registered`);
|
|
37163
38289
|
const otherOn = trigger === "rest" ? row.recurring_on_schedule === 1 : row.recurring_on_rest === 1;
|
|
37164
38290
|
const compactOn = row.recurring_on_compact === 1;
|
|
37165
|
-
const holdOn = row.recurring_pause_on_questions === 1;
|
|
37166
38291
|
const storedSeconds = row.recurring_interval_ms ? Math.round(row.recurring_interval_ms / 1e3) : void 0;
|
|
37167
38292
|
const on = write.enabled && write.prompt !== null;
|
|
37168
38293
|
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
|
|
38294
|
+
const next = prompt === null ? { prompt: null, stopHook: false, heartbeat: false, postCompaction: false } : {
|
|
37170
38295
|
prompt,
|
|
37171
38296
|
stopHook: trigger === "rest" ? on : otherOn,
|
|
37172
38297
|
heartbeat: trigger === "schedule" ? on : otherOn,
|
|
37173
38298
|
postCompaction: compactOn,
|
|
37174
|
-
pauseOnQuestions: holdOn,
|
|
37175
38299
|
intervalSeconds: trigger === "schedule" && on ? write.intervalSeconds : storedSeconds
|
|
37176
38300
|
};
|
|
37177
38301
|
assertRecurringPromptArmable(next, row);
|
|
@@ -37180,7 +38304,6 @@ function createRouter(ctx) {
|
|
|
37180
38304
|
stopHook: next.stopHook,
|
|
37181
38305
|
heartbeat: next.heartbeat,
|
|
37182
38306
|
postCompaction: next.postCompaction,
|
|
37183
|
-
pauseOnQuestions: next.pauseOnQuestions,
|
|
37184
38307
|
intervalMs: recurringIntervalMs(next),
|
|
37185
38308
|
armedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
37186
38309
|
})) {
|
|
@@ -38004,7 +39127,6 @@ function createRouter(ctx) {
|
|
|
38004
39127
|
stopHook: input.stopHook,
|
|
38005
39128
|
heartbeat: input.heartbeat,
|
|
38006
39129
|
postCompaction: input.postCompaction,
|
|
38007
|
-
pauseOnQuestions: input.pauseOnQuestions,
|
|
38008
39130
|
intervalMs: recurringIntervalMs(input),
|
|
38009
39131
|
armedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
38010
39132
|
})) {
|
|
@@ -38034,7 +39156,6 @@ function createRouter(ctx) {
|
|
|
38034
39156
|
stopHook: input.stopHook,
|
|
38035
39157
|
heartbeat: input.heartbeat,
|
|
38036
39158
|
postCompaction: input.postCompaction,
|
|
38037
|
-
pauseOnQuestions: input.pauseOnQuestions,
|
|
38038
39159
|
intervalMs: recurringIntervalMs(input),
|
|
38039
39160
|
armedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
38040
39161
|
})) {
|
|
@@ -38111,56 +39232,106 @@ function createRouter(ctx) {
|
|
|
38111
39232
|
output: OwnThreadTimersResult,
|
|
38112
39233
|
handler: async ({ input }) => ({ timers: armedTimerViews(input.slug) })
|
|
38113
39234
|
}),
|
|
38114
|
-
//
|
|
38115
|
-
//
|
|
38116
|
-
//
|
|
38117
|
-
//
|
|
38118
|
-
|
|
38119
|
-
|
|
38120
|
-
|
|
39235
|
+
// EVERY kind of background work this thread has out, with the id its awaiting fence names it by.
|
|
39236
|
+
//
|
|
39237
|
+
// The fence is structural — it references live things by id — so a worker that no longer has those
|
|
39238
|
+
// ids to hand cannot write a correct fence at all, and the failure is silent: it names something
|
|
39239
|
+
// wrong, frizz refuses the park, and the thread queues. That is the exact stall the grammar exists
|
|
39240
|
+
// to prevent, so the ids have to be RETRIEVABLE rather than remembered. Same list the sign-off nudge
|
|
39241
|
+
// prints, from the same source, so the two can never tell a worker different things.
|
|
39242
|
+
//
|
|
39243
|
+
// A shell is keyed by its RUNTIME task id ("Command running in background with ID: bzvtnt3ig"),
|
|
39244
|
+
// never its launch tool_use id: that is the string the worker was actually shown and the one it will
|
|
39245
|
+
// reach for. Falls back to the launch id only when the ack has not landed yet.
|
|
39246
|
+
listOwnThreadActivity: mutation({
|
|
39247
|
+
input: ListOwnThreadActivityInput,
|
|
39248
|
+
output: OwnThreadActivityResult,
|
|
39249
|
+
handler: async ({ input }) => {
|
|
39250
|
+
const tele = ctx.tailer.get(input.slug);
|
|
39251
|
+
const activity = [];
|
|
39252
|
+
for (const sh of tele?.bgShells ?? []) {
|
|
39253
|
+
if (sh.state !== "running") continue;
|
|
39254
|
+
const id = sh.taskId ?? sh.id;
|
|
39255
|
+
if (id) activity.push({ kind: "shell", id, label: sh.label, since: sh.startedAt });
|
|
39256
|
+
}
|
|
39257
|
+
for (const a of tele?.subAgents ?? []) {
|
|
39258
|
+
if (a.state !== "running") continue;
|
|
39259
|
+
if (a.id) activity.push({ kind: "agent", id: a.id, label: a.label, since: a.startedAt });
|
|
39260
|
+
}
|
|
39261
|
+
for (const t of ctx.storage.listThreadTimers(input.slug, { armedOnly: true })) {
|
|
39262
|
+
activity.push({
|
|
39263
|
+
kind: "timer",
|
|
39264
|
+
id: t.id,
|
|
39265
|
+
label: t.prompt.trim().replace(/\s+/g, " ").slice(0, 120),
|
|
39266
|
+
since: new Date(t.created_at).toISOString(),
|
|
39267
|
+
until: new Date(t.fire_at).toISOString()
|
|
39268
|
+
});
|
|
39269
|
+
}
|
|
39270
|
+
for (const w of ctx.storage.listPrWatches(input.slug, { armedOnly: true })) {
|
|
39271
|
+
activity.push({
|
|
39272
|
+
kind: "pr",
|
|
39273
|
+
id: `${w.owner}/${w.repo}#${w.number}`,
|
|
39274
|
+
label: `${w.owner}/${w.repo}#${w.number}`,
|
|
39275
|
+
since: new Date(w.created_at).toISOString()
|
|
39276
|
+
});
|
|
39277
|
+
}
|
|
39278
|
+
return { activity };
|
|
39279
|
+
}
|
|
39280
|
+
}),
|
|
39281
|
+
// THE WATCHER REGISTRY IS GONE (2026-08-14). `mcp__frizz__watch` and its four procedures are
|
|
39282
|
+
// deleted rather than shimmed: a wait is now a `watch:` line in the worker's own ```awaiting fence,
|
|
39283
|
+
// which is BOTH the park and the wake (scheduler `watchVerdict`), so there is nothing to register and
|
|
39284
|
+
// nothing an alias could usefully do. A session dispatched before the change still holds the old MCP
|
|
39285
|
+
// binary and will get a 404 from it — which is the honest answer, and the tool it should not be
|
|
39286
|
+
// calling is the only casualty.
|
|
39287
|
+
// ---- REGISTERED PR WATCHERS (add / drop / list) ---------------------------------------------
|
|
39288
|
+
// The worker's own PR watchers, from `mcp__frizz__watch_pr`. Same caller and therefore the same rules
|
|
39289
|
+
// as the timers above: slug-only (the MCP server outlives the session ids underneath it), and no
|
|
39290
|
+
// thread parameter a model could aim elsewhere.
|
|
39291
|
+
addOwnPrWatch: mutation({
|
|
39292
|
+
input: AddOwnPrWatchInput,
|
|
39293
|
+
output: AddOwnPrWatchResult,
|
|
38121
39294
|
handler: async ({ input }) => {
|
|
38122
39295
|
const row = ctx.storage.getSession(input.slug);
|
|
38123
39296
|
if (!row) throw new Error(`thread ${input.slug} is not registered`);
|
|
38124
39297
|
if (row.state === "archived" || row.archived === 1) {
|
|
38125
39298
|
throw new Error("Reopen this thread before registering a watcher on it");
|
|
38126
39299
|
}
|
|
38127
|
-
const
|
|
38128
|
-
|
|
38129
|
-
|
|
38130
|
-
if (armed.length >= WATCH_MAX_ARMED) {
|
|
38131
|
-
throw new Error(`this thread already has ${armed.length} armed watchers (the limit is ${WATCH_MAX_ARMED}) \u2014 drop one first`);
|
|
39300
|
+
const ref = parsePrRef(input.target);
|
|
39301
|
+
if (!ref) {
|
|
39302
|
+
throw new Error(`\`${input.target}\` is not a pull request I can watch \u2014 give owner/repo#123 or a PR URL`);
|
|
38132
39303
|
}
|
|
38133
|
-
const
|
|
38134
|
-
|
|
39304
|
+
const armed = ctx.storage.listPrWatches(input.slug, { armedOnly: true });
|
|
39305
|
+
const existing = armed.find((w) => w.owner === ref.owner && w.repo === ref.repo && w.number === ref.number);
|
|
39306
|
+
const target = `${ref.owner}/${ref.repo}#${ref.number}`;
|
|
39307
|
+
if (existing) return { id: existing.id, target, alreadyArmed: true, watches: armedPrWatchViews(input.slug) };
|
|
39308
|
+
if (armed.length >= PR_WATCH_MAX_ARMED) {
|
|
39309
|
+
throw new Error(`this thread already watches ${armed.length} pull requests (the limit is ${PR_WATCH_MAX_ARMED}) \u2014 drop one first`);
|
|
39310
|
+
}
|
|
39311
|
+
const forMs = input.for === void 0 ? PR_WATCH_DEFAULT_FOR_MS : parseAwaitingDuration(input.for);
|
|
39312
|
+
if (forMs === null) {
|
|
39313
|
+
throw new Error(`\`for: ${input.for}\` is not a duration \u2014 give one like \`30m\`, \`2h\` or \`3d\` (max 24h)`);
|
|
39314
|
+
}
|
|
39315
|
+
const now = Date.now();
|
|
39316
|
+
const id = `prw_${randomUUID16().replace(/-/g, "").slice(0, 12)}`;
|
|
39317
|
+
ctx.storage.armPrWatch({ id, slug: input.slug, owner: ref.owner, repo: ref.repo, number: ref.number, createdAtMs: now, expiresAtMs: now + forMs });
|
|
38135
39318
|
ctx.board.refresh();
|
|
38136
|
-
return { id, alreadyArmed: false, watches:
|
|
39319
|
+
return { id, target, alreadyArmed: false, watches: armedPrWatchViews(input.slug) };
|
|
38137
39320
|
}
|
|
38138
39321
|
}),
|
|
38139
|
-
|
|
38140
|
-
input:
|
|
38141
|
-
output:
|
|
39322
|
+
dropOwnPrWatch: mutation({
|
|
39323
|
+
input: DropOwnPrWatchInput,
|
|
39324
|
+
output: DropOwnPrWatchResult,
|
|
38142
39325
|
handler: async ({ input }) => {
|
|
38143
|
-
const dropped = ctx.storage.
|
|
39326
|
+
const dropped = ctx.storage.dropPrWatch(input.slug, input.id, Date.now());
|
|
38144
39327
|
if (dropped) ctx.board.refresh();
|
|
38145
|
-
return { dropped, watches:
|
|
38146
|
-
}
|
|
38147
|
-
}),
|
|
38148
|
-
// Hand a foreground watch back to the scheduler. Called by the blocking tool call when its timeout
|
|
38149
|
-
// expires, so a wait the worker started degrades into a durable one rather than being lost — which
|
|
38150
|
-
// is the property that makes choosing the blocking mode safe.
|
|
38151
|
-
promoteOwnThreadWatch: mutation({
|
|
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) };
|
|
39328
|
+
return { dropped, watches: armedPrWatchViews(input.slug) };
|
|
38158
39329
|
}
|
|
38159
39330
|
}),
|
|
38160
|
-
|
|
38161
|
-
input:
|
|
38162
|
-
output:
|
|
38163
|
-
handler: async ({ input }) => ({ watches:
|
|
39331
|
+
listOwnPrWatches: mutation({
|
|
39332
|
+
input: ListOwnPrWatchesInput,
|
|
39333
|
+
output: OwnPrWatchesResult,
|
|
39334
|
+
handler: async ({ input }) => ({ watches: armedPrWatchViews(input.slug) })
|
|
38164
39335
|
}),
|
|
38165
39336
|
// The SUPERSEDED worker procedures, aliased onto the row above — see SetOwnThreadStopHookInput for
|
|
38166
39337
|
// why they cannot simply be deleted. A worker's MCP server outlives every server restart, so these
|
|
@@ -38238,10 +39409,7 @@ function createRouter(ctx) {
|
|
|
38238
39409
|
if (!fenceAt || !Number.isFinite(Date.parse(fenceAt)) || fenceAt !== input.fenceAt) {
|
|
38239
39410
|
throw new Error("This awaiting proposal changed before it could be confirmed");
|
|
38240
39411
|
}
|
|
38241
|
-
const snoozedUntil =
|
|
38242
|
-
if (hint.kind === "timer" && !snoozedUntil) {
|
|
38243
|
-
throw new Error("This awaiting proposal changed before it could be confirmed");
|
|
38244
|
-
}
|
|
39412
|
+
const snoozedUntil = null;
|
|
38245
39413
|
if (snoozedUntil && Date.parse(snoozedUntil) <= Date.now()) {
|
|
38246
39414
|
throw new Error("This scheduled time has already passed");
|
|
38247
39415
|
}
|
|
@@ -38738,12 +39906,12 @@ function createRouter(ctx) {
|
|
|
38738
39906
|
output: DispatchPreferences,
|
|
38739
39907
|
handler: async ({ input }) => setDispatchPreference(ctx.storage, ctx.getSettings(), input, readCodexModels())
|
|
38740
39908
|
}),
|
|
38741
|
-
// The shipped GitHub batch-dispatch prompt
|
|
38742
|
-
// The Settings UI reads
|
|
38743
|
-
//
|
|
39909
|
+
// The shipped GitHub batch-dispatch prompt template (single source of truth: server/github.ts).
|
|
39910
|
+
// The Settings UI reads it to prefill the editor and to power "reset to default"; an empty/unset
|
|
39911
|
+
// githubPrompt setting means the server uses exactly this. One template serves issues AND PRs.
|
|
38744
39912
|
githubPromptDefaults: query({
|
|
38745
|
-
output: external_exports.object({
|
|
38746
|
-
handler: async () => ({
|
|
39913
|
+
output: external_exports.object({ prompt: external_exports.string() }),
|
|
39914
|
+
handler: async () => ({ prompt: DEFAULT_GITHUB_PROMPT })
|
|
38747
39915
|
}),
|
|
38748
39916
|
// ---- GitHub-first batch dispatch ----
|
|
38749
39917
|
// gh availability: installed (cache-warmed resolveInstalled) + inRepo/nameWithOwner (cache-warmed
|
|
@@ -38775,6 +39943,18 @@ function createRouter(ctx) {
|
|
|
38775
39943
|
return await listItems(repo, input.kind, input.sort, input.page, input.perPage);
|
|
38776
39944
|
}
|
|
38777
39945
|
}),
|
|
39946
|
+
// Hovercard data for every GitHub reference the client autolinked into the prose on screen —
|
|
39947
|
+
// ONE request for a whole page of refs, answered from a process-lifetime cache (see
|
|
39948
|
+
// github-hovercard.ts). The client asks as the prose renders, so the hover itself never waits on
|
|
39949
|
+
// the network; `refresh` is its revalidation of the handful it is actually pointing at.
|
|
39950
|
+
//
|
|
39951
|
+
// Never throws: a missing gh, an unauthenticated one, a rate limit and a network stall all come
|
|
39952
|
+
// back as `error` with whatever cards the cache already holds, and the anchor stays a plain link.
|
|
39953
|
+
githubRefPreview: query({
|
|
39954
|
+
input: GithubRefPreviewInput,
|
|
39955
|
+
output: GithubRefPreviewResult,
|
|
39956
|
+
handler: async ({ input }) => await githubHovercards.preview(input.refs, { refresh: input.refresh })
|
|
39957
|
+
}),
|
|
38778
39958
|
// Spin up one frizz thread per checked item: hydrate each fresh from gh, template a server-side
|
|
38779
39959
|
// prompt (single source of truth, unit-tested), then REUSE ctx.dispatcher.dispatch (no new spawn
|
|
38780
39960
|
// logic). SEQUENTIAL — a burst of 20 concurrent tmux spawns would hammer the box (risk 5). A
|
|
@@ -38786,14 +39966,13 @@ function createRouter(ctx) {
|
|
|
38786
39966
|
validateGithubDispatchProfile(input);
|
|
38787
39967
|
const repo = await resolveRepo();
|
|
38788
39968
|
if (!repo) throw new Error("not a GitHub repo");
|
|
38789
|
-
const
|
|
39969
|
+
const template = effectiveTemplate(ctx.getSettings().githubPrompt);
|
|
38790
39970
|
const dispatched = [];
|
|
38791
39971
|
const failed = [];
|
|
38792
39972
|
for (const it of input.items) {
|
|
38793
39973
|
try {
|
|
38794
39974
|
const title = `${it.kind === "issue" ? "Investigate" : "Review"} ${repo}#${it.number}`;
|
|
38795
39975
|
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
39976
|
const hydrated = it.kind === "issue" ? await hydrateIssue(repo, it.number) : await hydratePr(repo, it.number);
|
|
38798
39977
|
const prompt = renderGithubPrompt(template, repo, hydrated, slug, it.kind);
|
|
38799
39978
|
const request = githubDispatcherRequest(input, { prompt, title, slug });
|
|
@@ -38808,7 +39987,7 @@ function createRouter(ctx) {
|
|
|
38808
39987
|
})
|
|
38809
39988
|
};
|
|
38810
39989
|
}
|
|
38811
|
-
var SlugInput, cachedLivenessTerminator, HOLD_OPS_MAX, HOLD_LABEL_MAX;
|
|
39990
|
+
var SlugInput, cachedLivenessTerminator, HOLD_OPS_MAX, HOLD_LABEL_MAX, githubHovercards;
|
|
38812
39991
|
var init_router5 = __esm({
|
|
38813
39992
|
"packages/server/src/router.ts"() {
|
|
38814
39993
|
"use strict";
|
|
@@ -38826,6 +40005,7 @@ var init_router5 = __esm({
|
|
|
38826
40005
|
init_local_file();
|
|
38827
40006
|
init_project();
|
|
38828
40007
|
init_github();
|
|
40008
|
+
init_github_hovercard();
|
|
38829
40009
|
init_dispatch();
|
|
38830
40010
|
init_codex_models();
|
|
38831
40011
|
init_codex();
|
|
@@ -38860,6 +40040,7 @@ var init_router5 = __esm({
|
|
|
38860
40040
|
};
|
|
38861
40041
|
HOLD_OPS_MAX = 8;
|
|
38862
40042
|
HOLD_LABEL_MAX = 100;
|
|
40043
|
+
githubHovercards = createGithubHovercardService();
|
|
38863
40044
|
}
|
|
38864
40045
|
});
|
|
38865
40046
|
|
|
@@ -38999,6 +40180,52 @@ var init_local_origin = __esm({
|
|
|
38999
40180
|
}
|
|
39000
40181
|
});
|
|
39001
40182
|
|
|
40183
|
+
// packages/server/src/compression.ts
|
|
40184
|
+
import { brotliCompressSync, gzipSync, constants as constants3 } from "node:zlib";
|
|
40185
|
+
function negotiateEncoding(acceptEncoding) {
|
|
40186
|
+
if (!acceptEncoding) return null;
|
|
40187
|
+
const offers = /* @__PURE__ */ new Map();
|
|
40188
|
+
for (const part of acceptEncoding.split(",")) {
|
|
40189
|
+
const [name, ...params] = part.trim().split(";");
|
|
40190
|
+
if (!name) continue;
|
|
40191
|
+
const q = params.map((p) => /^\s*q=([0-9.]+)\s*$/i.exec(p)).find(Boolean);
|
|
40192
|
+
offers.set(name.trim().toLowerCase(), q ? Number(q[1]) : 1);
|
|
40193
|
+
}
|
|
40194
|
+
const wildcard = offers.get("*");
|
|
40195
|
+
const accepted = (name) => {
|
|
40196
|
+
const explicit = offers.get(name);
|
|
40197
|
+
if (explicit !== void 0) return explicit > 0;
|
|
40198
|
+
return wildcard !== void 0 && wildcard > 0;
|
|
40199
|
+
};
|
|
40200
|
+
if (accepted("br")) return "br";
|
|
40201
|
+
if (accepted("gzip")) return "gzip";
|
|
40202
|
+
return null;
|
|
40203
|
+
}
|
|
40204
|
+
function compress(body, encoding) {
|
|
40205
|
+
const out = encoding === "br" ? brotliCompressSync(body, {
|
|
40206
|
+
params: {
|
|
40207
|
+
[constants3.BROTLI_PARAM_QUALITY]: 4,
|
|
40208
|
+
// Telling brotli the input size lets it size its window instead of guessing.
|
|
40209
|
+
[constants3.BROTLI_PARAM_SIZE_HINT]: body.byteLength
|
|
40210
|
+
}
|
|
40211
|
+
}) : gzipSync(body, { level: 6 });
|
|
40212
|
+
return Uint8Array.from(out);
|
|
40213
|
+
}
|
|
40214
|
+
function shouldCompress(headers, byteLength2) {
|
|
40215
|
+
if (byteLength2 < MIN_COMPRESS_BYTES) return false;
|
|
40216
|
+
if (headers.has("content-encoding")) return false;
|
|
40217
|
+
const type = headers.get("content-type") ?? "";
|
|
40218
|
+
if (type.includes("text/event-stream")) return false;
|
|
40219
|
+
return /json|text\/|javascript|xml|svg/i.test(type);
|
|
40220
|
+
}
|
|
40221
|
+
var MIN_COMPRESS_BYTES;
|
|
40222
|
+
var init_compression = __esm({
|
|
40223
|
+
"packages/server/src/compression.ts"() {
|
|
40224
|
+
"use strict";
|
|
40225
|
+
MIN_COMPRESS_BYTES = 1024;
|
|
40226
|
+
}
|
|
40227
|
+
});
|
|
40228
|
+
|
|
39002
40229
|
// packages/server/src/local-image.ts
|
|
39003
40230
|
import { readFileSync as readFileSync22, realpathSync as realpathSync9, statSync as statSync18 } from "node:fs";
|
|
39004
40231
|
import { extname as extname2, isAbsolute as isAbsolute4 } from "node:path";
|
|
@@ -39035,7 +40262,7 @@ var init_local_image = __esm({
|
|
|
39035
40262
|
|
|
39036
40263
|
// packages/server/src/project-icon.ts
|
|
39037
40264
|
import { readFileSync as readFileSync23, readdirSync as readdirSync11, realpathSync as realpathSync10, statSync as statSync19 } from "node:fs";
|
|
39038
|
-
import { homedir as
|
|
40265
|
+
import { homedir as homedir17 } from "node:os";
|
|
39039
40266
|
import { basename as basename11, dirname as dirname15, extname as extname3, join as join33, relative, sep as sep2 } from "node:path";
|
|
39040
40267
|
function normalizeStem(stem) {
|
|
39041
40268
|
const sized = stem.toLowerCase().replace(/[@-]\d+(\.\d+)?x(\d+)?$/u, "").replace(/[-_]\d{2,4}$/u, "");
|
|
@@ -39174,7 +40401,7 @@ function iconMediaType(path) {
|
|
|
39174
40401
|
}
|
|
39175
40402
|
function resolveProjectIconResponse(id, options = {}) {
|
|
39176
40403
|
if (!id) return { status: 400 };
|
|
39177
|
-
const home = options.home ??
|
|
40404
|
+
const home = options.home ?? homedir17();
|
|
39178
40405
|
const entry = readRegistry(home).projects.find((project) => project.id === id);
|
|
39179
40406
|
if (!entry) return { status: 404 };
|
|
39180
40407
|
const path = resolveProjectIcon(id, (root) => detectProjectIcon(root)?.path, { home });
|
|
@@ -39502,6 +40729,21 @@ function createApp(ctx, options = {}) {
|
|
|
39502
40729
|
c.header("x-frizz-boot", ctx.bootId);
|
|
39503
40730
|
await next();
|
|
39504
40731
|
});
|
|
40732
|
+
app.use(`${frizzRoute("/rpc")}/*`, async (c, next) => {
|
|
40733
|
+
await next();
|
|
40734
|
+
const encoding = negotiateEncoding(c.req.header("accept-encoding"));
|
|
40735
|
+
if (!encoding || !c.res.body) return;
|
|
40736
|
+
const body = new Uint8Array(await c.res.arrayBuffer());
|
|
40737
|
+
if (!shouldCompress(c.res.headers, body.byteLength)) {
|
|
40738
|
+
c.res = new Response(body, { status: c.res.status, headers: c.res.headers });
|
|
40739
|
+
return;
|
|
40740
|
+
}
|
|
40741
|
+
const headers = new Headers(c.res.headers);
|
|
40742
|
+
headers.set("content-encoding", encoding);
|
|
40743
|
+
headers.set("vary", "Accept-Encoding");
|
|
40744
|
+
headers.delete("content-length");
|
|
40745
|
+
c.res = new Response(compress(body, encoding), { status: c.res.status, headers });
|
|
40746
|
+
});
|
|
39505
40747
|
app.get(frizzRoute("/health"), (c) => c.json({
|
|
39506
40748
|
ok: true,
|
|
39507
40749
|
projectId: ctx.project.id,
|
|
@@ -39608,6 +40850,7 @@ var init_app = __esm({
|
|
|
39608
40850
|
init_src();
|
|
39609
40851
|
init_router5();
|
|
39610
40852
|
init_local_origin();
|
|
40853
|
+
init_compression();
|
|
39611
40854
|
init_local_image();
|
|
39612
40855
|
init_project_icon();
|
|
39613
40856
|
init_local_visualization();
|
|
@@ -40071,6 +41314,11 @@ function createAppSocketServer(deps) {
|
|
|
40071
41314
|
const bufferedAmount = deps.bufferedAmount ?? ((ws) => ws.bufferedAmount);
|
|
40072
41315
|
const now = deps.now ?? Date.now;
|
|
40073
41316
|
const serializeMessage = deps.serializeMessage ?? JSON.stringify;
|
|
41317
|
+
const scheduleSubscriptionFlush = deps.scheduleSubscriptionFlush ?? ((flush) => {
|
|
41318
|
+
const immediate = setImmediate(flush);
|
|
41319
|
+
immediate.unref?.();
|
|
41320
|
+
return () => clearImmediate(immediate);
|
|
41321
|
+
});
|
|
40074
41322
|
const terminationTimers = /* @__PURE__ */ new WeakMap();
|
|
40075
41323
|
const keyframes = /* @__PURE__ */ new Set();
|
|
40076
41324
|
let closing = false;
|
|
@@ -40307,7 +41555,33 @@ function createAppSocketServer(deps) {
|
|
|
40307
41555
|
scheduleTranscriptFlush(BACKPRESSURE_RETRY_MS);
|
|
40308
41556
|
}
|
|
40309
41557
|
const pendingSubscriptionPushes = /* @__PURE__ */ new Map();
|
|
40310
|
-
let
|
|
41558
|
+
let subscriptionFlushArmed = false;
|
|
41559
|
+
let cancelSubscriptionFlush = null;
|
|
41560
|
+
function flushSubscriptionPushes() {
|
|
41561
|
+
subscriptionFlushArmed = false;
|
|
41562
|
+
cancelSubscriptionFlush = null;
|
|
41563
|
+
if (closing) {
|
|
41564
|
+
pendingSubscriptionPushes.clear();
|
|
41565
|
+
return;
|
|
41566
|
+
}
|
|
41567
|
+
const batch = [...pendingSubscriptionPushes];
|
|
41568
|
+
pendingSubscriptionPushes.clear();
|
|
41569
|
+
const batchResults = /* @__PURE__ */ new Map();
|
|
41570
|
+
for (const [candidate, candidateSlugs] of batch) {
|
|
41571
|
+
if (candidate.readyState !== candidate.OPEN) continue;
|
|
41572
|
+
for (const candidateSlug of candidateSlugs) {
|
|
41573
|
+
if (!registry.isSubscribed(candidate, candidateSlug)) continue;
|
|
41574
|
+
let result = batchResults.get(candidateSlug);
|
|
41575
|
+
if (!result || result.kind === "limited" && result.scope === "origin") {
|
|
41576
|
+
result = snapshotFor(candidateSlug, socketOrigins.get(candidate));
|
|
41577
|
+
if (!(result.kind === "limited" && result.scope === "origin")) {
|
|
41578
|
+
batchResults.set(candidateSlug, result);
|
|
41579
|
+
}
|
|
41580
|
+
}
|
|
41581
|
+
pushSnapshotResult(candidate, candidateSlug, result);
|
|
41582
|
+
}
|
|
41583
|
+
}
|
|
41584
|
+
}
|
|
40311
41585
|
function enqueueSubscriptionPush(ws, slug) {
|
|
40312
41586
|
deleteTranscriptSnapshot(slug);
|
|
40313
41587
|
let slugs = pendingSubscriptionPushes.get(ws);
|
|
@@ -40316,32 +41590,10 @@ function createAppSocketServer(deps) {
|
|
|
40316
41590
|
pendingSubscriptionPushes.set(ws, slugs);
|
|
40317
41591
|
}
|
|
40318
41592
|
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?.();
|
|
41593
|
+
if (subscriptionFlushArmed) return;
|
|
41594
|
+
subscriptionFlushArmed = true;
|
|
41595
|
+
const cancel = scheduleSubscriptionFlush(flushSubscriptionPushes);
|
|
41596
|
+
if (subscriptionFlushArmed) cancelSubscriptionFlush = cancel ?? null;
|
|
40345
41597
|
}
|
|
40346
41598
|
function cancelPendingSubscriptionPush(ws, slug) {
|
|
40347
41599
|
if (slug === void 0) {
|
|
@@ -40643,8 +41895,12 @@ function createAppSocketServer(deps) {
|
|
|
40643
41895
|
offTranscript();
|
|
40644
41896
|
} catch {
|
|
40645
41897
|
}
|
|
40646
|
-
|
|
40647
|
-
|
|
41898
|
+
try {
|
|
41899
|
+
cancelSubscriptionFlush?.();
|
|
41900
|
+
} catch {
|
|
41901
|
+
}
|
|
41902
|
+
cancelSubscriptionFlush = null;
|
|
41903
|
+
subscriptionFlushArmed = false;
|
|
40648
41904
|
pendingSubscriptionPushes.clear();
|
|
40649
41905
|
if (transcriptRefreshTimer) clearTimeout(transcriptRefreshTimer);
|
|
40650
41906
|
transcriptRefreshTimer = null;
|