transitions-refine 0.3.14 → 0.3.16
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 +2 -0
- package/demo.html +49 -28
- package/package.json +1 -1
- package/server/relay.mjs +93 -12
package/README.md
CHANGED
|
@@ -98,6 +98,8 @@ Like Replace, Accept needs the agent — run `/refine live` (or `--llm` / `REFIN
|
|
|
98
98
|
| `REFINE_RELAY_PORT` | `7331` | relay port |
|
|
99
99
|
| `REFINE_AGENT_CMD` | — | one-shot LLM CLI the relay spawns per job |
|
|
100
100
|
| `REFINE_AGENT_TIMEOUT_MS` | `120000` | per-run timeout |
|
|
101
|
+
| `REFINE_SCAN_MODEL` | `composer-2.5-fast` | cursor-agent model for the initial group scan (`""` = agent default) |
|
|
102
|
+
| `REFINE_SCAN_EFFORT` | `low` | Codex reasoning effort for the scan — `minimal\|low\|medium\|high` (`""` = configured effort) |
|
|
101
103
|
| `REFINE_AUTO=0` | — | disable auto-answer and wait for an external poller |
|
|
102
104
|
| `window.REFINE_RELAY_URL` | injected origin | browser override for the relay URL |
|
|
103
105
|
|
package/demo.html
CHANGED
|
@@ -1437,32 +1437,49 @@
|
|
|
1437
1437
|
.tl-scan-note .tl-dotm { width: 14px; height: 14px; }
|
|
1438
1438
|
.tl-scan-error { color: var(--c-warn, #c2410c); }
|
|
1439
1439
|
.tl-scan-error svg { flex: none; }
|
|
1440
|
-
/* /refine live chip —
|
|
1440
|
+
/* /refine live chip — Figma icons 594:22938. Live = green chip + green Stop
|
|
1441
|
+
pill; Live Off = gray chip, no button. Tooltip warns the chat-loop mode
|
|
1442
|
+
bills credits while idle. */
|
|
1441
1443
|
.tl-live-wrap { display: inline-flex; }
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1444
|
+
/* LIVE (active) — node 594:22828 */
|
|
1445
|
+
.tl-live-chip { box-sizing: border-box; display: inline-flex; align-items: center;
|
|
1446
|
+
justify-content: space-between; width: 94px; height: 28px; padding: 0 2px 0 9px;
|
|
1447
|
+
border-radius: 36px; cursor: help;
|
|
1448
|
+
background: rgba(132,211,125,0.1);
|
|
1449
|
+
box-shadow: 0 1px 3px 0 rgba(0,220,37,0.06),
|
|
1450
|
+
inset 0 0 0 1px rgba(48,124,21,0.06), inset 0 -1px 0 0 rgba(0,85,9,0.08);
|
|
1447
1451
|
/* mount: token dropdown-open feel — 250ms Fast, scale 0.97, smooth ease out */
|
|
1448
1452
|
animation: tl-live-in 250ms cubic-bezier(0.22,1,0.36,1) both; }
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
.tl-live-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
.tl-live-dot { width: 7px; height: 7px; border-radius: 50%;
|
|
1455
|
-
background: currentColor; flex: none;
|
|
1453
|
+
.tl-live-main { display: inline-flex; align-items: center; gap: 5px; flex: none; }
|
|
1454
|
+
.tl-live-label { font-size: 12px; font-weight: 400; line-height: 14px; color: #18a32f; white-space: nowrap; }
|
|
1455
|
+
html[data-theme="dark"] .tl-live-label { color: #4ade80; }
|
|
1456
|
+
.tl-live-dot { width: 8px; height: 8px; border-radius: 50%; flex: none;
|
|
1457
|
+
background: #34CC20; box-shadow: inset 0 0 0 0.5px rgba(0,182,30,0.55);
|
|
1456
1458
|
animation: tl-live-pulse 1600ms linear infinite; }
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1459
|
+
/* Stop pill — node 594:22874 (default / hover+active = Variant2/3) */
|
|
1460
|
+
.tl-live-stop { all: unset; box-sizing: border-box; cursor: pointer; flex: none;
|
|
1461
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
1462
|
+
height: 24px; padding: 0 8px; border-radius: 36px;
|
|
1463
|
+
font-size: 12px; font-weight: 500; line-height: 14px; color: #118c25;
|
|
1464
|
+
background: rgba(68,167,84,0.1);
|
|
1465
|
+
box-shadow: inset 0 0 0 1px rgba(18,130,28,0.04);
|
|
1466
|
+
transition: background 150ms cubic-bezier(0.22,1,0.36,1),
|
|
1467
|
+
color 150ms cubic-bezier(0.22,1,0.36,1); }
|
|
1468
|
+
.tl-live-stop:hover,
|
|
1469
|
+
.tl-live-stop:active { background: rgba(68,167,84,0.2); color: #057217; }
|
|
1470
|
+
html[data-theme="dark"] .tl-live-stop { color: #4ade80; }
|
|
1471
|
+
html[data-theme="dark"] .tl-live-stop:hover,
|
|
1472
|
+
html[data-theme="dark"] .tl-live-stop:active { color: #86efac; }
|
|
1473
|
+
.tl-live-stop:focus-visible { outline: 2px solid #118c25; outline-offset: 1px; }
|
|
1474
|
+
/* LIVE OFF — node 594:22912 (gray, no button) */
|
|
1475
|
+
.tl-live-chip.is-stopped { width: auto; justify-content: flex-start; padding: 0 9px;
|
|
1476
|
+
background: rgba(102,102,102,0.04);
|
|
1477
|
+
box-shadow: 0 1px 3px 0 rgba(0,0,0,0.02),
|
|
1478
|
+
inset 0 0 0 1px rgba(77,77,77,0.1), inset 0 -1px 0 0 rgba(0,0,0,0.04); }
|
|
1479
|
+
.tl-live-chip.is-stopped .tl-live-dot { background: #A1A1A1;
|
|
1480
|
+
box-shadow: inset 0 0 0 0.5px rgba(146,146,146,0.6); animation: none; }
|
|
1481
|
+
.tl-live-chip.is-stopped .tl-live-label { color: #525252; }
|
|
1482
|
+
html[data-theme="dark"] .tl-live-chip.is-stopped .tl-live-label { color: #a1a1a1; }
|
|
1466
1483
|
@keyframes tl-live-in { from { opacity: 0; transform: scale(0.97); } to { opacity: 1; transform: scale(1); } }
|
|
1467
1484
|
@keyframes tl-live-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
|
|
1468
1485
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -2906,8 +2923,9 @@
|
|
|
2906
2923
|
chatLoop
|
|
2907
2924
|
? h("span",{className:"t-tt-wrap tl-live-wrap"},
|
|
2908
2925
|
h("span",{className:"tl-live-chip",tabIndex:0},
|
|
2909
|
-
h("span",{className:"tl-live-
|
|
2910
|
-
|
|
2926
|
+
h("span",{className:"tl-live-main"},
|
|
2927
|
+
h("span",{className:"tl-live-dot"}),
|
|
2928
|
+
h("span",{className:"tl-live-label"},"Live")),
|
|
2911
2929
|
h("button",{className:"tl-live-stop",title:"Stop /refine live",
|
|
2912
2930
|
onClick:(e)=>{e.stopPropagation();onStopLive&&onStopLive();}},"Stop")),
|
|
2913
2931
|
h("span",{className:"t-tt tl-tt-below tl-tt-msg",role:"tooltip"},
|
|
@@ -2915,8 +2933,9 @@
|
|
|
2915
2933
|
: liveStopped
|
|
2916
2934
|
? h("span",{className:"t-tt-wrap tl-live-wrap"},
|
|
2917
2935
|
h("span",{className:"tl-live-chip is-stopped",tabIndex:0},
|
|
2918
|
-
h(
|
|
2919
|
-
|
|
2936
|
+
h("span",{className:"tl-live-main"},
|
|
2937
|
+
h("span",{className:"tl-live-dot"}),
|
|
2938
|
+
h("span",{className:"tl-live-label"},"Live Off"))),
|
|
2920
2939
|
h("span",{className:"t-tt tl-tt-below tl-tt-msg",role:"tooltip"},
|
|
2921
2940
|
"The `/refine live` loop was asked to stop and will exit on its next poll. Run `/refine live` again to resume."))
|
|
2922
2941
|
: null,
|
|
@@ -4159,9 +4178,11 @@
|
|
|
4159
4178
|
cssRules:harvestCssHints(e)}));
|
|
4160
4179
|
try{
|
|
4161
4180
|
const{id}=await relayCreateJob({kind:"scan",url:location.href,raw});
|
|
4162
|
-
for(let i=0;i<
|
|
4181
|
+
for(let i=0;i<520;i++){
|
|
4163
4182
|
if(scanTokenRef.current!==token)return; // superseded / unmounted
|
|
4164
|
-
|
|
4183
|
+
// Ramp: poll fast (120ms) for the first ~2.4s so a quick scan returns
|
|
4184
|
+
// almost immediately, then settle to 300ms to keep the request rate low.
|
|
4185
|
+
await new Promise(r=>setTimeout(r,i<20?120:300));
|
|
4165
4186
|
if(scanTokenRef.current!==token)return;
|
|
4166
4187
|
const job=await relayGetJob(id);
|
|
4167
4188
|
if(job.status==="done"){
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "transitions-refine",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.16",
|
|
4
4
|
"description": "Live, agent-driven Refine panel for CSS/Motion transitions — injects a timeline + Refine UI and runs transitions.dev suggestions via your coding agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/server/relay.mjs
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import { createServer } from "node:http";
|
|
26
26
|
import { randomUUID } from "node:crypto";
|
|
27
27
|
import { spawn } from "node:child_process";
|
|
28
|
-
import { existsSync } from "node:fs";
|
|
28
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
29
29
|
import { homedir } from "node:os";
|
|
30
30
|
import { delimiter, join } from "node:path";
|
|
31
31
|
import { refineTimings, DURATION_TOKENS, SCALE_TOKENS, BLUR_TOKENS, SMOOTH_OUT } from "./motion-tokens.mjs";
|
|
@@ -33,6 +33,12 @@ import { buildInjectModule } from "./inject.mjs";
|
|
|
33
33
|
|
|
34
34
|
const PORT = Number(process.env.REFINE_RELAY_PORT) || 7331;
|
|
35
35
|
const AUTO = process.env.REFINE_AUTO !== "0";
|
|
36
|
+
// Own package version, surfaced on /health so you can verify which relay build is
|
|
37
|
+
// actually running (npx caches — a stale relay is the usual "fix didn't work").
|
|
38
|
+
let PKG_VERSION = "0.0.0";
|
|
39
|
+
try {
|
|
40
|
+
PKG_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version || PKG_VERSION;
|
|
41
|
+
} catch {}
|
|
36
42
|
|
|
37
43
|
// A bare `cursor-agent` goes interactive: it prints "⚠ Workspace Trust Required"
|
|
38
44
|
// and exits 1, so every headless refine/scan/apply job fails. Force the headless
|
|
@@ -41,7 +47,15 @@ const AUTO = process.env.REFINE_AUTO !== "0";
|
|
|
41
47
|
// --print), and --force (auto-allow tool calls so apply/scan don't hang on
|
|
42
48
|
// approval). Append only the missing flags; leave non-cursor-agent commands alone.
|
|
43
49
|
function augmentAgentCmd(cmd) {
|
|
44
|
-
if (!cmd
|
|
50
|
+
if (!cmd) return cmd;
|
|
51
|
+
// `codex exec` reads the prompt from stdin ONLY when given a trailing `-`
|
|
52
|
+
// (the stdin marker). The relay always pipes the prompt on stdin, so without
|
|
53
|
+
// it Codex gets no prompt and exits 1. Append `-` when it's missing (a lone
|
|
54
|
+
// `-` token, not the dashes inside flags like `--sandbox`).
|
|
55
|
+
if (/(^|\s|\/)codex(\s|$)/.test(cmd) && /(^|\s)exec(\s|$)/.test(cmd)) {
|
|
56
|
+
return /(^|\s)-(\s|$)/.test(cmd) ? cmd : `${cmd} -`;
|
|
57
|
+
}
|
|
58
|
+
if (!/(^|\s|\/)cursor-agent(\s|$)/.test(cmd)) return cmd;
|
|
45
59
|
const has = (...flags) => flags.some((f) => new RegExp(`(^|\\s)${f.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$)`).test(cmd));
|
|
46
60
|
const extra = [];
|
|
47
61
|
if (!has("-p", "--print")) extra.push("-p");
|
|
@@ -55,6 +69,13 @@ const AGENT_CMD = augmentAgentCmd(process.env.REFINE_AGENT_CMD || null);
|
|
|
55
69
|
// (Opus / GPT-5.5) — forcing a fast model here keeps the initial scan snappy.
|
|
56
70
|
// Override with REFINE_SCAN_MODEL="" to fall back to the agent's default.
|
|
57
71
|
const SCAN_MODEL = process.env.REFINE_SCAN_MODEL ?? "composer-2.5-fast";
|
|
72
|
+
// Reasoning effort for scan jobs on Codex (cursor-agent uses SCAN_MODEL instead).
|
|
73
|
+
// Grouping is a structured, near-mechanical task — the cssRules payload already
|
|
74
|
+
// hands the agent the timings + state selectors — so it does NOT need deep
|
|
75
|
+
// reasoning. A default-effort Codex run can take tens of seconds; "low" cuts that
|
|
76
|
+
// dramatically with no grouping/naming loss. Override with REFINE_SCAN_EFFORT
|
|
77
|
+
// (minimal|low|medium|high) or "" to use the agent's configured effort.
|
|
78
|
+
const SCAN_EFFORT = process.env.REFINE_SCAN_EFFORT ?? "low";
|
|
58
79
|
// Pin a fast model for refine (suggestion) jobs too. The motion-token vocabulary
|
|
59
80
|
// is inlined into the prompt (see MOTION_TOKENS_BLOCK) so a fast model has every
|
|
60
81
|
// fact it needs for the common token-tweak path — keeping suggestions snappy
|
|
@@ -77,6 +98,28 @@ function withModel(cmd, model) {
|
|
|
77
98
|
if (/(^|\s)--model(\s|=)/.test(cmd)) return cmd; // respect an explicit choice
|
|
78
99
|
return cmd.replace(/^(\s*\S+)/, `$1 --model ${model}`);
|
|
79
100
|
}
|
|
101
|
+
|
|
102
|
+
// Speed up the SCAN job per agent — grouping is structured and doesn't need a
|
|
103
|
+
// heavy model / deep reasoning:
|
|
104
|
+
// • cursor-agent → pin SCAN_MODEL (a fast model) via --model.
|
|
105
|
+
// • codex exec → drop reasoning to SCAN_EFFORT ("low") and silence reasoning
|
|
106
|
+
// summaries (less work AND cleaner stdout for JSON parsing) via `-c` config
|
|
107
|
+
// overrides, inserted right after `exec` (before the trailing `-` stdin marker).
|
|
108
|
+
// No-ops if the user already pinned a model / reasoning effort, or disabled it
|
|
109
|
+
// (REFINE_SCAN_MODEL="" / REFINE_SCAN_EFFORT=""). Other CLIs are left untouched.
|
|
110
|
+
function withScanSpeed(cmd) {
|
|
111
|
+
if (!cmd) return cmd;
|
|
112
|
+
let out = withModel(cmd, SCAN_MODEL); // cursor-agent fast model
|
|
113
|
+
const isCodexExec =
|
|
114
|
+
/(^|\s|\/)codex(\s|$)/.test(out) && /(^|\s)exec(\s|$)/.test(out);
|
|
115
|
+
if (isCodexExec && SCAN_EFFORT && !/model_reasoning_effort/.test(out)) {
|
|
116
|
+
out = out.replace(
|
|
117
|
+
/(^|\s)exec(\s|$)/,
|
|
118
|
+
`$1exec -c model_reasoning_effort="${SCAN_EFFORT}" -c model_reasoning_summary="none"$2`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
80
123
|
const LONGPOLL_MS = Number(process.env.REFINE_LONGPOLL_MS) || 25000;
|
|
81
124
|
// Grace window after a `/refine live` agent's last poll during which LLM mode is
|
|
82
125
|
// still reported "available". Kept well above LONGPOLL_MS so the normal gaps
|
|
@@ -91,6 +134,13 @@ const PENDING_TIMEOUT_MS = Number(process.env.REFINE_PENDING_TIMEOUT_MS) || 1200
|
|
|
91
134
|
// to stop (it returns {stop:true} from /jobs/next). 0 disables. Only the chat
|
|
92
135
|
// loop is affected — a wired REFINE_AGENT_CMD never polls /jobs/next.
|
|
93
136
|
const POLLER_IDLE_STOP_MS = Number(process.env.REFINE_POLLER_IDLE_STOP_MS) || 600000;
|
|
137
|
+
// While the Stop latch is held, a genuinely new `/refine live` session can resume
|
|
138
|
+
// itself implicitly: only after polling has been *quiet* this long (the old loop
|
|
139
|
+
// actually died) do we treat the next poll as a fresh session and auto-resume.
|
|
140
|
+
// Must exceed a looping agent's poll gap, so an agent that ignores Stop and keeps
|
|
141
|
+
// polling can never clear the latch — it just keeps getting {stop:true}. (An
|
|
142
|
+
// updated agent also resumes explicitly via POST /poller/start, no wait needed.)
|
|
143
|
+
const STOP_QUIET_MS = Number(process.env.REFINE_STOP_QUIET_MS) || 15000;
|
|
94
144
|
|
|
95
145
|
/** @type {Map<string, Job>} */
|
|
96
146
|
const jobs = new Map();
|
|
@@ -106,6 +156,9 @@ let lastPollAt = 0;
|
|
|
106
156
|
// re-polls — can never silently revive the session. This is what makes the
|
|
107
157
|
// panel's Stop button stick instead of flipping "Live" back on seconds later.
|
|
108
158
|
let pollerStopped = false;
|
|
159
|
+
// When did a poll last arrive *while latched*? Used to detect that the old loop
|
|
160
|
+
// went quiet so a genuinely new session (or re-run) can auto-resume.
|
|
161
|
+
let lastStoppedPollAt = 0;
|
|
109
162
|
const pollerActive = () => !pollerStopped && now() - lastPollAt < POLLER_TTL_MS;
|
|
110
163
|
const llmAvailable = () => Boolean(AGENT_CMD) || pollerActive();
|
|
111
164
|
|
|
@@ -353,6 +406,18 @@ const AGENT_RETRIES = Number.isFinite(Number(process.env.REFINE_AGENT_RETRIES))
|
|
|
353
406
|
? Number(process.env.REFINE_AGENT_RETRIES)
|
|
354
407
|
: 2;
|
|
355
408
|
|
|
409
|
+
// CLIs (Codex especially) print verbose startup WARNINGs to stderr even on
|
|
410
|
+
// success — e.g. "could not create PATH aliases" and "failed to open state db"
|
|
411
|
+
// (Codex literally logs "proceeding" right after). Those lines used to eat the
|
|
412
|
+
// 300-char error budget, truncating away the REAL failure. Strip the known
|
|
413
|
+
// noise so the genuine error surfaces; keep a generous cap.
|
|
414
|
+
function cleanAgentErr(err) {
|
|
415
|
+
const noise = /could not create PATH aliases|failed to open state db|state DB at|codex_state::runtime|^\s*WARNING:|(^|\s)WARN(\s|:)/i;
|
|
416
|
+
const lines = (err || "").split(/\r?\n/).filter((l) => l.trim() && !noise.test(l));
|
|
417
|
+
const msg = lines.join("\n").trim() || (err || "").trim() || "(no stderr)";
|
|
418
|
+
return msg.slice(0, 1500);
|
|
419
|
+
}
|
|
420
|
+
|
|
356
421
|
function runAgentOnce(cmd, prompt, parse) {
|
|
357
422
|
return new Promise((resolve, reject) => {
|
|
358
423
|
const child = spawn("sh", ["-c", cmd], { stdio: ["pipe", "pipe", "pipe"] });
|
|
@@ -370,7 +435,7 @@ function runAgentOnce(cmd, prompt, parse) {
|
|
|
370
435
|
});
|
|
371
436
|
child.on("close", (code) => {
|
|
372
437
|
clearTimeout(timer);
|
|
373
|
-
if (code !== 0) return reject(new Error(`agent exited ${code}: ${err
|
|
438
|
+
if (code !== 0) return reject(new Error(`agent exited ${code}: ${cleanAgentErr(err)}`));
|
|
374
439
|
try {
|
|
375
440
|
resolve(parse(out));
|
|
376
441
|
} catch (e) {
|
|
@@ -519,7 +584,7 @@ async function answerJob(job) {
|
|
|
519
584
|
);
|
|
520
585
|
}
|
|
521
586
|
job.statusLog.push({ message: "Reading components from source…", at: now() });
|
|
522
|
-
result = await runAgentCmd(
|
|
587
|
+
result = await runAgentCmd(withScanSpeed(AGENT_CMD), buildScanPrompt(job), parseScanOutput);
|
|
523
588
|
job.result = { groups: result.groups, summary: result.summary };
|
|
524
589
|
job.status = "done";
|
|
525
590
|
job.updatedAt = now();
|
|
@@ -596,9 +661,11 @@ const server = createServer(async (req, res) => {
|
|
|
596
661
|
if (method === "GET" && path === "/health") {
|
|
597
662
|
return send(res, 200, {
|
|
598
663
|
ok: true,
|
|
664
|
+
version: PKG_VERSION,
|
|
599
665
|
auto: AUTO,
|
|
600
666
|
llmAvailable: llmAvailable(),
|
|
601
667
|
pollerActive: pollerActive(),
|
|
668
|
+
pollerStopped,
|
|
602
669
|
agentCmd: Boolean(AGENT_CMD),
|
|
603
670
|
cliInstalled: cursorCliInstalled(),
|
|
604
671
|
jobs: jobs.size,
|
|
@@ -661,10 +728,20 @@ const server = createServer(async (req, res) => {
|
|
|
661
728
|
// GET /jobs/next — long-poll claimed by a `/refine live` agent (LLM jobs).
|
|
662
729
|
if (method === "GET" && path === "/jobs/next") {
|
|
663
730
|
// Stopped latch: a Stop was issued and not yet resumed. Keep telling every
|
|
664
|
-
// poller to exit and report inactive
|
|
665
|
-
//
|
|
731
|
+
// poller to exit and report inactive until either an explicit POST
|
|
732
|
+
// /poller/start, or polling goes quiet for STOP_QUIET_MS (the old loop died)
|
|
733
|
+
// and a new poll arrives — only then is it a genuinely fresh session.
|
|
734
|
+
// A pending job always wins so we never drop real work on the edge.
|
|
666
735
|
if (pollerStopped && !nextPendingLlm()) {
|
|
667
|
-
|
|
736
|
+
if (lastStoppedPollAt && now() - lastStoppedPollAt >= STOP_QUIET_MS) {
|
|
737
|
+
// Quiet gap → the previous loop is gone. This poll is a fresh session.
|
|
738
|
+
pollerStopped = false;
|
|
739
|
+
lastStoppedPollAt = 0;
|
|
740
|
+
// fall through to normal handling below (counts as live again)
|
|
741
|
+
} else {
|
|
742
|
+
lastStoppedPollAt = now(); // a poller is still hammering us → hold the latch
|
|
743
|
+
return send(res, 200, { stop: true });
|
|
744
|
+
}
|
|
668
745
|
}
|
|
669
746
|
lastPollAt = now();
|
|
670
747
|
if (!lastJobAt) lastJobAt = now(); // seed idle window on the loop's first poll
|
|
@@ -672,7 +749,8 @@ const server = createServer(async (req, res) => {
|
|
|
672
749
|
// loop to exit. A pending job always wins so we never drop real work.
|
|
673
750
|
if ((stopRequested || (POLLER_IDLE_STOP_MS && now() - lastJobAt >= POLLER_IDLE_STOP_MS)) && !nextPendingLlm()) {
|
|
674
751
|
stopRequested = false;
|
|
675
|
-
pollerStopped = true; // latch until
|
|
752
|
+
pollerStopped = true; // latch until /poller/start or a quiet gap
|
|
753
|
+
lastStoppedPollAt = 0;
|
|
676
754
|
lastJobAt = 0;
|
|
677
755
|
lastPollAt = 0; // loop is exiting → report it inactive immediately on /health
|
|
678
756
|
return send(res, 200, { stop: true });
|
|
@@ -682,7 +760,8 @@ const server = createServer(async (req, res) => {
|
|
|
682
760
|
if (res.writableEnded) return;
|
|
683
761
|
if (stopRequested && !nextPendingLlm()) {
|
|
684
762
|
stopRequested = false;
|
|
685
|
-
pollerStopped = true; // latch until
|
|
763
|
+
pollerStopped = true; // latch until /poller/start or a quiet gap
|
|
764
|
+
lastStoppedPollAt = 0;
|
|
686
765
|
lastJobAt = 0;
|
|
687
766
|
lastPollAt = 0; // loop is exiting → report it inactive immediately on /health
|
|
688
767
|
return send(res, 200, { stop: true });
|
|
@@ -705,17 +784,19 @@ const server = createServer(async (req, res) => {
|
|
|
705
784
|
if (method === "POST" && path === "/poller/stop") {
|
|
706
785
|
const wasActive = now() - lastPollAt < POLLER_TTL_MS;
|
|
707
786
|
stopRequested = true;
|
|
708
|
-
pollerStopped = true; // latch: stays stopped until
|
|
787
|
+
pollerStopped = true; // latch: stays stopped until /poller/start or a quiet gap
|
|
788
|
+
lastStoppedPollAt = 0;
|
|
709
789
|
lastPollAt = 0; // report inactive immediately; a straggler poll won't revive it
|
|
710
790
|
return send(res, 200, { ok: true, stopping: wasActive });
|
|
711
791
|
}
|
|
712
792
|
|
|
713
793
|
// POST /poller/start — a fresh `/refine live` agent (or loop) announces itself
|
|
714
794
|
// and clears the Stop latch so its polls count as live again. Without this an
|
|
715
|
-
// explicit re-run
|
|
716
|
-
//
|
|
795
|
+
// explicit re-run would have to wait out the quiet window. Call it once at loop
|
|
796
|
+
// startup, before the first GET /jobs/next.
|
|
717
797
|
if (method === "POST" && path === "/poller/start") {
|
|
718
798
|
pollerStopped = false;
|
|
799
|
+
lastStoppedPollAt = 0;
|
|
719
800
|
stopRequested = false;
|
|
720
801
|
lastJobAt = now();
|
|
721
802
|
lastPollAt = now();
|