fix(runtime): normalize provider cooldown state path

Provider cooldown persistence now follows AGENT_STATUS_DIR, then legacy CLAWDIE_VAR_DIR, and otherwise defaults to repo-local tmp/state instead of ~/.clawdie/state. Updates docs to match the live behavior.

---
Build: pass | Tests: pass — 28 passed (2 files)

---
Build: pass | Tests: pass — Tests  1961 passed (1961)
This commit is contained in:
Operator & Codex 2026-04-26 16:37:21 +02:00
parent 32e671c802
commit 6983415357
3 changed files with 62 additions and 14 deletions

View file

@ -88,18 +88,14 @@ one more cost surface to monitor.
4. The cooldown auto-expires at the reset timestamp. Next run uses the primary
again.
The cooldown file lives at `$CLAWDIE_VAR_DIR/provider-cooldowns.json` (default
`$HOME/.clawdie/state/provider-cooldowns.json`). Expired entries are dropped
on load.
The cooldown file now resolves with the same project-local precedence used by
other operator artifacts:
> **Path convention note.** The cooldown file currently uses the legacy
> `$CLAWDIE_VAR_DIR` / `$HOME/.clawdie/state/` resolution. The newer
> [test/build status files](./structured-reports/#test-build-pipeline)
> moved to repo-local `tmp/` to align with `AGENTS.md` § "Temporary File
> Storage". A future code change should harmonize provider-fallback to the
> same precedence (`AGENT_STATUS_DIR``CLAWDIE_VAR_DIR``tmp/state/`).
> Until then, if you set `AGENT_STATUS_DIR`, also set `CLAWDIE_VAR_DIR` to
> the same path so both subsystems agree.
- `AGENT_STATUS_DIR/provider-cooldowns.json`
- otherwise `CLAWDIE_VAR_DIR/provider-cooldowns.json` (legacy compatibility)
- otherwise repo-local `tmp/state/provider-cooldowns.json`
Expired entries are dropped on load.
## Inspecting State

View file

@ -20,6 +20,59 @@ beforeEach(() => {
resetProviderCooldownsForTests();
});
describe('default persistence path', () => {
const previousAgentStatusDir = process.env.AGENT_STATUS_DIR;
const previousLegacyVarDir = process.env.CLAWDIE_VAR_DIR;
beforeEach(() => {
delete process.env.AGENT_STATUS_DIR;
delete process.env.CLAWDIE_VAR_DIR;
});
afterEach(() => {
if (previousAgentStatusDir === undefined) delete process.env.AGENT_STATUS_DIR;
else process.env.AGENT_STATUS_DIR = previousAgentStatusDir;
if (previousLegacyVarDir === undefined) delete process.env.CLAWDIE_VAR_DIR;
else process.env.CLAWDIE_VAR_DIR = previousLegacyVarDir;
});
it('defaults to repo-local tmp/state when no env override is set', async () => {
const now = new Date('2026-04-25T18:00:00');
markProviderBlocked('zai', new Date('2026-04-25T19:00:00'), 'cap', now);
await persistProviderCooldowns();
const expected = path.resolve(
process.cwd(),
'tmp',
'state',
'provider-cooldowns.json',
);
const raw = await fs.readFile(expected, 'utf8');
expect(raw).toContain('"provider": "zai"');
await fs.rm(expected, { force: true });
});
it('prefers AGENT_STATUS_DIR over the legacy CLAWDIE_VAR_DIR', async () => {
process.env.AGENT_STATUS_DIR = path.resolve(process.cwd(), 'tmp', 'status-a');
process.env.CLAWDIE_VAR_DIR = path.resolve(process.cwd(), 'tmp', 'status-b');
const now = new Date('2026-04-25T18:00:00');
markProviderBlocked('zai', new Date('2026-04-25T19:00:00'), 'cap', now);
await persistProviderCooldowns();
const preferred = path.join(
process.env.AGENT_STATUS_DIR,
'provider-cooldowns.json',
);
const legacy = path.join(
process.env.CLAWDIE_VAR_DIR,
'provider-cooldowns.json',
);
await expect(fs.readFile(preferred, 'utf8')).resolves.toContain(
'"provider": "zai"',
);
await expect(fs.access(legacy)).rejects.toBeDefined();
await fs.rm(preferred, { force: true });
});
});
describe('parseProviderCapError', () => {
it('extracts the reset timestamp from a zAI cap error', () => {
const now = new Date('2026-04-25T18:00:00');

View file

@ -43,12 +43,11 @@ const ZAI_CAP_PATTERN_NO_RESET = /\b429\s+Usage limit reached\b/iu;
const DEFAULT_COOLDOWN_SECONDS = 60 * 60; // 1 hour fallback when no reset stamp
function defaultPersistencePath(): string {
const explicit = process.env.CLAWDIE_VAR_DIR;
const explicit = process.env.AGENT_STATUS_DIR || process.env.CLAWDIE_VAR_DIR;
if (explicit && explicit.trim()) {
return path.join(explicit.trim(), 'provider-cooldowns.json');
}
const home = process.env.HOME || '/var/db/clawdie';
return path.join(home, '.clawdie', 'state', 'provider-cooldowns.json');
return path.resolve(process.cwd(), 'tmp', 'state', 'provider-cooldowns.json');
}
export function getFallbackPolicy(): FallbackPolicy {