Repository navigation
fix(pm): fleet-write fails closed on an indeterminate liveness read, reads it only behind the proxy re-exec, and stops reading a proxy 403 as a rate limit - #19780
Merged
objectstack-fleet[bot] merged 1 commit intoSep 23, 2026
Conversation
…nd reads it only behind the proxy re-exec Under auto, any liveness answer other than 200 + active turned a cloud seat direct — writing as its personal account instead of objectstack-fleet[bot]. A 401 from a read made before the --use-env-proxy re-exec (node's fetch does not read HTTPS_PROXY) did exactly that mid-session, for a relay that was alive. relayLive now judges three buckets: 200 + active is live; the file's 404 and the disabled_manually state are the two DEFINITE not-live signals, and still turn a seat direct with the printed line; any other status, a 200 without a readable state, any other state, or no answer is INDETERMINATE, and the selector refuses (exit 3) naming the status and the read — under auto and explicit dispatch alike. relayLive also refuses to read at all from a process whose fetch would bypass HTTPS_PROXY (indeterminate, naming the flag, not cached), and post-stamped re-execs on --dry-run too, so route resolution runs only behind the proxy in every tool. classifyHttp no longer reads an absent x-ratelimit-remaining header as zero (Number(null) is 0), and recognises the egress proxy's 403 body — the message naming the proxy, or a documentation_url that is not GitHub's — as a route failure: write-pace's stopSignalFrom writes no marker for it, noteResponse gives back the slot paceWrite took and prints one line, and fleet-token names the proxy and the relay spelling in its refusal. Ceilings, the size route, the op table, the executor, the workflow and the allow rules are untouched. Self-tests: liveness 401 / 403 / 500 / malformed / no answer refuse under auto and explicit dispatch; 404 and disabled_manually go direct; 200 + active is dispatch; the unrouted read makes no request and is not cached; --route behind a dead proxy exits 3; the proxy 403 classifies as prerequisite with no marker and no spend; the dry-run re-exec is pinned. Claude-Session: https://claude.ai/code/session_01GnJon4xkRvphn4w28xx3An Co-authored-by: Claude <noreply@anthropic.com>
objectstack-fleet
Bot
deleted the
claude/issue-19774-fleet-write-liveness-fail-closed
branch
September 23, 2026 04:14
This was referenced Sep 23, 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.
Fixes #19774
What changed
scripts/pm/**only — the fleet's own write tools, not on the governed register.relayLive()infleet-write/dispatch.mjsrefuses to read from a process whose fetch would bypassHTTPS_PROXY(a proxy configured, no--use-env-proxyon the process): the reading is indeterminate, names the flag, and is not cached, so the re-exec'd child reads for itself.post-stamped.mjsnow re-execs on--dry-runtoo (a dry run resolves the route, and the route's read is a request);label-write,issue-create,close-cardsandwith-fleet.shalready re-exec before resolving, and--routedid. The ordering is pinned in post-stamped's self-test and the unrouted refusal in dispatch's.relayLive()judges three buckets: 200 withstate === 'active'is live (dispatch); the file's 404 and thedisabled_manuallystate are the two DEFINITE not-live signals and still turn a cloud seat direct with the printed line; any other status (401, 403, 5xx), a 200 without a readable state, any other state, or no answer is INDETERMINATE, andselectTransport/resolveRouterefuse with exit 3 naming the status and the read — underautoand explicitOS_FLEET_TRANSPORT=dispatchalike, neverdirect, never silent. The refusal carries the session so a tool can say who was refused. ExplicitOS_FLEET_TRANSPORT=directremains the only way to write as the personal account in a cloud container, and the tool prints it.classifyHttp(label-write.mjs) no longer reads an absentx-ratelimit-remainingheader as zero (Number(null)is0, which is how the mint's proxy 403 wrote a 30-minute STOP MARKER), and recognises the egress proxy's 403 body —isProxyRefusal()in write-pace.mjs: the message naming the proxy, or adocumentation_urlthat is not GitHub's, the measured discriminant — as a route failure (prerequisite).stopSignalFromwrites no marker for such a body whatever verdict or header accompanies it;noteResponsegives back the slotpaceWritetook (no budget spent) and prints one line;fleet-tokennames the proxy and the relay spelling in its refusal. Callers (dispatch.mjs, fleet-token.mjs) now hand the body to the classifier. Existing markers are not migrated; they expire.Verification record
Live, in this cloud container, with NO
OS_FLEET_SESSIONset (origin/mainat 2005a55, worktree at 25ee9c2):A bare
node scripts/pm/post-stamped.mjs --comment=19774 --file=… --dry-run(no proxy flag on the command) now prints the re-exec line and thenpost-stamped: transport dispatch — …with the derived session andstate active; onmainthe same command read condition ③ asHTTP 401and reporteddirect. The forced-401 case is proven in the dispatch self-test's fake platform (exit-3 shaped refusal naming HTTP 401 under auto and under explicit dispatch), and--routebehind a proxy that answers nothing is proven end to end (real re-exec, exit 3, error naming no answer).Batteries, each run alone with its exit captured before any pipe:
pnpm check:pm-fleet-write-validatepnpm check:pm-fleet-write-executepnpm check:pm-fleet-write-dispatchpnpm check:pm-with-fleetpnpm check:pm-post-stampedpnpm check:pm-label-writepnpm check:pm-issue-createpnpm check:pm-close-cardspnpm check:pm-write-pacepnpm check:pm-fleet-tokennpx eslint --no-inline-configon the five changed.mjsfiles: exit 0.Gates —
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandson the edited tree derived 39 families (6 paths vs merge base 2005a55; 355 changed lines, under the human-merge threshold). Each was run alone with its exit code captured before any pipe; all 39 exited 0.--ranwith the codes recorded:39 derived famil(ies) accounted for — 39 run, 0 NOT-MEASURED (a DERIVED zero — all 39 recorded an exit code and none of them is 3). Repo-widepnpm lintis CI's run; the changed files were linted directly (above).Changeset
None:
scripts/pm/**publishes nothing from any released package, soskip-changesetapplies. This dispatch forbids label writes, so the label is the seat's to apply.Acceptance notes
disabled_inactivity(GitHub disabling a workflow for inactivity) is treated as indeterminate, not as a definite not-live signal: the card names exactly two definite signals, and this one is neither the maintainer's switch nor an absent file. If the seat wants it definite, it is a one-line change inrelayLiveplus its pin..claude/skills/pm-dispatch/references/rest-channel.mdandplatform-readings.mdstill describe the previous fall-back on any non-200 read; governed paths, outside this card'sscripts/pm/**scope.Generated by Claude Code