mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-11 03:00:26 +01:00
* ship: token cap with real meters and fresh-context delegation (GSTA-152 PR A) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (stopped or lost capped run no longer blocks uncapped review starts; short child return reconciles incomplete) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: carry estimate spend across a meter epoch change; calibration docs match their evidence Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: load the committed calibration file by its repo path so pregate maps it to a lane Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ship: fix fence placeholders and register claude-usage fixture (GSTA-152 PR A) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: run ship-budget and review-log bins through their interpreters so Windows can launch them Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
160 lines
8.6 KiB
Markdown
160 lines
8.6 KiB
Markdown
# /ship token budget and fresh-context handoffs
|
||
|
||
`/ship --token-cap 25M` (or `GSTACK_SHIP_TOKEN_CAP=25M`) bounds what one /ship run spends, and
|
||
long runs hand their heavy segments to fresh-context children. The helper is
|
||
`bin/gstack-ship-budget`; the skill's procedure is `ship/sections/budget-handoff.md`.
|
||
There is no default cap: without one, /ship meters usage when it can and prints it, and
|
||
nothing else changes.
|
||
|
||
## What the cap guarantees
|
||
|
||
It is an **admission cap**, not a hard ceiling. Before every step and every dispatch batch,
|
||
`check` admits the work only when
|
||
|
||
used + open reservations + unknown charges + projection + tail reserve + stop overhead <= cap
|
||
|
||
Exactly at the cap is admitted; one token over is not. A parallel review batch is reserved as
|
||
one sum before any of it starts. Admitted work can still cost more than its projection, and
|
||
gstack cannot stop a host mid-turn from bash, so a run can finish above the cap by the
|
||
misprojection of work already admitted. The tail reserve (default 2M, `--reserve <n>`) keeps
|
||
room for the receipt steps, and the stop overhead (calibrated) keeps room to stop cleanly.
|
||
|
||
## Quickstart
|
||
|
||
**Capy.** Read the root counter in one execute program that returns only that number:
|
||
|
||
```js
|
||
return (await tools.thread.get({ thread_id: "this" })).cost.tokens;
|
||
```
|
||
|
||
then
|
||
|
||
```bash
|
||
gstack-ship-budget start --record <record> --token-cap 25M --reading <n>
|
||
gstack-ship-budget check --run <id> --step 6 --next 7 --reading <n> [--prior-reading <n from your previous call>]
|
||
```
|
||
|
||
The counter rolls up every subagent of the thread, outside voices included; Capy exposes no
|
||
per-subagent counter, so other work in the same thread counts too (it only stops earlier).
|
||
|
||
**Claude Code.** `gstack-ship-budget start --record <record> --token-cap 25M --session <id>`. Without the
|
||
session id, `start --meter claude` prints a nonce: echo it once, then rerun with `--nonce <value>`.
|
||
The helper reads the session journal, its subagent journals and the journals of headless
|
||
children it launched; you never pass `--reading`.
|
||
|
||
**Anywhere else.** `--meter estimate` projects with the calibration and prints `meter=estimate`.
|
||
With a cap set and no readable meter, start refuses
|
||
([BUDGET_METER_UNAVAILABLE](troubleshooting.md#budget-meter-unavailable)).
|
||
|
||
## The BUDGET line
|
||
|
||
BUDGET: used=<n> reserved=<n> unknown=<n> admission_cap=<n|none> tail=<n> unit=<u> meter=<m> epoch=<n> call=<n|unknown> next=<id> projected=<n> verdict=continue|delegate|stop
|
||
|
||
| Field | Meaning |
|
||
|---|---|
|
||
| `used` | Metered usage since start (all epochs), or settled estimates under `meter=estimate` |
|
||
| `reserved` | Open reservations: admitted steps, started dispatches, and each open child's sub-cap counted as max(sub_cap, child_used), never both |
|
||
| `unknown` | Charges the meter cannot see (an external-CLI outside voice on Claude Code), counted at their reservation |
|
||
| `admission_cap` | The cap, or `none` |
|
||
| `tail` | Tail reserve plus stop overhead, held back from every admission |
|
||
| `unit`, `meter`, `epoch` | The meter contract: `capy-tokens` (root counter), `claude-usage-tokens` (journal usage), `estimate-tokens`; a meter change opens a new epoch |
|
||
| `call` | The per-call charge: two Capy readings in consecutive calls, or the last Claude Code call's usage |
|
||
| `next`, `projected` | What the check was for and its calibrated projection |
|
||
| `verdict` | `continue` (exit 0, prints `RESERVATION:` under a cap), `delegate` (exit 0), `stop` (exit 3) |
|
||
|
||
Any exit other than 0 means the next step does not start. Exit 2 is a usage or validation
|
||
error on stderr with the code, the value and the repair. GSTACK_RESULT appears only on a stop,
|
||
an incomplete close and a child's return; /ship's real terminal line stays Step 22's.
|
||
|
||
## Dispatches
|
||
|
||
Under a cap, `gstack-review-log --start` refuses a dispatch without an unspent, one-shot
|
||
reservation ([BUDGET_CHECK_SKIPPED](troubleshooting.md#budget-check-skipped)):
|
||
|
||
```bash
|
||
gstack-ship-budget check --run <id> --step 9 --next dispatch:specialist,red-team --reading <n>
|
||
gstack-review-log --start review --reservation <id>
|
||
```
|
||
|
||
Roles are review-depth's: `specialist`, `red-team`, `adversarial-native`, `outside-voice`,
|
||
`fix-check`. `--finish <token>` settles the reservation at the dispatch's completion receipt.
|
||
`close` audits after the fact: each review-log row of the run with no reservation gets one
|
||
BUDGET_CHECK_SKIPPED line and the run ends status=incomplete for the owner to accept
|
||
(`close --approved-by <who> --words "<their words>"`); a reservation is never created after the fact.
|
||
|
||
## Delegation
|
||
|
||
At start, a per-call charge of 100k or more delegates the whole /ship. Before Step 7 and before
|
||
Step 12, 150k or more (or an unknown charge) delegates the next segment (7–11.5, 12–22):
|
||
|
||
1. `gstack-ship-budget handoff --run <id> --from 12 --to 22 --base origin/<base> --reading <n>` writes a
|
||
brief (mode 0600, under the run directory) with the pins (HEAD, merge base, working tree,
|
||
reviewed tree), the record with its six headings and start tokens, the decisions in scope
|
||
with the owner's words, the decision trail, the budget and the return contract. It reserves
|
||
the child's sub-cap: min(cap − used − reserved − tail, the segment estimate), or `--sub-cap`.
|
||
2. The child is a Capy subagent on the shared machine, or on Claude Code
|
||
`gstack-ship-budget launch --in <brief>`: a background `claude -p` (resolved through lib/claude-bin.ts)
|
||
with explicit permission flags and the return file named.
|
||
3. The child runs `verify-handoff` first; a moved pin refuses
|
||
([HANDOFF_PIN_MOVED](troubleshooting.md#handoff-pin-moved)). It admits against its sub-cap
|
||
with child_used = reading − reading at handoff, is the only writer of the record, and never
|
||
delegates again: a delegate rule stops it. It ends with `return` (at most 40 lines).
|
||
4. `gstack-ship-budget reconcile --run <id> --return <file> --reading <n>`; idempotent per handoff hash,
|
||
so a duplicate return charges nothing twice. A refused, incomplete or interrupted child
|
||
exits 3: regenerate the handoff or stop.
|
||
|
||
`--delegate never` turns delegate into stop for hosts without children.
|
||
|
||
## Stop and resume
|
||
|
||
A stop prints
|
||
|
||
STOP: reason=budget_exhausted spent=<n> reserved=<n> unknown=<n> admission_cap=<n> next=<id> projected=<n>
|
||
RECOVER: /ship --resume <record> --token-cap <used + reserved + unknown + projected tail>
|
||
GSTACK_RESULT: skill=ship status=incomplete run=<dir>
|
||
|
||
Raising the cap is the owner's call; the resume journals who raised it and their words:
|
||
|
||
```bash
|
||
gstack-ship-budget resume --record <record> --token-cap <n> --approved-by Garry --words "raise it to finish" --reading <n>
|
||
```
|
||
|
||
Every command takes `--run` or `--record`; runs are never found by branch or directory, and a
|
||
/ship without `--resume` or `--continue` always starts a new run. The ledger and record are
|
||
mirrored to a private Drive checkpoint (`GSTACK_SHIP_BUDGET_CHECKPOINT_DIR`, else the
|
||
project-user Drive folder on Capy) bound to the run id and their hashes, so after a machine
|
||
change `resume --run <id>` restores both. A corrupt ledger
|
||
([BUDGET_LEDGER_INVALID](troubleshooting.md#budget-ledger-invalid)) is rebuilt from the
|
||
record's block with `--rebuild-from-record`; open reservations are then charged as unknown.
|
||
|
||
## Calibration
|
||
|
||
`lib/ship-budget-calibration.json` holds per-meter constants (per call, per dispatch, stop
|
||
overhead, calls and dispatches per step) fitted from metered runs: each constant is the
|
||
largest observation times 1.5, rounded up to 10,000 tokens. The leave-one-out test
|
||
(test/ship-budget-calibration.test.ts) covers the per-call constant only, the one observed
|
||
across three runs; per-dispatch has two observations and stop overhead one. Only the Capy
|
||
meter has observations; the Claude Code and estimate units are seeded from it until a
|
||
Claude-metered /ship completes.
|
||
|
||
## Token cap and USD spend cap
|
||
|
||
A lane's `--spend-cap` (USD) bounds paid gates and evals through lib/spend-ledger.ts; `--token-cap`
|
||
bounds the /ship run's model tokens through its own ledger. They are independent: set both
|
||
when a lane runs paid gates inside /ship, and neither converts into the other.
|
||
|
||
## Claiming savings
|
||
|
||
Savings count completed ships only. Preregister the comparison with `/eval-plan` (arms, bars,
|
||
stop rules) before claiming a saving from the cap or from delegation.
|
||
|
||
## Result codes
|
||
|
||
[BUDGET_CAP_REACHED](troubleshooting.md#budget-cap-reached) ·
|
||
[BUDGET_READING_MISSING](troubleshooting.md#budget-reading-missing) ·
|
||
[BUDGET_READING_REJECTED](troubleshooting.md#budget-reading-rejected) ·
|
||
[BUDGET_METER_UNAVAILABLE](troubleshooting.md#budget-meter-unavailable) ·
|
||
[BUDGET_LEDGER_INVALID](troubleshooting.md#budget-ledger-invalid) ·
|
||
[BUDGET_CHECK_SKIPPED](troubleshooting.md#budget-check-skipped) ·
|
||
[HANDOFF_PIN_MOVED](troubleshooting.md#handoff-pin-moved)
|