fix(cap-hook): shrink the inherited deny window and print the way out of it
DEFAULT_TTL_MS was 6h, measured from marker.startedAt rather than last
activity, and `claude --resume` keeps the same session_id - so a run that
died holding its marker handed the resumed session the remainder of that
window, denying every WebSearch/WebFetch/Task including work unrelated to
research. The header's design goal ("An unrelated session must never be
denied") held across sessions and read as broader than it was: by this
scope key a resume IS the same session.
Three changes, none of which pretends to close it:
- The tombstone boundary (32e20fc) already removed the common case. A run
that crashed MID-loop leaves no denial record, so the resume is allowed;
only a crash AFTER the cap denied a turn opens a window at all. Pinned by
a test with a part-spent ledger and no tombstone.
- TTL 6h -> 2h. A 24-turn loop at a couple of minutes per turn is under an
hour, so nothing needed six, and debris no longer owns the rest of the
working day.
- Every denial now prints the marker path with "if this loop is not
running, delete it", plus the auto-reset horizon. The window existed
before with no stated remedy, which is what made it expensive.
A liveness check would close it properly. This hook has nothing
trustworthy to check liveness against - the marker's writer is a shell
snippet whose $$ is a subshell, not the session - so the limit is written
into the header as a limit instead of being papered over.
Review finding d913d1b655012fe206ea925b9fc77b401566a39e (MINOR).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LuGhWAbWyRFBFeemfhxoVv
This commit is contained in:
parent
9dfdc4a42f
commit
22cb7df403
4 changed files with 80 additions and 3 deletions
|
|
@ -177,6 +177,50 @@ test('pre-agent-cap ALLOWS when the scope marker is older than the TTL', async (
|
|||
assert.strictEqual(code, 0, 'a stale marker must auto-reset, not deny forever');
|
||||
});
|
||||
|
||||
// The TTL runs from marker.startedAt, not from last activity, and `claude
|
||||
// --resume` keeps the same session_id — so a run that died leaving its marker
|
||||
// behind hands the resumed session whatever deny window is left. Two things
|
||||
// bound that: the window is hours, not the machine's life (below), and it only
|
||||
// opens at all once the budget gate has actually denied a turn.
|
||||
test('pre-agent-cap ALLOWS a resumed session whose crashed run never exhausted its budget', async () => {
|
||||
// Marker still fresh, ledger part-spent, no tombstone: the run died mid-loop.
|
||||
const dir = fixture({ turns: 5 });
|
||||
const { code } = await runHookWithEnv(CAP_HOOK, searchInput(), {
|
||||
...CAPPED_ENV,
|
||||
CLAUDE_PLUGIN_DATA: dir,
|
||||
});
|
||||
assert.strictEqual(
|
||||
code, 0,
|
||||
'a part-spent run leaves no denial record, so resuming its session must not brick unrelated work',
|
||||
);
|
||||
});
|
||||
|
||||
test('pre-agent-cap uses a default TTL of hours, not a day — a 3h-old marker auto-resets', async () => {
|
||||
const dir = fixture({
|
||||
turns: BUDGET,
|
||||
exhausted: true,
|
||||
startedAt: new Date(Date.now() - 3 * 3600 * 1000),
|
||||
});
|
||||
const { code } = await runHookWithEnv(CAP_HOOK, searchInput(), {
|
||||
...CAPPED_ENV,
|
||||
CLAUDE_PLUGIN_DATA: dir,
|
||||
}); // no VOYAGE_CAP_SCOPE_TTL_MS — this is the built-in default
|
||||
assert.strictEqual(code, 0, 'no real research run lasts 3h; a marker that old is debris');
|
||||
});
|
||||
|
||||
test('pre-agent-cap names the marker path when it denies, so the operator has a remedy', async () => {
|
||||
const dir = fixture({ turns: BUDGET, exhausted: true });
|
||||
const { code, stderr } = await runHookWithEnv(CAP_HOOK, searchInput(), {
|
||||
...CAPPED_ENV,
|
||||
CLAUDE_PLUGIN_DATA: dir,
|
||||
});
|
||||
assert.strictEqual(code, 2);
|
||||
assert.ok(
|
||||
stderr.includes(join(dir, 'trekresearch-loop-scope', `${SESSION}.json`)),
|
||||
`stderr must name the marker to delete; got:\n${stderr}`,
|
||||
);
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Fail CLOSED once in scope — the hook's own header says a budget control
|
||||
// that cannot count must not grant. The unreadable-ledger branch returned 0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue