Repository navigation
feat(live): agent-facing long-poll endpoint for proceed signals - #169
Conversation
Adds an agent long-poll channel to the live server so the Quirk bridge can replace its events-file poll loop with one-request/one-response GETs. - GET /__agent-isles/agent/events: returns the newest matching `proceed` record (latest-wins, at-most-once) or parks up to `hold` seconds (clamped [0,110]) then 204s; `screen`/`since` filters mirror the bridge's lenient `_screen_matches`/`latest_proceed_event`. - GET /__agent-isles/agent/health: cheap authed readiness probe. - In-memory proceed queue (bounded 64), cleared on new-screen advance so a stale click cannot satisfy a wait for the next screen. - Per-session bearer token published in state/server-info (0600); both agent routes require it and reject any request bearing an Origin header. - Held holds count as active clients against idle shutdown and are released with 503 at shutdown. - Bonus fire-and-forget live:signal SSE broadcast on every click. Strictly additive: the events file, existing routes, and record shape are unchanged. Rust parity (crates/isles-server) and the bridge switch are follow-ups per the plan.
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR introduces an authenticated agent long-poll endpoint to replace the bridge’s file-poll loop, backed by an in-memory bounded queue of proceed events, and adds integration tests to validate the new behavior.
Changes:
- Added
/__agent-isles/agent/events(long-poll withhold,since,screen) and/__agent-isles/agent/health, protected by a per-session bearer token. - Enqueued/broadcast signal records on both POST and WebSocket paths; cleared in-memory agent queue on screen advance; prevented idle shutdown while agent holds are parked.
- Added extensive test coverage for queueing, draining, hold behavior, filtering, auth/origin gate, idle shutdown, and WS signal ingestion.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| tests/live.test.mjs | Adds integration tests and small HTTP/WS helpers for the new agent long-poll + auth/token behavior. |
| src/live.mjs | Implements token auth, agent event queue + hold handling, broadcasts signals, and updates idle/close behavior accordingly. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Follow-up to the Fable-model red-team of the agent long-poll endpoint.
- Redact the bearer token from `isles live` stdout: it lands in scrollback
/ CI logs / agent transcripts where the 0600 file mode can't protect it.
The bridge reads the token from the file, so stdout never needed it.
- Select the newest queued record by insertion order (last matching), not
wall-clock-max timestamp — this is exactly the bridge's
`latest_proceed_event` ("last matching line") and stays correct if the
clock steps backwards between two clicks. Also simpler.
- Harden the server-info 0600 write: remove any stale tmp first so the
token is always *created* at 0600, never carried over at a looser mode.
- Add a test for held-request cleanup on client abort mid-hold (the
plan's `req.close` path); mutation-verified it fails if cleanup regresses.
Adversarial review (Fable-model subagent) — follow-up in
|
Addresses Copilot review feedback on PR #169. - openSignalWs: reject on a non-101 response or timeout instead of hanging forever if the WS route ever stops upgrading. - agentGet: add a settled guard (resolve once) and a request timeout so a stalled response fails fast rather than blocking the runner; keep the deliberate resolve-on-close so the shutdown-503 status survives a force-closed socket (now documented). - Move parseHoldSeconds/parseSinceSeconds/agentScreenMatches off the public export surface into an explicit `__internal` namespace; they are exposed only for unit tests, not as supported API.
Summary
Implements
docs/plans/agent-events-long-poll.md— an agent-facing long-poll endpoint on theisles liveserver so the Quirk bridge can replace itsstate/eventsfile-poll loop with a boring one-request/one-response GET (no poll latency, no file races, server-push on click).Node server only; strictly additive. Rust parity (
crates/isles-server) and the bridge'swaitswitch are follow-ups per the plan's Non-goals.What changed (
src/live.mjs)GET /__agent-isles/agent/events— returns the newest matchingproceedrecord (latest-wins, at-most-once, removed on delivery) or parks up toholdseconds (default 100, clamped[0,110];hold=0= check once) then204s.screen/sincefilters mirror the bridge's lenient_screen_matches(matchesscreenslug orscreen_file; unstamped records match any filter) andlatest_proceed_event;sinceuses>=. Body is the verbatimappendSignalEventrecord.GET /__agent-isles/agent/health—{"ok":true,"pid":<pid>}readiness probe.type === "proceed"is queued; bounded to the newest 64; cleared on new-screen advance so a stale click can't satisfy a wait for the next screen (withscreen/sinceas second/third guards).crypto.randomBytes(16)bearer token published instate/server-info(now written0600). Both agent routes require it (401on missing/wrong) and reject any request bearing anOriginheader (403).503at shutdown.live:signalSSE broadcast on every click (browsers ignore unknown events; nothing depends on it).Back-compat (all hard requirements met)
appendSignalEventstill writes the events file; record shape unchanged./,/events,/__agent-isles/{screens,signal,tree,raw}, or reader mode.tokenis an additiveserver-infofield; the bridge's parser ignores unknown fields.Test plan
tests/live.test.mjscovering every bullet of the plan's Testing section: queue delivery/drain, park-and-resolve (with push-immediacy assertion), hold-expiry retention across invocations,since>=edge,screenslug/file/unstamped matching, two-clicks-newest-wins, new-screen queue clear, auth401/403,server-infotoken,live:signalbroadcast, non-proceed-not-queued, idle-vs-hold,503-on-shutdown, WebSocket signal-twin ingress, and queue-bound eviction.npm testgreen: 392/394 unit (the 2cli-packsfailures are pre-existing/environmental, confirmed via stash) + 28/28 browser.curlloop against a realisles livedir: click → poll returns the record instantly withscreen/screen_filestamps; re-poll →204;server-infois-rw-------; no-token →401;Origin→403.