mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 22:45:02 +01:00
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:
1 parent
94c8962e74
commit
e05c77089c
4 files changed
+22
-3
No files matched your search
+2
-1
@@ -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.
|
||||
|
||||
@@ -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
@@ -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. */
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in new issue
Block a user