transitions-refine 0.3.13 → 0.3.14

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.
@@ -54,6 +54,19 @@ The relay reports the agent as "available" for ~120s after your last poll, so
54
54
  short pauses keep you live. A successful job always resets idle, so an active
55
55
  session never backs off.
56
56
 
57
+ 0. **Announce yourself once, before the first poll.** The relay keeps a *sticky*
58
+ Stop latch: after a panel **Stop** (or the idle auto-stop) it answers every
59
+ `GET /jobs/next` with `{"stop": true}` until a new agent explicitly resumes —
60
+ so a stopped session can't silently come back. Clear the latch a single time
61
+ at startup, then begin polling:
62
+
63
+ ```bash
64
+ curl -s -X POST http://localhost:7331/poller/start
65
+ ```
66
+
67
+ Do **not** call this again mid-loop (it would defeat a user's Stop). Only on a
68
+ fresh `/refine live`.
69
+
57
70
  1. **Claim the next job (long-poll).** This call blocks up to ~25s, then returns.
58
71
 
59
72
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "transitions-refine",
3
- "version": "0.3.13",
3
+ "version": "0.3.14",
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": {
@@ -446,6 +446,10 @@ async function pollOnce() {
446
446
 
447
447
  async function loop() {
448
448
  log(`live refine loop → ${RELAY} (workspace: ${WORKSPACE})`);
449
+ // Announce ourselves so the relay clears any prior Stop latch — otherwise a
450
+ // relay that's still "stopped" from a previous panel Stop would answer our
451
+ // first poll with {stop:true} and we'd exit immediately.
452
+ try { await post("/poller/start", {}); } catch {}
449
453
  // eslint-disable-next-line no-constant-condition
450
454
  while (true) {
451
455
  try {
package/server/relay.mjs CHANGED
@@ -91,13 +91,6 @@ const PENDING_TIMEOUT_MS = Number(process.env.REFINE_PENDING_TIMEOUT_MS) || 1200
91
91
  // to stop (it returns {stop:true} from /jobs/next). 0 disables. Only the chat
92
92
  // loop is affected — a wired REFINE_AGENT_CMD never polls /jobs/next.
93
93
  const POLLER_IDLE_STOP_MS = Number(process.env.REFINE_POLLER_IDLE_STOP_MS) || 600000;
94
- // After a Stop (manual or idle), hold the poller "stopped" for this long: every
95
- // GET /jobs/next keeps getting {stop:true} and /health reports the poller
96
- // inactive. Without it, a single straggler poll fired right after the stop
97
- // (an agent mid-backoff, or one that batches a poll before exiting) re-registers
98
- // as live and the panel flips "Live" back on a few seconds later. A genuine
99
- // re-run of `/refine live` after the window resumes normally.
100
- const STOP_GRACE_MS = Number(process.env.REFINE_STOP_GRACE_MS) || 5000;
101
94
 
102
95
  /** @type {Map<string, Job>} */
103
96
  const jobs = new Map();
@@ -106,10 +99,14 @@ const now = () => Date.now();
106
99
  // When did a `/refine live` agent last poll? Used to know if LLM mode can be
107
100
  // served by a live editor agent (vs. needing REFINE_AGENT_CMD).
108
101
  let lastPollAt = 0;
109
- // While now() < stoppedUntil the relay holds a just-issued Stop (see STOP_GRACE_MS):
110
- // the poller reports inactive and every /jobs/next answers {stop:true}.
111
- let stoppedUntil = 0;
112
- const pollerActive = () => now() >= stoppedUntil && now() - lastPollAt < POLLER_TTL_MS;
102
+ // Stop latch. Set by POST /poller/stop (and idle auto-stop); cleared ONLY by an
103
+ // explicit POST /poller/start (a fresh `/refine live` / agent loop announcing
104
+ // itself). While latched the poller reports inactive and every GET /jobs/next
105
+ // answers {stop:true}, so a loop that ignores the stop or keeps polling /
106
+ // re-polls — can never silently revive the session. This is what makes the
107
+ // panel's Stop button stick instead of flipping "Live" back on seconds later.
108
+ let pollerStopped = false;
109
+ const pollerActive = () => !pollerStopped && now() - lastPollAt < POLLER_TTL_MS;
113
110
  const llmAvailable = () => Boolean(AGENT_CMD) || pollerActive();
114
111
 
115
112
  // Stop signal for the in-chat `/refine live` loop. Set by POST /poller/stop
@@ -663,32 +660,31 @@ const server = createServer(async (req, res) => {
663
660
 
664
661
  // GET /jobs/next — long-poll claimed by a `/refine live` agent (LLM jobs).
665
662
  if (method === "GET" && path === "/jobs/next") {
666
- // Stop-grace window: a Stop was just issued, so keep telling every poller to
667
- // exit and don't count these polls as live (we deliberately do NOT update
668
- // lastPollAt here). This stops a straggler poll from reviving the session.
669
- // A pending job still wins so we never drop real work on the stop edge.
670
- if (now() < stoppedUntil && !nextPendingLlm()) {
663
+ // Stopped latch: a Stop was issued and not yet resumed. Keep telling every
664
+ // poller to exit and report inactive (don't touch lastPollAt) until a fresh
665
+ // POST /poller/start. A pending job still wins so we never drop real work.
666
+ if (pollerStopped && !nextPendingLlm()) {
671
667
  return send(res, 200, { stop: true });
672
668
  }
673
669
  lastPollAt = now();
674
670
  if (!lastJobAt) lastJobAt = now(); // seed idle window on the loop's first poll
675
- // Stop signal (manual Stop button or idle auto-stop) → tell the loop to exit.
676
- // A pending job always wins so we never drop real work on the stop edge.
671
+ // Idle auto-stop (or a leftover stopRequested) → latch stopped + tell the
672
+ // loop to exit. A pending job always wins so we never drop real work.
677
673
  if ((stopRequested || (POLLER_IDLE_STOP_MS && now() - lastJobAt >= POLLER_IDLE_STOP_MS)) && !nextPendingLlm()) {
678
674
  stopRequested = false;
675
+ pollerStopped = true; // latch until an explicit /poller/start
679
676
  lastJobAt = 0;
680
677
  lastPollAt = 0; // loop is exiting → report it inactive immediately on /health
681
- stoppedUntil = now() + STOP_GRACE_MS; // hold the stop so re-polls can't revive it
682
678
  return send(res, 200, { stop: true });
683
679
  }
684
680
  const deadline = now() + LONGPOLL_MS;
685
681
  const attempt = () => {
686
682
  if (res.writableEnded) return;
687
- if ((stopRequested || now() < stoppedUntil) && !nextPendingLlm()) {
683
+ if (stopRequested && !nextPendingLlm()) {
688
684
  stopRequested = false;
685
+ pollerStopped = true; // latch until an explicit /poller/start
689
686
  lastJobAt = 0;
690
687
  lastPollAt = 0; // loop is exiting → report it inactive immediately on /health
691
- stoppedUntil = now() + STOP_GRACE_MS; // hold the stop so re-polls can't revive it
692
688
  return send(res, 200, { stop: true });
693
689
  }
694
690
  const job = nextPendingLlm();
@@ -709,11 +705,23 @@ const server = createServer(async (req, res) => {
709
705
  if (method === "POST" && path === "/poller/stop") {
710
706
  const wasActive = now() - lastPollAt < POLLER_TTL_MS;
711
707
  stopRequested = true;
708
+ pollerStopped = true; // latch: stays stopped until POST /poller/start
712
709
  lastPollAt = 0; // report inactive immediately; a straggler poll won't revive it
713
- stoppedUntil = now() + STOP_GRACE_MS; // hold the stop through the grace window
714
710
  return send(res, 200, { ok: true, stopping: wasActive });
715
711
  }
716
712
 
713
+ // POST /poller/start — a fresh `/refine live` agent (or loop) announces itself
714
+ // and clears the Stop latch so its polls count as live again. Without this an
715
+ // explicit re-run couldn't resume, since a latched relay tells every poll to
716
+ // stop. Call it once at loop startup, before the first GET /jobs/next.
717
+ if (method === "POST" && path === "/poller/start") {
718
+ pollerStopped = false;
719
+ stopRequested = false;
720
+ lastJobAt = now();
721
+ lastPollAt = now();
722
+ return send(res, 200, { ok: true });
723
+ }
724
+
717
725
  const m = path.match(/^\/jobs\/([^/]+)(?:\/(status|result|error))?$/);
718
726
  if (m) {
719
727
  const job = jobs.get(m[1]);