You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit ed4b655
Browse filesBrowse the repository at this point in the historyBrowse files
authored
fix(pm): fleet-write derives the seat session from the container; the workflow’s Actions state is the kill switch; size-routed direct for post-stamped (#19760)
Fixes#19755
## What changed
`scripts/pm/**` only — the fleet's own write tools, not on the governed
register.
1. **The seat session is derived from the container.**
`sessionSource(env)` in `fleet-write/dispatch.mjs` reads
`OS_FLEET_SESSION` first (explicit wins, validation unchanged; an
explicit value that is malformed is refused, never replaced). When it is
absent, the container's `CLAUDE_CODE_REMOTE_SESSION_ID` — `cse_ID` — is
read and becomes `session_ID`; malformed or absent gives `null` as
before. The route's reason names which source was read, and `--route`
now prints the session and its source. The container variable's name is
`CONTAINER_SESSION_ENV` in `fleet-write/ops.mjs`, beside `SESSION_ENV`,
with its shape `CONTAINER_SESSION_SHAPE` beside `SESSION_SHAPE`.
2. **Condition ③ reads the workflow's Actions state.** Relay live = the
file on `main` (today's read) AND `GET
/repos/{board}/actions/workflows/fleet-write.yml` answering 200 with
`state === 'active'`. Any other state, status or no answer is not live,
the reason naming it — `disabled_manually` verbatim when that is the
state, described as the maintainer's kill switch. Both reads are cached
once per process and reached only after ① and ②. Explicit
`OS_FLEET_TRANSPORT=dispatch` against a disabled workflow is a
prerequisite refusal (exit 3), never a silent fall-back.
3. **Unchanged:** the two ceilings, the no-run fall-back under `auto`,
the op table, the executor, the workflow file, every allow rule. Every
`--help` line and header comment that told a seat to export
`OS_FLEET_SESSION` (dispatch.mjs, label-write.mjs, issue-create.mjs,
with-fleet.sh) now says a cloud seat's session is read from the
container and `OS_FLEET_SESSION` overrides it (a local checkout, a
test). No documented spelling carries a prefix.
4. **Self-tests** for the derivation (well-formed / malformed / absent /
explicit wins / malformed explicit never replaced / blank explicit
counts as absent / the reason names the source), the state read
(`active` / `disabled_manually` / another state / a 200 without a state
/ 404 / 500 / no answer / cached once / reached only after the file read
/ strict refusal), and the CLI (`--dry-run` and `--route` with the
container variable alone). The dispatch and with-fleet CLI cases clear
the inherited container variable, so both batteries hold inside a cloud
container.
6. **Size-routed fall-back under `auto` (addendum, item 6).**
`sizeRoute(route, body)` in `post-stamped.mjs` measures the RENDERED
body in UTF-8 bytes against `MAX_BODY_BYTES` (60,000; the platform's
`client_payload` cap is 64 KB). Over it under `auto`, THAT write takes
`direct` and the transport line becomes one line naming the bytes, the
cap and the identity used (the seat's own user, not
`objectstack-fleet[bot]`). Explicit `OS_FLEET_TRANSPORT=dispatch` over
the cap is refused (exit 3) naming the bytes. A route already direct, or
already carrying an error, is untouched; a refused op, a failed run and
UNCONFIRMED keep their register; `label-write`, `issue-create`,
`close-cards` are not size-routed; no compression codec (pinned by
import shape).
## Verification record
**`--route` in this cloud container, with NO `OS_FLEET_SESSION` set**
(the seat's own id from the claude-code-remote `get_session` tool, field
`ccr.id`, is `session_01GnJon4xkRvphn4w28xx3An`; the container carries
`CLAUDE_CODE_REMOTE_SESSION_ID=cse_01GnJon4xkRvphn4w28xx3An` — same
tail). Angle-bracket placeholders in the pasted reason are spelled `ID`
here for the body sanitizer.
```
$ node scripts/pm/fleet-write/dispatch.mjs --route
{"requested":"auto","transport":"dispatch","failed":null,"session":"session_01GnJon4xkRvphn4w28xx3An","session_source":"CLAUDE_CODE_REMOTE_SESSION_ID","reason":"OS_FLEET_TRANSPORT is auto → dispatch: CCR_AGENT_PROXY_ENABLED=1 (a cloud seat container, whose proxy replaces the Authorization header), the session was derived from the container's CLAUDE_CODE_REMOTE_SESSION_ID (cse_ID → session_ID; OS_FLEET_SESSION is absent), and the relay workflow .github/workflows/fleet-write.yml is on objectstack-ai/objectstack@main (GET answered 200) and its Actions state is active (GET /repos/objectstack-ai/objectstack/actions/workflows/fleet-write.yml answered 200)","error":null}
exit 0
```
Before this change the same command in the same container answered
`"transport":"direct","failed":"session"`.
**Size route, live dry runs in this container** (`node --use-env-proxy
scripts/pm/post-stamped.mjs --comment=19755 --file=… --dry-run`; nothing
written). A 60,159-byte rendered body:
```
post-stamped: transport direct — OS_FLEET_TRANSPORT is auto → direct for THIS write: the body is 60,159 UTF-8 bytes, 159 over the relay's 60,000-byte body cap (a repository_dispatch client_payload is capped by the platform at 64 KB), so it cannot take the relay. Written DIRECT as the seat's own user (the token this process holds), not as objectstack-fleet[bot]. ⛔ Not compressed: a body this size is the thing to shrink.
```
A small body from the same tree: `post-stamped: transport dispatch — …
the session was derived from the container's
CLAUDE_CODE_REMOTE_SESSION_ID … its Actions state is active …`. The
disabled-workflow case is proven in the dispatch self-test's fake
platform; the live workflow was not touched.
**Batteries, each run alone with its exit captured before any pipe**
(worktree at 6470e4e):
| command | exit | verdict line |
|---|---|---|
| `pnpm check:pm-fleet-write-validate` | 0 | 69 cases pass across 7
batteries |
| `pnpm check:pm-fleet-write-execute` | 0 | 48 cases pass across 9
batteries |
| `pnpm check:pm-fleet-write-dispatch` | 0 | 90 cases pass across 10
batteries (was 64 across 9 at c120dbd) |
| `pnpm check:pm-with-fleet` | 0 | 32 cases pass (was 31; floor 28 → 29)
|
| `pnpm check:pm-post-stamped` | 0 | 609 cases pass across 21 batteries
(was 596 across 20) |
| `pnpm check:pm-label-write` | 0 | 88 cases pass across 10 batteries |
| `pnpm check:pm-issue-create` | 0 | 42 cases pass across 6 batteries |
| `pnpm check:pm-close-cards` | 0 | 111 cases pass across 12 batteries |
| `pnpm check:pm-write-pace` | 0 | 109 cases pass across 12 batteries |
`npx eslint --no-inline-config` on the five changed `.mjs` files: exit
0, no findings.
**Gates** — `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` on the edited tree derived 38
families (6 paths vs merge base c120dbd; 449 changed lines, under the
human-merge threshold). Each was run alone with its exit code captured
before any pipe; all 38 exited 0. `--ran` with the codes recorded: `38
derived famil(ies) accounted for — 38 run, 0 NOT-MEASURED (a DERIVED
zero — all 38 recorded an exit code and none of them is 3)`. Repo-wide
`pnpm lint` is CI's run; the changed files were linted directly (above).
## Changeset
None: `scripts/pm/**` publishes nothing from any released package, so
`skip-changeset` applies. This dispatch forbids label writes, so the
label is the seat's to apply.
## Acceptance notes
- `post-stamped --dry-run` skips the proxy re-exec by design ("that path
makes no request"), yet the route resolution on a dry run does read the
relay, so in a cloud container a dry run without `--use-env-proxy` reads
condition ③ as `HTTP 401` and reports `direct`. Pre-existing (the read
existed before this change; it was simply never reached without a
session). Observation only, not filed — a live run re-arms first and is
unaffected.
- `close-cards.mjs` and the sibling tools' self-test fixtures still
spell `OS_FLEET_SESSION is absent` as a fixture error string; those are
fixtures, not documented spellings, and were left as they are.
- `.claude/skills/pm-dispatch/references/rest-channel.md` still
describes the old opt-in sentence; that path is on the governed register
and outside this card's `scripts/pm/**` scope.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01GnJon4xkRvphn4w28xx3An)_
<sub>Dispatching seat (director, summon #27): `skip-changeset` applied
by the seat — `scripts/pm/**` publishes nothing; the card's `Claim:`
naming this branch is on #19755. Written 2026-09-23T00:48Z.</sub>
Co-authored-by: objectstack-fleet <support@objectstack.ai>
Co-authored-by: Claude <noreply@anthropic.com>
constsize=`the body is ${grouped(bytes)} UTF-8 bytes, ${grouped(bytes-cap)} over the relay's ${grouped(cap)}-byte body cap (a repository_dispatch client_payload is capped by the platform at 64 KB)`;
2860
+
if(route.requested==='auto'){
2861
+
return{
2862
+
...route,
2863
+
transport: 'direct',
2864
+
bytes,
2865
+
cap,
2866
+
sizeRouted: true,
2867
+
reason: `${TRANSPORT_ENV} is auto → direct for THIS write: ${size}, so it cannot take the relay. Written DIRECT as the seat's own user (the token this process holds), not as objectstack-fleet[bot]. ⛔ Not compressed: a body this size is the thing to shrink.`,
2868
+
};
2869
+
}
2870
+
return{
2871
+
...route,
2872
+
bytes,
2873
+
cap,
2874
+
sizeRouted: true,
2875
+
error: `${TRANSPORT_ENV}=dispatch but ${size} — the relay cannot carry it. ⛔ Not falling back to direct: the operator asked for the relay, and the fall-back would change the identity the write is booked against. Shrink the body, or run under ${TRANSPORT_ENV}=auto to let THIS write go direct as the seat's own user.`,
'the shared rule: this tool and H56 cannot come to disagree': 6,
3265
3316
'the keyed lines: a claim\'s exact-value fields, judged by the readers that own them': 20,
3266
3317
'the relay transport: the same act as one op, the comment found on the board, the exit register kept apart': 12,
3318
+
"the size route: over the relay's body cap under auto THIS write goes direct with one line naming bytes, cap and identity; at or under it the relay; explicit dispatch refuses naming the bytes; nothing else re-routes": 12,
3267
3319
});
3268
3320
constSELF_TEST_BATTERY_FLOOR=16;
3269
3321
constUNATTRIBUTED_BATTERY='(unattributed)';
@@ -4356,6 +4408,33 @@ export function selfTest() {
4356
4408
t('structural: in main a dry run returns before the relay can be reached, and ONE read-back verdict serves both transports',mainSource.indexOf('if (options.dryRun) {')<mainSource.indexOf('await writeViaRelay(')&&(mainSource.match(/readBackVerdict\(\{/g)??[]).length===1);
4357
4409
}
4358
4410
4411
+
// ── the size route ───────────────────────────────────────────────────────
4412
+
battery("the size route: over the relay's body cap under auto THIS write goes direct with one line naming bytes, cap and identity; at or under it the relay; explicit dispatch refuses naming the bytes; nothing else re-routes");
t('one byte over the cap under auto: DIRECT for this write, flagged as size-routed, no error',over.transport==='direct'&&over.sizeRouted===true&&over.error===null);
4419
+
t('…and the ONE line names the bytes sent',over.reason.includes(`${grouped(MAX_BODY_BYTES+1)} UTF-8 bytes`));
4420
+
t('…the cap',over.reason.includes(`${grouped(MAX_BODY_BYTES)}-byte body cap`));
4421
+
t("…and the identity used — the seat's own user, not the fleet bot",over.reason.includes("seat's own user")&&over.reason.includes('not as objectstack-fleet[bot]'));
t('one byte under the cap under auto: the relay, the route untouched',under.transport==='dispatch'&&under.sizeRouted===false&&under.reason===AUTO.reason&&under.error===null);
4424
+
t('…and exactly AT the cap is still the relay (the validator admits it)',sizeRoute(AUTO,ofBytes(MAX_BODY_BYTES)).transport==='dispatch');
t('⛔ explicit dispatch one byte over: a PREREQUISITE refusal naming the bytes and saying it will NOT fall back, the transport NOT changed',strict.transport==='dispatch'&&typeofstrict.error==='string'&&strict.error.includes(grouped(MAX_BODY_BYTES+1))&&strict.error.includes('Not falling back'));
4427
+
t('bytes are UTF-8 BYTES, not characters: 20,001 three-byte characters are over the cap, 20,000 are not',sizeRoute(AUTO,'中'.repeat(20_001)).transport==='direct'&&sizeRoute(AUTO,'中'.repeat(20_000)).transport==='dispatch');
4428
+
t('a route already direct is never touched, whatever the size',sizeRoute({ ...AUTO,transport: 'direct'},ofBytes(MAX_BODY_BYTES+1)).sizeRouted===false);
4429
+
t('a route that already carries an error is never touched — the size never masks a missing session',sizeRoute({ ...STRICT,error: 'no session'},ofBytes(MAX_BODY_BYTES+1)).error==='no session');
4430
+
t('⛔ nothing else re-routes: a refused dispatch, a failed run and UNCONFIRMED keep their register',relayExitFor({state: 'refused'})===EXIT_PREREQUISITE_NOT_MET&&relayExitFor({state: 'failure'})===EXIT_NOT_STORED&&relayExitFor({state: 'timeout'})===EXIT_UNCONFIRMED);
4431
+
constownSource=readFileSync(SELF_PATH,'utf8');
4432
+
constmainSource=ownSource.slice(ownSource.indexOf('async function main(argv)'),ownSource.indexOf('// --self-test — offline'));
4433
+
t('structural: main hands the resolved route through sizeRoute on the RENDERED body before the transport line is printed',mainSource.includes('sizeRoute(await resolveRoute(process.env), rendered.body)')&&mainSource.indexOf('sizeRoute(')<mainSource.indexOf('transport ${route.transport}'));
4434
+
// An import SHAPE, not the module's bare name: the name is spelled in this very line, so a name test could never fail.
4435
+
t('⛔ no compression codec: this file imports nothing from zlib',/from'(node:)?zlib'/.test(ownSource)===false&&/require\('(node:)?zlib'\)/.test(ownSource)===false);
4436
+
}
4437
+
4359
4438
// The floor, evaluated last: a battery that stops running names itself here.
st_case 'a command under auto in a cloud container WITHOUT a session is direct (the seat has not opted in): it runs, and the line names OS_FLEET_SESSION' \
253
+
st_case 'a command under auto in a cloud container WITHOUT a session from either source is direct: it runs, and the line names OS_FLEET_SESSION' \
st_case 'a command under auto with the session DERIVED from the container (no OS_FLEET_SESSION) is REFUSED (3) with the relay spelling, the line naming the container variable, and no token reached it' \
0 commit comments