ship-budget: a ledger rebuilt from its record keeps meter_scope=tree; document the cap on a tree run

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-31 14:05:37 +00:00
1 parent 94c8962e74
commit e05c77089c
4 files changed
+22 -3

No files matched your search

+2 -1
View File
@@ -81,7 +81,8 @@ reads `"this"` and starts with `--subagent --reading-from this --meter-scope tre
`GSTACK_SHIP_SUBAGENT=1` in its brief): the run is tree-scoped, and every BUDGET, NOTE, BUDGET_FINAL
and BUDGET_TIMELINE line it prints is followed by `METER: tree (root counter, upper bound)`. A
subagent that asks for `--meter-scope self` while reading `"this"` is refused
(`METER_SCOPE_PARENT`). Exact per-run cost comes from a root replay thread: a root thread that runs
(`METER_SCOPE_PARENT`). A `--token-cap` on a tree-scoped run stops on that upper bound, so sibling
lanes count against it. Exact per-run cost comes from a root replay thread: a root thread that runs
only that /ship, whose `"this"` is its own counter. A root thread's own jam id is its `"this"`. A Capy handoff child cannot read its own jam id, so it
verifies with no reading and prints `METER: parent`; the parent's handoff and reconcile readings,
taken on its own counter (which holds the child), measure it.
+1 -1
View File
@@ -477,7 +477,7 @@ function rebuildLedger(file: string, record: string | undefined, ctx: Ctx, o: Op
meter: { contract: METER_CONTRACTS[meter], epoch: num('epoch') + 1, baseline: meter === 'capy' ? reading! : 0, last: meter === 'capy' ? reading! : null },
epochs: [], carried_used: num('used'), estimate_used: 0, unknown: num('unknown') + num('reserved'), next_id: 1, reservations: [], handoffs: [], cap_raises: [],
...(() => { try { return { marker: reviewPaths(ctx.cwd, ctx.env).marker, review_log: reviewPaths(ctx.cwd, ctx.env).reviewLog }; } catch { return {}; } })(),
...(b.mode === 'observe' ? { mode: 'observe' as const } : {}),
...(b.mode === 'observe' ? { mode: 'observe' as const } : {}), ...(b.meter_scope === 'tree' ? { meter_scope: 'tree' as const } : {}),
};
if (meter === 'estimate') l.estimate_used = 0;
fs.mkdirSync(l.dir, { recursive: true, mode: 0o700 });
+1 -1
View File
@@ -477,7 +477,7 @@ export function recordBlock(l: Ledger): string {
const t = tally(l);
const open = l.handoffs.filter(h => !h.reconciled_at).map(h => h.sha).join(',') || 'none';
const credits = creditsUsed(l);
return `run=${l.run} ledger=${l.file} role=${l.role} cap=${l.cap ?? 'none'} tail=${l.tail} unit=${l.meter.contract.unit} meter=${l.meter.contract.meter} epoch=${l.meter.epoch} baseline=${l.meter.baseline} used=${t.used}${credits === null ? '' : ` credits=${credits}`} reserved=${t.reserved} unknown=${t.unknown} stop=${l.stopped?.reason ?? 'none'} handoff=${open}${l.mode === 'observe' ? ' mode=observe' : ''}`;
return `run=${l.run} ledger=${l.file} role=${l.role} cap=${l.cap ?? 'none'} tail=${l.tail} unit=${l.meter.contract.unit} meter=${l.meter.contract.meter} epoch=${l.meter.epoch} baseline=${l.meter.baseline} used=${t.used}${credits === null ? '' : ` credits=${credits}`} reserved=${t.reserved} unknown=${t.unknown} stop=${l.stopped?.reason ?? 'none'} handoff=${open}${l.mode === 'observe' ? ' mode=observe' : ''}${l.meter_scope === 'tree' ? ' meter_scope=tree' : ''}`;
}
/** The record's fenced gstack-budget block: written by the bin, never by the agent. */
+18
View File
@@ -73,6 +73,24 @@ describe('meter scope (GST-467)', () => {
}
});
// Value: protects=a tree run rebuilt from its record after a lost ledger keeps the label; fails_when=the record block drops meter_scope or rebuildLedger does not restore it (red-team, ship gst467-p1);
// why_new=the cases above never lose the ledger; seam=none; no_claim=checkpoint restore, which copies the whole ledger
test('a tree run rebuilt from its record still labels its cost lines', () => {
const h = fresh();
const record = h.record();
const s = h.sb(['start', '--record', record, '--mode', 'observe', '--reading', '1000000', '--meter-scope', 'tree']);
const ledger = h.field(s.out, 'BUDGET_RUN', 'ledger');
fs.writeFileSync(ledger, '{"kind": "gstack-ship-budget", oops');
const rebuilt = h.sb(['resume', '--record', record, '--rebuild-from-record', '--reading', '1500000']);
expect(rebuilt.out, rebuilt.out + rebuilt.err).toContain('REBUILT:');
const close = h.sb(['close', '--record', record, '--reading', '2000000']);
for (const out of [rebuilt, close]) {
const labels = costLinesLabeled(out.out);
expect(labels.length, out.out).toBeGreaterThan(0);
expect(labels.every(Boolean), out.out).toBe(true);
}
});
// Value: protects=a subagent cannot claim its own cost while reading the root counter; fails_when=start accepts --meter-scope self from a subagent metering "this", or the refusal lacks its code and fix;
// why_new=GST-467; seam=none; no_claim=whether the brief sets the subagent flag
test('a subagent metering "this" that asks for a self scope is refused with METER_SCOPE_PARENT and a fix', () => {