fix(ship): a poll settles a reader only at done, failed or a final summary, never a fresh idle; return settles the child's open step reservation (GSTA-167 replay 7e)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-30 16:51:44 +00:00
1 parent ad21fcef4c
commit 962efcf247
9 files changed
+40 -8

No files matched your search

+1 -1
View File
@@ -46,7 +46,7 @@ Capy checklist for `/autoplan`: drive `gstack-autoplan`, never a hand adaptation
Token caps on /ship: pass a brief's cap as `--token-cap <n>`. /ship reads `cost.tokens` and `cost.credits` from `tools.thread.get({ thread_id: "<your own jam id>" })` (its own jam, subagents included; `"this"` names the root thread, so only a root run may pass it) inside the same execute program as the `gstack-ship-budget` command that takes them as `--reading <n> --credits <c>`, never in a call of its own. `ship/DRIVER.md` starts `--mode observe`: each program ends with `note`, and only a note past the cap stops (`docs/ship-budget.md`).
/ship on Capy is about a dozen calls, each one execute program (`ship/DRIVER.md`). A review pass, Step 11 and the Step 14.5 audit each run inside one program, in order: (1) `gstack-review-plan --base <base>` with the pass's flags (its `DISPATCH:` lines); (2) `gstack-review-log --start <skill>` once per row the step logs (under ship/SKILL.md's admission mode, add `--reservation <id>` from check's `RESERVATION_ID:` line); (3) `BEFORE`, the counter's tokens and credits; (4) `tools.subagent.create`, one reader each, `machine: "shared"`, its brief copied verbatim from the section that dispatches it; audit readers (Steps 7, 8, 9.2.1, 14.5) cannot block, so they add `model: { model_id: "anthropic/claude-sonnet-5-5", reasoning_mode: "low" }` (`anthropic/claude-haiku-5-5` also fits); (5) a poll of `tools.subagent.get` with `transcript: "none"` every ~20 s for at most 240 s; (6) `gstack-ship-budget note --step <n> --reading <AFTER> --credits <c> --dispatched-at <BEFORE> --dispatched-credits <c> --readers <n>`; (7) return only each reader's final output, or what still runs. The parent never walks the dispatch steps call by call. Waits: never let 5 minutes pass between calls. During tests, pregate, readers and CI, re-issue `wait` with `duration_seconds: 240` and the operation ids: a call after a longer gap bills the whole context fresh (about 40x a cached call), and a ping costs a few percent of that. A small waiting child saves nothing: the parent's cache expires during its wait too.
/ship on Capy is about a dozen calls, each one execute program (`ship/DRIVER.md`). A review pass, Step 11 and the Step 14.5 audit each run inside one program, in order: (1) `gstack-review-plan --base <base>` with the pass's flags (its `DISPATCH:` lines); (2) `gstack-review-log --start <skill>` once per row the step logs (under ship/SKILL.md's admission mode, add `--reservation <id>` from check's `RESERVATION_ID:` line); (3) `BEFORE`, the counter's tokens and credits; (4) `tools.subagent.create`, one reader each, `machine: "shared"`, its brief copied verbatim from the section that dispatches it; audit readers (Steps 7, 8, 9.2.1, 14.5) cannot block, so they add `model: { model_id: "anthropic/claude-sonnet-5-5", reasoning_mode: "low" }` (`anthropic/claude-haiku-5-5` also fits); (5) a poll of `tools.subagent.get` with `transcript: "none"` every ~20 s for at most 240 s, where a reader is settled only at status `done` or `failed` or with a `summary` (a just-created `idle` reader has not run); (6) `gstack-ship-budget note --step <n> --reading <AFTER> --credits <c> --dispatched-at <BEFORE> --dispatched-credits <c> --readers <n>`; (7) return only each reader's final output, or what still runs. The parent never walks the dispatch steps call by call. Waits: never let 5 minutes pass between calls. During tests, pregate, readers and CI, re-issue `wait` with `duration_seconds: 240` and the operation ids: a call after a longer gap bills the whole context fresh (about 40x a cached call), and a ping costs a few percent of that. A small waiting child saves nothing: the parent's cache expires during its wait too.
## Plan, approve, ship
+2 -1
View File
@@ -26,7 +26,7 @@ import { wtreeFingerprint } from './script-invocation';
import { resultLine } from './result-codes';
import {
MAX_CHAIN_DEPTH, SHIP_STEPS, budgetError, budgetLine, ledgerFileFor, loadCalibration, mintRunId, mutateLedger, parseStep, pointMarker,
projector, readLedger, reserveIn, runDirFor, segmentEstimate, tally, usageError, writeLedger, type Handoff, type Ledger,
projector, readLedger, reserveIn, runDirFor, segmentEstimate, settleIn, tally, usageError, writeLedger, type Handoff, type Ledger,
} from './ship-budget';
import { loadMeterState, saveMeterState } from './claude-usage';
import { parseArgs, readMeter, stopLines, str, tokensOpt, writeReturnFile, type Ctx, type Outcome } from './ship-budget-cli';
@@ -218,6 +218,7 @@ export function cmdReturn(argv: string[], ctx: Ctx): Outcome {
return mutateLedger(ledgerFileFor({ run: str(o, 'run') }, ctx.env), ctx.env, l => {
if (l.role !== 'child' || !l.parent) throw usageError(`run ${l.run} is not a child run`);
readMeter(l, o, false);
for (const r of l.reservations) if (r.kind === 'step' && r.state === 'open') settleIn(l, r);
const head = git(ctx.cwd, ['rev-parse', 'HEAD']);
const tree = pinsNow(ctx.cwd, 'HEAD', ctx.env).wtree;
const accepted = new Set((l as any).accepted_skips as string[] | undefined ?? []);
+2 -1
View File
@@ -46,7 +46,8 @@ waiting child saves nothing: the parent's cache expires during its wait too. Pas
parent makes no other call while it runs: (1) `gstack-review-plan --base <base>` with the pass's
flags (its `DISPATCH:` lines; Step 14.5 has one reader, `docs`); (2) `gstack-review-log --start
<skill>` once per row the step logs (Step 14.5 takes none); (3) `BEFORE`; (4) one subagent per reader;
(5) a poll until every reader settles; (6) `note` with the reading pair; a later batch (the Red Team,
(5) a poll until every reader settles (status `done` or `failed`, or a final summary; a fresh `idle`
has not run); (6) `note` with the reading pair; a later batch (the Red Team,
given the specialists' collected lines) repeats (3)-(6); (7) the collect: return only each reader's
final output and status. The next call applies the section's merge, judgment, fixes and persistence;
it never walks the dispatch steps call by call.
+2 -1
View File
@@ -44,7 +44,8 @@ waiting child saves nothing: the parent's cache expires during its wait too. Pas
parent makes no other call while it runs: (1) `gstack-review-plan --base <base>` with the pass's
flags (its `DISPATCH:` lines; Step 14.5 has one reader, `docs`); (2) `gstack-review-log --start
<skill>` once per row the step logs (Step 14.5 takes none); (3) `BEFORE`; (4) one subagent per reader;
(5) a poll until every reader settles; (6) `note` with the reading pair; a later batch (the Red Team,
(5) a poll until every reader settles (status `done` or `failed`, or a final summary; a fresh `idle`
has not run); (6) `note` with the reading pair; a later batch (the Red Team,
given the specialists' collected lines) repeats (3)-(6); (7) the collect: return only each reader's
final output and status. The next call applies the section's merge, judgment, fixes and persistence;
it never walks the dispatch steps call by call.
+2 -1
View File
@@ -36,7 +36,8 @@ after they settle; `close` flags roles never re-checked. `--finish` settles the
the same order): `gstack-review-plan` (its `DISPATCH:` lines), `check --step <n> --next dispatch:<roles>`,
`gstack-review-log --start <skill> --reservation <id>` once per
row the step logs, the reading `BEFORE`, the readers exactly as the step dispatches them (their
prompts unchanged), the wait until the last settles, the reading `AFTER`, `gstack-ship-budget settle`;
prompts unchanged), the wait until the last settles (`done`, `failed` or a final summary; a fresh `idle`
has not run), the reading `AFTER`, `gstack-ship-budget settle`;
a later batch of the same pass (the Red Team or an ADMIT remainder) takes no start and repeats the
rest (`--finish` settles it); last the collect, returning only the readers' final outputs. Step 11's
voices and Step 14.5's audit (`dispatch:docs`, no start) are one program each.
+2 -1
View File
@@ -34,7 +34,8 @@ after they settle; `close` flags roles never re-checked. `--finish` settles the
the same order): `gstack-review-plan` (its `DISPATCH:` lines), `check --step <n> --next dispatch:<roles>`,
`gstack-review-log --start <skill> --reservation <id>` once per
row the step logs, the reading `BEFORE`, the readers exactly as the step dispatches them (their
prompts unchanged), the wait until the last settles, the reading `AFTER`, `gstack-ship-budget settle`;
prompts unchanged), the wait until the last settles (`done`, `failed` or a final summary; a fresh `idle`
has not run), the reading `AFTER`, `gstack-ship-budget settle`;
a later batch of the same pass (the Red Team or an ADMIT remainder) takes no start and repeats the
rest (`--finish` settles it); last the collect, returning only the readers' final outputs. Step 11's
voices and Step 14.5's audit (`dispatch:docs`, no start) are one program each.
+19
View File
@@ -83,6 +83,25 @@ describe('the Capy meter is the agent\'s own jam counter', () => {
expect(budget).toContain('On Capy pass no --reading or --credits: the parent charges child_used from its own counter around your work');
});
// Value: protects=a child's return settles its open step reservation, so no step reservation stays open in the child ledger after it hands back; fails_when=return leaves the last step's reservation open (replay 7e #36: return left the Step 17 reservation r8, 742,152 tokens, open);
// why_new=replay 7e; seam=none; no_claim=the settled amount's accuracy
test('return settles the child\'s open Step 17 reservation', () => {
const h = fresh();
const s = h.sb(['start', '--record', h.record(), '--token-cap', '400M', '--reading', '1000000']);
const ho = h.sb(['handoff', '--run', h.field(s.out, 'BUDGET_RUN', 'id'), '--from', '12', '--to', '22', '--base', 'origin/main', '--reading', '1500000']);
const v = h.sb(['verify-handoff', '--in', ho.out.match(/^HANDOFF: (\S+) /m)![1]!, '--reading', '200000']);
expect(v.code, v.out + v.err).toBe(0);
const child = h.field(v.out, 'BUDGET_RUN', 'id');
const c = h.sb(['check', '--run', child, '--step', '16', '--next', '17', '--reading', '900000']);
expect(c.code, c.out + c.err).toBe(0);
const id = h.field(c.out, 'RESERVATION', 'id');
const ret = h.sb(['return', '--run', child, '--to', '22', '--reading', '1600000']);
expect(ret.code, ret.out + ret.err).toBe(0);
const ledger = h.ledger(h.field(v.out, 'BUDGET_RUN', 'ledger'));
expect(ledger.reservations.find((r: any) => r.id === id).state).toBe('settled');
expect(ledger.reservations.filter((r: any) => r.state === 'open')).toEqual([]);
});
// Value: protects=a Capy child with no jam id of its own verifies, checks, returns and is reconciled from the parent's counter delta; fails_when=verify-handoff or a child check refuses with BUDGET_READING_MISSING, an unknown charge delegates the child onward, or reconcile charges the child's own BUDGET used=0 instead of the parent's delta;
// why_new=replay 7e #36 (handoffs/GSTA-167/replay7e-36.md); seam=none; no_claim=that the parent's counter excludes its own pings
test('a child handoff passes verify-handoff with no jam id of its own and the parent measures it', () => {
+8
View File
@@ -187,6 +187,14 @@ describe('variant G: the call plan, observe budget, waits and audit readers (GST
for (const text of [doc, capy]) expect(text).not.toMatch(/\b270 ?s\b/);
});
// Value: protects=a reader counts as settled only once it has run (status done or failed, or a final summary), in the driver, the Capy skill and the budget section, so a poll never closes a batch on a subagent that was just created; fails_when=the settled rule is dropped or lets a bare idle status through (replay 7e #36: the Red Team reservation settled actual=0 because a just-created subagent read idle);
// why_new=replay 7e; seam=none; no_claim=the host's status semantics beyond done, failed, idle and summary
test('a poll settles a reader only at done, failed or a summary, never on a fresh idle', () => {
expect(flat(doc)).toContain('(5) a poll until every reader settles (status `done` or `failed`, or a final summary; a fresh `idle` has not run)');
expect(read('capy/skill/gstack/SKILL.md')).toContain('a reader is settled only at status `done` or `failed` or with a `summary` (a just-created `idle` reader has not run)');
expect(flat(read('ship/sections/budget-handoff.md.tmpl'))).toContain('the wait until the last settles (`done`, `failed` or a final summary; a fresh `idle` has not run)');
});
// Value: protects=audits that cannot block (Steps 7, 8, 9.2.1, 14.5) run on a small model at low effort with their sections' own briefs, and skip their reader when --audit decides them; fails_when=an audit reader inherits the lane's model and effort (#36: the coverage audit alone was 2.45M at Opus), or an audit brief is paraphrased;
// why_new=audit model routing is new; seam=none; no_claim=the audits' recall
test('audit readers run on a small model at low effort, named for Capy, and Phase 3 dispatches only what READER names', () => {
+2 -2
View File
@@ -1,4 +1,4 @@
{
"bytes": 1548,
"why": "GSTA-167: ship preflight/release bins and the compact driver (+841); variant E: budget-handoff.md runs one program per review pass and hands Steps 9-11.5 to the review child (+362); variant G: budget-handoff.md points driver runs at observe mode, reads credits beside tokens and verifies a Capy child with no reading (+345)"
"bytes": 1614,
"why": "GSTA-167: ship preflight/release bins and the compact driver (+841); variant E: budget-handoff.md runs one program per review pass and hands Steps 9-11.5 to the review child (+362); variant G: budget-handoff.md points driver runs at observe mode, reads credits beside tokens, verifies a Capy child with no reading and settles a reader only once it has run (+411)"
}