Repository navigation
feat(agent): supervise can poll briefly instead of long-polling - #237
Merged
gusfcarvalho merged 8 commits intoOct 28, 2026
Merged
Conversation
agent.yaml gets a poll block (wait: whole seconds 0s-60s, default 30s; interval: default 0s, at least 1s with wait: 0s). The worker carries the settings as worker.Poll, whose zero wait reaches the wire as wait=0s, and sleeps the interval after a 204 only. supervise passes the block through. Task: IMPROVEMENTS item 5(b), stack 5 PR 7 of 7. Design: P3 (#223). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…al-dispatcher test Self-review pass 1: the fix for a never-used connection holding serve's 5s shutdown moves into served.stop; the worker package comment is reflowed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
config.Poll.validate parses poll.wait and poll.interval (a unit is now required, so a bare "0" is refused) and checks them with worker.Poll.Validate, reporting its *PollError under the agent.yaml key. config's MaxPollWait and MinPollInterval copies are gone. Review: PRRT_kwDOVK0lgs6wxG1y (CORE-DUP-001) and the held nit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o fx/imp5b-7-supervise-poll
ccf-lisa
Bot
changed the base branch from
fx/imp5a-object-store-design
to
fx/imp5b-6-provider-no-list
October 28, 2026 11:57
…o fx/imp5b-7-supervise-poll
…o fx/imp5b-7-supervise-poll
…o fx/imp5b-7-supervise-poll
…imp5b-7-supervise-poll The stack below was rebased onto main; this keeps the branch's own changes on the new base.
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.
Task: IMPROVEMENTS item 5(b), stack 5 PR 7 of 7: fx/imp5b-7-supervise-poll on fx/imp5a-object-store-design. Design: P3 (#223).
Built on #223's branch (it touches only AGT's files) and to be restacked on top of PR 6.
Summary
Implements P3 §Agents poll briefly (
supervise), Decision 6, the Rollout's PR 7 and §Tests "Brief polling".agent.yamlgets apollblock (internal/agent/config):config.Poll{Wait, Interval string}, decoded strictly like the rest of the file.wait: whole seconds from0sto60s, default30s(filled only whenpoll.waitis absent).500ms,1.5s,61s,-1s, a bare number and a quoted, unit-less"0"are refused.interval: default0s, never negative, with a unit.wait: 0sneeds anintervalof at least1s, or the agent would poll in a hot loop.worker.Poll.Validate, reporting its*worker.PollErrorunder the key (poll.wait: "61s" must be whole seconds from 0s to 60s). The rules and their bounds live only in the worker.Poll.WaitDuration()/IntervalDuration()give the parsed values (the pattern ofWorkspaceSize.MemoryGiB).internal/agent/worker):Options.LeaseWaitis replaced byOptions.Poll, aworker.Poll{Wait, Interval time.Duration}.Waitmeans zero, sowait=0sreaches the wire. There is no default inside the worker:worker.DefaultPoll()is the long poll ({Wait: 30s}), andNewrefuses the zeroPollas a hot loop.Poll.Validateholds the rules (whole-second wait from 0s to 60s, an interval that isn't negative, at least 1s of interval with a zero wait) and returns a typed*PollError{Field, Value, Reason}.IntervalonloopCtx, so a drain or an abort ends the sleep. A poll that gets a lease polls again at once, so free slots fill. Throttling, the failure backoff, heartbeats, completes and abandons are unchanged.supervisepasses the block through (agentloop.go) and logspollWait/pollIntervalat start.deploy/poc/agent.yaml.example: the block is commented out, with a note that older agents refuse the key, so rolling an agent back means removing it.deploykit_test.gowalksconfig.Polltoo and checks that the example keeps the long poll.internal/server/agents.go:136-145:waitRE(^[0-9]{1,2}s$) accepts0s.Scheduler.Lease(internal/scheduler/lease.go:31-58) with a zero wait makes onetryLeasepass and returns 204 when nothing matches.Verification
make reviewable(with a golangci-lint built with go1.26, since the one on PATH is built with go1.25 and refuses the module's Go version): exit 0. Lint reports 0 issues, every package isok, and openapi-lint scores 100.internal/agent/config:TestParsePoll(defaults per key, a zero wait stays zero, the shortest interval for it, a minute wait, a sub-second interval with a long poll);TestParseDefaults(no block: the long poll);TestParseInvalid(wait over 60s, negative,500ms,1.5s, not a duration, a bare number; a negative interval and one that isn't a duration;wait: 0swith no interval or with500ms; a unit-less"0"for either key; an unknownpollkey).internal/agent/worker:TestNewValidates, now a table overPoll(the zeroPoll, a sub-second interval with a zero wait, a wait over 60s, a negative wait, a wait not in whole seconds, a negative interval).TestPollWait:wait=0sandwait=30son the wire. The defaults poll again at once on an unmovedclock.Fake; with an interval, no poll comes before it ends.TestBriefPollLeasePollsAgainAtOnce: a lease polls again at once, the next 204 sleeps, and a drain ends the sleep.TestBriefPollAgainstRealDispatcher:factory serve --dev-fake-providersas a subprocess. Idle polls get 204 and come an interval apart. A task created by a webhook while the agent sleeps is leased on the next poll, and every poll sentwait=0s.cmd/factory-agent:TestSupervisePoll(no block sendswait=30s;poll: {wait: 0s, interval: 1s}sendswait=0swith the polls at least 1s apart);TestDeployAgentExample.Options(fake_test.go's harness,TestRegisterRefused, the mock, real-dispatcher and effector tests) now setsPoll. The fake dispatcher records each poll'swaitand answerswait=0sat once.-race -count=8,TestSupervisePoll-count=5, and the real-dispatcher tests-count=3three times over after theserved.stopfix below."0"accepted) are fixed in c5d2e85;make reviewablepasses again (exit 0, 0 lint issues).No bucket is involved: nothing here needs a real cloud bucket. Whether brief polls keep Cloud Run's bill near zero can only be checked on a real deployment (IMPROVEMENTS item 9).
Ownership
AGT owns
internal/agent/andcmd/factory-agent. The spec also names two files outside that line, and this PR edits both:deploy/poc/agent.yaml.example(the run kit's example, whichcmd/factory-agent/deploykit_test.gochecks) anddesign/workstreams/agent/LLD/supervise.md(AGT's own LLD).For triage
http.Server.Shutdowntreats a connection that never carried a request as active for 5 s. The tests run serve with--shutdown-timeout 5s(internal/agent/worker/dispatcher_test.go'sstartServe, andcmd/factory/serve_app.go:404-422's drain). So a connection the default transport dialled but never used failedserved.stopwithdrain requests: context deadline exceeded, about 1 run in 3 of the new test.served.stopcloses the default transport's idle connections before SIGTERM, for every real-dispatcher test (AGT's file).serve's drain closes connections still inStateNew(viaConnState) once the listeners stop accepting.Deviations from the spec
None in behaviour. Two choices the spec leaves open:
poll.intervalhas no upper bound and no whole-second rule, since P3 sets neither. A long poll may carry an interval too: it then sleeps after each 204.Pollinstead of defaulting it. A zero wait has to mean zero, and the zeroPollwould be a hot loop, so every caller passesDefaultPoll()or the config's settings.🤖 Generated with Claude Code