Repository navigation
Report activity to the platform's local activity socket - #538
Merged
pufit merged 9 commits intoOct 21, 2026
Merged
Conversation
A platform that parks idle machines needs to know, from inside the machine, whether Nerve is busy and when it next has scheduled work. When platform.activity_socket (a Unix socket, default /run/nerve/guestd.sock) exists at startup, the gateway now keeps it current: - POST /v1/activity with runningTurns (session turns plus queued or draining workflow runs, each counted once), activeSessions (open /ws clients) and the next scheduled task from the cron service. Sent on every change (turn start/end, client connect/leave, schedule change, workflow run finished), at most once every 250 ms, and every platform.activity_interval_seconds. A turn that starts and ends between two reports is still reported once. - Keep-awake leases (POST/DELETE /v1/leases, renewed every half platform.lease_ttl_seconds): nerve-memorize, nerve-config-sync, nerve-backup through keep_awake(), and nerve-background while any session has a live background agent. Requests are best effort: failures are logged once at INFO, then at DEBUG, and never raised. A socket that refuses nextScheduledName gets reports without it. Without the socket nothing is sent; an empty platform.activity_socket switches reporting off. SessionManager gains running_count() and running listeners, the agent registry any_live() and live listeners. nerve doctor shows whether the socket is present.
…est-activity-reporter
The reporter now calls CronService.next_scheduled() and its schedule listeners directly; it stays inert about the schedule only when the cron service did not start. A task name is cleaned the way the service names ticks (other characters become "_", cut at 64), so the socket never refuses a report for it, and a socket that does not accept task names gets times only, said once at INFO. Tests run the reporter against a real CronService: its forecast is reported, and a finished run is followed at once by a report that names the task after it.
Config loading keeps platform.activity_interval_seconds and platform.lease_ttl_seconds as given, like every other setting; the reporter holds them to 5-300 s and 10-3600 s when it is built and says so in a warning. A test drives the reporter with the real engine: its session turns and a background task reach the socket as a report and the nerve-background lease. The docs say that a background task that never ends keeps the machine awake until it is stopped.
The reporter asked the cron service for the next scheduled task before every report, so a slow gate forecast or a busy database held up the report of a turn start for up to five seconds. The forecast is now kept and made again only when it may have changed: when the schedule changes, when a turn ends (the turn may have scheduled a wakeup) and for the periodic report. It runs beside the loop. A turn start goes out at once with the last forecast. Other reports wait for the forecast under way, and a finished forecast is reported before the next one starts, so a stream of changes cannot hold reports back. A socket that refused reports for a reason other than the task's name got every report twice, once with the name and once without. When the report is refused without the name too, later reports now go out once each, without the name, until the socket takes one; the report after that has the name again. A failed lease request is retried after a quarter of the lease lifetime (at most five seconds) instead of half of it. A renewal that times out at the shortest lifetime then still gets its retry in before the lease runs out. A failed forecast and a failure inside the reporting loop are logged as the reporter's own, not as the socket's.
A 404 to a lease release has a test now: the lease counts as released and is not tried again. The test that holds a lease from a worker thread waited by polling, which kept the event loop awake, so a wake that cannot reach a sleeping loop passed it as well. It now waits on an event the fake socket sets when a request arrives, and the worker takes the lease once the loop has gone to sleep. The test with the real engine keeps the Codex home in its temporary directory instead of leaving an empty directory in the working directory. The docs say "at most one post every 250 ms", and the background lease row lists everything it covers: anything live in the session's Agents panel.
The page is docs/agents.md. The link spelled it in capitals, which only resolves on a case-insensitive file system.
A wakeup recorded without a turn, as "Run later" in the web UI does, reached the socket only with the next forecast: the report sent when the client then left reused the last one, since notify() asked for a report and no forecast. Now notify(), which client connections and finished workflow runs use, asks for a new forecast, and the report waits for it. A turn start is still the one report that goes out at once with the last forecast. The report that follows a turn which started and ended between two reports asks for no forecast of its own: the turn's end asked for one. Say in the docs which reports wait for a forecast, and that one already under way is waited for too.
alex-clickhouse
added this pull request to stack #540
October 21, 2026 14:46
alex-clickhouse
approved these changes
Oct 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Report activity to the platform's local activity socket
Stacked on #537 (gate forecasts). A platform that parks idle machines may expose a local Unix socket where the application reports what it is doing. When that socket exists, Nerve now keeps it current; otherwise nothing changes.
POST /v1/activity {runningTurns, activeSessions, nextScheduledAt, nextScheduledName}on every change (at most once per 250 ms; a turn start is reported promptly) and everyplatform.activity_interval_seconds(30 s): running turns (sessions + active workflow runs, counted once), connected web clients, and the next tick fromCronService.next_scheduled()(cached; refreshed on schedule changes, turn end and the periodic report).POST/DELETE /v1/leases, renewed at half-life, retried before they can lapse) while background work that is not a turn is in flight: memorization, workspace sync, backup, live background agents. Documented coverage indocs/platform-activity.md.[A-Za-z0-9._:-]{0,64}; a socket that answers 400 gets one retry without the task name and is then served without it; errors never reach callers or stop the gateway.platform:(activity_socket,activity_interval_seconds,lease_ttl_seconds);activity_socket: ""turns reporting off;nerve doctorshows whether the socket is present.Tests: 62 new (fake Unix-socket server: payloads, counts, leases, coalescing, 400 fallback, a run-later wakeup reported after the last client disconnects, inert without the socket). Full suite 6,773 passed, 38 skipped at the head of the stack; ruff unchanged.
🤖 Generated with Claude Code