autoplan shadow: reply files never follow a link; the retry reserves against the current cap

- a model reply is written by temp file and rename, so a link at the reply path is replaced, not written through
- each shadow reservation reads the run's current spend cap, so a budget change during the call is not undone

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-11-01 19:29:05 +00:00
1 parent 3bc7341b02
commit d5bf54316d
2 files changed
+28 -2

No files matched your search

+2 -2
View File
@@ -308,7 +308,7 @@ export async function runShadow(out: string, claim: ShadowClaim, o: RunShadowOpt
const remaining = deadline - Date.now();
if (remaining <= 0) throw new ShadowFailure('timeout', `the ${(o.deadlineMs ?? SHADOW_DEADLINE_MS) / 1000} s deadline passed before the call`);
const attempt = `shadow-r${rev}-${callIds(readJournal(out), rev).length + 1}`;
try { reserve(ledgerFile, attempt, estimate(maxTokens), { cap: state.spend_cap_usd, label: 'shadow' }); }
try { reserve(ledgerFile, attempt, estimate(maxTokens), { cap: readState(out).spend_cap_usd, label: 'shadow' }); }
catch (e: any) { throw new ShadowFailure('budget', e.message); }
journal(out, { event: 'shadow_dispatched', gate_rev: rev, attempt, model, max_tokens: maxTokens, estimate_usd: estimate(maxTokens), retrospective: claim.retrospective });
let result: ApiCallResult;
@@ -322,7 +322,7 @@ export async function runShadow(out: string, claim: ShadowClaim, o: RunShadowOpt
const { input_tokens: inTok, output_tokens: outTok } = result.usage;
const usd: number | 'unknown' = typeof inTok === 'number' && typeof outTok === 'number' ? estimateCostUsd({ input: inTok, output: outTok }, model) : 'unknown';
settle(ledgerFile, attempt, usd);
fs.writeFileSync(path.join(shadowDir(out), `${attempt}.reply.txt`), result.text);
writeAtomic(path.join(shadowDir(out), `${attempt}.reply.txt`), result.text);
const reply = { result, usd, attempt };
replies.push(reply);
return reply;
+26
View File
@@ -216,6 +216,32 @@ describe('runShadow: retry, truncation, failures and the deadline', async () =>
expect(readLedger(path.join(out, 'spend.json')).settled.filter(s => s.label === 'shadow').map(s => [s.attempt, s.usd])).toEqual([['shadow-r1-1', 0.014]]);
expect(fs.existsSync(path.join(out, 'shadow', 'r1.pending'))).toBe(false);
});
test('a link at a reply path is replaced, never written through', async () => {
const f = await fake('linked-reply', [{ mode: 'valid' }]);
const out = gateRun();
const { claim } = claimShadow(out);
const sentinel = path.join(TMP, 'sentinel.txt');
fs.writeFileSync(sentinel, 'keep');
const reply = path.join(out, 'shadow', 'shadow-r1-1.reply.txt');
fs.symlinkSync(sentinel, reply);
expect((await runShadow(out, claim!, { env: { ...process.env, ...f.env } })).status).toBe('recorded');
expect(fs.readFileSync(sentinel, 'utf8')).toBe('keep');
expect(fs.lstatSync(reply).isSymbolicLink()).toBe(false);
expect(fs.readFileSync(reply, 'utf8')).toContain('fake prediction');
});
test('a cap changed during the call admits the retry against the new cap and is not written back over', async () => {
const f = await fake('cap-moved', [{ mode: 'truncated', delay_ms: 400 }, { mode: 'valid' }]);
const out = gateRun();
const { claim } = claimShadow(out);
const pending = runShadow(out, claim!, { env: { ...process.env, ...f.env } });
await Bun.sleep(150);
const run = readRun(out);
run.spend_cap_usd = 2;
fs.writeFileSync(path.join(out, 'run.json'), JSON.stringify(run, null, 2) + '\n');
expect((await pending).status).toBe('recorded');
expect(f.requests().length).toBe(2);
expect(readLedger(path.join(out, 'spend.json')).cap_usd).toBe(2);
});
test('a malformed page is asked once more; items valid in either reply record, the rest record malformed', async () => {
const fixed = await shadowed('missing-then-valid', [{ mode: 'missing' }, { mode: 'valid' }]);
expect(fixed.f.requests().length).toBe(2);