diff --git a/src/cli.test.ts b/src/cli.test.ts index 04ba556..0125893 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -1647,6 +1647,7 @@ describe("CLI entry point", () => { const captured = capture(); const automaticReset = (accountId: string) => ({ threshold: { remainingPercent: 1, usedPercent: 99 }, + policy: { state: "active" as const }, observation: { state: "unavailable" as const, reason: "weekly_window_unavailable" as const, diff --git a/src/cli/render.test.ts b/src/cli/render.test.ts index 83e596b..702a198 100644 --- a/src/cli/render.test.ts +++ b/src/cli/render.test.ts @@ -2237,6 +2237,7 @@ describe("CLI rendering", () => { account: { id: "acct_00000000000000000000000000000000", label: "Work" }, automaticReset: { threshold: { remainingPercent: 1, usedPercent: 99 }, + policy: { state: "active" }, observation: { state: "available", creditsAvailable: 1, @@ -2274,6 +2275,7 @@ describe("CLI rendering", () => { expect(rendered).toContain("Work\n"); expect(rendered).toContain("lifetime tokens: 12,345"); expect(rendered).toContain("automatic reset policy: 99% used (1% remaining)"); + expect(rendered).toContain("automatic reset reconciliation: active"); expect(rendered).toContain("weekly Codex limit: 27.5% used; 72.5% remaining"); expect(rendered).toContain("reset credits available: 1"); expect(rendered).toContain("most recent automatic reset attempt: settled (reset)"); @@ -2294,6 +2296,7 @@ describe("CLI rendering", () => { }; const base = { threshold: { remainingPercent: 1, usedPercent: 99 }, + policy: { state: "active" }, observation: { state: "unavailable", reason: "weekly_window_unavailable", @@ -2302,6 +2305,14 @@ describe("CLI rendering", () => { }; for (const automaticReset of [ { ...base, idempotencyKey: "00000000-0000-4000-8000-000000000001" }, + { + ...base, + policy: { + state: "window_suppressed", + weeklyWindowResetsAt: 2_000_000_000_000, + accountFingerprint: "a".repeat(64), + }, + }, { ...base, lastAttempt: { diff --git a/src/cli/render.ts b/src/cli/render.ts index de6f720..6869882 100644 --- a/src/cli/render.ts +++ b/src/cli/render.ts @@ -1454,6 +1454,14 @@ const automaticResetRows = (value: unknown): readonly string[] => { rows.push( ` automatic reset policy: ${threshold.usedPercent.toLocaleString("en-US", { maximumFractionDigits: 2 })}% used (${threshold.remainingPercent.toLocaleString("en-US", { maximumFractionDigits: 2 })}% remaining)`, ); + const reconciliation = root.policy; + rows.push( + reconciliation.state === "active" + ? " automatic reset reconciliation: active" + : reconciliation.state === "reconciliation_required" + ? " automatic reset reconciliation: required before automatic resets" + : ` automatic reset reconciliation: current weekly window suppressed through ${instant(reconciliation.weeklyWindowResetsAt)}`, + ); const observation = root.observation; if ( observation.state === "available" diff --git a/src/daemon/service.test.ts b/src/daemon/service.test.ts index 2025bf2..5718555 100644 --- a/src/daemon/service.test.ts +++ b/src/daemon/service.test.ts @@ -2209,6 +2209,7 @@ describe("HraService", () => { }, { signal })).resolves.toMatchObject({ usage: [{ account: { providerEmail: "other@example.com" }, + automaticReset: { policy: { state: "reconciliation_required" } }, poll: { state: "never_observed" }, snapshot: null, }], @@ -2248,6 +2249,57 @@ describe("HraService", () => { expect(codex.resetIdempotencyKeys).toEqual([]); }); + test("keeps below-threshold usage polling to the identity sandwich", async () => { + const { service, codex } = await fixture(); + const added = await service.execute({ + kind: "account.add", + label: "Below reset threshold", + }, { signal }) as { account: { id: `acct_${string}` } }; + await service.execute({ + kind: "account.login", + account: added.account.id, + deviceCode: false, + }, { signal }); + codex.usageResult = { + revision: 1, + observedAt: 2_000, + payload: { + rateLimits: { + primary: { + limitId: "codex", + primary: { + usedPercent: 50, + windowDurationMins: 10_080, + resetsAt: automaticResetWindowResetsAtSeconds, + }, + secondary: null, + }, + byLimitId: null, + resetCreditsAvailable: 1, + }, + }, + }; + + const callsBefore = codex.calls.length; + await expect(service.execute({ + kind: "account.usage", + account: added.account.id, + refresh: true, + }, { signal })).resolves.toMatchObject({ + usage: [{ + automaticReset: { + refresh: { state: "not_eligible", reason: "below_threshold" }, + }, + }], + }); + expect(codex.calls.slice(callsBefore)).toEqual([ + "readAccount", + "usage", + "readAccount", + ]); + expect(codex.resetIdempotencyKeys).toEqual([]); + }); + test("automatically consumes one reset at one percent remaining and rereads limits", async () => { const { service, codex, store } = await fixture(); const added = await service.execute({ kind: "account.add", label: "Auto reset" }, { signal }) as { account: { id: `acct_${string}` } }; @@ -2318,6 +2370,481 @@ describe("HraService", () => { expect(codex.resetIdempotencyKeys).toHaveLength(1); }); + test("rechecks the provider identity immediately before automatic reset dispatch", async () => { + const { service, codex, daemonAuthority, store } = await fixture(); + const added = await service.execute({ + kind: "account.add", + label: "Reset identity fence", + }, { signal }) as { account: { id: `acct_${string}` } }; + await service.execute({ + kind: "account.login", + account: added.account.id, + deviceCode: false, + }, { signal }); + codex.usageResult = { + revision: 1, + observedAt: 2_000, + payload: { + rateLimits: { + primary: { + limitId: "codex", + primary: { + usedPercent: 99, + windowDurationMins: 10_080, + resetsAt: automaticResetWindowResetsAtSeconds, + }, + secondary: null, + }, + byLimitId: null, + resetCreditsAvailable: 1, + }, + }, + }; + let identityChanged = false; + daemonAuthority.beforeAssert = async () => { + if ( + !identityChanged + && codex.calls.slice(-3).join(",") === "readAccount,usage,readAccount" + ) { + identityChanged = true; + codex.accountProjection = { + signedIn: true, + email: "replacement@example.com", + plan: "Plus", + }; + } + }; + + const callsBefore = codex.calls.length; + await expect(service.execute({ + kind: "account.usage", + account: added.account.id, + refresh: true, + }, { signal })).rejects.toMatchObject({ code: "CONFLICT" }); + expect(identityChanged).toBe(true); + expect(codex.calls.slice(callsBefore)).toEqual([ + "readAccount", + "usage", + "readAccount", + "readAccount", + ]); + expect(codex.resetIdempotencyKeys).toEqual([]); + expect(store.requireProfileById(added.account.id)).toMatchObject({ + providerEmail: "replacement@example.com", + }); + expect(store.requireAccountRateLimitResetPolicy(added.account.id)).toMatchObject({ + state: "reconciliation_required", + accountFingerprint: null, + weeklyWindowResetsAt: null, + }); + await expect(service.execute({ + kind: "account.usage", + account: added.account.id, + refresh: false, + }, { signal })).resolves.toMatchObject({ + usage: [{ + automaticReset: { policy: { state: "reconciliation_required" } }, + }], + }); + + await expect(service.execute({ + kind: "account.usage", + account: added.account.id, + refresh: true, + }, { signal })).resolves.toMatchObject({ + usage: [{ automaticReset: { + policy: { + state: "window_suppressed", + weeklyWindowResetsAt: automaticResetWindowResetsAt, + }, + refresh: { state: "suppressed", reason: "reconciliation_window" }, + } }], + }); + expect(codex.resetIdempotencyKeys).toEqual([]); + expect(store.requireAccountRateLimitResetPolicy(added.account.id)).toMatchObject({ + state: "window_suppressed", + accountFingerprint: createHash("sha256") + .update("replacement@example.com").digest("hex"), + weeklyWindowResetsAt: automaticResetWindowResetsAt, + }); + }); + + test("suppresses the first reconciled window through its boundary before activating a later window", async () => { + let now = 1_000_000_000; + const suppressedWindow = now + 3 * 24 * 60 * 60 * 1_000; + const laterWindow = suppressedWindow + 3 * 24 * 60 * 60 * 1_000; + const { service, codex, store } = await fixture( + undefined, + new FakeCloud(), + () => undefined, + () => now, + ); + const added = await service.execute({ + kind: "account.add", + label: "Legacy reset reconciliation", + }, { signal }) as { account: { id: `acct_${string}` } }; + await service.execute({ + kind: "account.login", + account: added.account.id, + deviceCode: false, + }, { signal }); + const original = store.requireProfileById(added.account.id); + const originalFingerprint = createHash("sha256") + .update("person@example.com").digest("hex"); + expect(store.authorizeAccountRateLimitResetPolicy({ + profileId: original.id, + processGeneration: original.processGeneration, + accountFingerprint: originalFingerprint, + weeklyWindowDurationMinutes: 10_080, + weeklyWindowResetsAt: suppressedWindow, + }).decision).toBe("allow"); + const legacyEmail = "legacy@example.com"; + expect(store.setProfileState( + original.id, + original.processGeneration, + "signed_in", + { email: legacyEmail, plan: "Plus" }, + )).toBe(true); + const legacy = store.requireProfileById(original.id); + const legacyFingerprint = createHash("sha256").update(legacyEmail).digest("hex"); + expect(store.requireAccountRateLimitResetPolicy(legacy.id)).toMatchObject({ + state: "reconciliation_required", + accountFingerprint: null, + weeklyWindowResetsAt: null, + }); + codex.accountProjection = { signedIn: true, email: legacyEmail, plan: "Plus" }; + + const payload = (usedPercent: number, credits: number, resetsAt: number) => ({ + rateLimits: { + primary: { + limitId: "codex", + primary: { + usedPercent, + windowDurationMins: 10_080, + resetsAt, + }, + secondary: null, + }, + byLimitId: null, + resetCreditsAvailable: credits, + }, + }); + codex.usageResult = { + revision: 1, + observedAt: 2_000, + payload: payload(0, 0, suppressedWindow / 1_000), + }; + await expect(service.execute({ + kind: "account.usage", + account: legacy.id, + refresh: true, + }, { signal })).resolves.toMatchObject({ + usage: [{ automaticReset: { + policy: { + state: "window_suppressed", + weeklyWindowResetsAt: suppressedWindow, + }, + refresh: { state: "suppressed", reason: "reconciliation_window" }, + } }], + }); + expect(codex.resetIdempotencyKeys).toEqual([]); + expect(store.latestAccountRateLimitResetAttempt(legacy.id, legacyFingerprint)) + .toBeNull(); + + codex.usageResult = { + revision: 2, + observedAt: 3_000, + payload: payload(99, 1, suppressedWindow / 1_000), + }; + await service.execute({ + kind: "account.usage", + account: legacy.id, + refresh: true, + }, { signal }); + expect(codex.resetIdempotencyKeys).toEqual([]); + + now = suppressedWindow - 1_000; + codex.usageResult = { + revision: 3, + observedAt: 4_000, + payload: payload(99, 1, laterWindow / 1_000), + }; + await expect(service.execute({ + kind: "account.usage", + account: legacy.id, + refresh: true, + }, { signal })).resolves.toMatchObject({ + usage: [{ automaticReset: { + policy: { + state: "window_suppressed", + weeklyWindowResetsAt: suppressedWindow, + }, + refresh: { state: "suppressed", reason: "weekly_window_nonmonotonic" }, + } }], + }); + expect(codex.resetIdempotencyKeys).toEqual([]); + + now = suppressedWindow; + codex.usageResult = { + revision: 4, + observedAt: 5_000, + payload: payload(0, 1, laterWindow / 1_000), + }; + await expect(service.execute({ + kind: "account.usage", + account: legacy.id, + refresh: true, + }, { signal })).resolves.toMatchObject({ + usage: [{ automaticReset: { + policy: { state: "active" }, + refresh: { state: "not_eligible", reason: "below_threshold" }, + } }], + }); + expect(codex.resetIdempotencyKeys).toEqual([]); + expect(store.requireAccountRateLimitResetPolicy(legacy.id)).toMatchObject({ + state: "active_bound", + weeklyWindowResetsAt: laterWindow, + }); + + codex.usageResult = { + revision: 5, + observedAt: 6_000, + payload: payload(99, 1, laterWindow / 1_000), + }; + await service.execute({ + kind: "account.usage", + account: legacy.id, + refresh: true, + }, { signal }); + expect(codex.resetIdempotencyKeys).toHaveLength(1); + expect(JSON.stringify(store.requireAccountRateLimitResetPolicy(legacy.id))) + .not.toContain(legacyEmail); + }); + + test("migrates a signed-in v24 profile and suppresses its first valid window across restart and notification", async () => { + const now = 3_000_000_000; + const firstWindow = now + 3 * 24 * 60 * 60 * 1_000; + const value = await fixture( + undefined, + new FakeCloud(), + () => undefined, + () => now, + ); + const added = await value.service.execute({ + kind: "account.add", + label: "V24 reset reconciliation", + }, { signal }) as { account: { id: `acct_${string}` } }; + await value.service.execute({ + kind: "account.login", + account: added.account.id, + deviceCode: false, + }, { signal }); + + await value.service.close(); + value.store.close(); + stores.splice(stores.indexOf(value.store), 1); + const legacy = new Database(value.paths.database, { create: false, strict: true }); + try { + legacy.exec("PRAGMA foreign_keys=OFF"); + const resetTriggers = legacy.query( + `SELECT name FROM sqlite_master + WHERE type='trigger' AND name GLOB 'account_rate_limit_reset_*' + ORDER BY name`, + ).all() as Array<{ name: string }>; + for (const { name } of resetTriggers) { + if (!/^account_rate_limit_reset_[a-z_]+$/u.test(name)) { + throw new Error("Unexpected reset trigger name."); + } + legacy.exec(`DROP TRIGGER "${name}"`); + } + legacy.exec(` + DROP TABLE account_rate_limit_reset_rebinds; + DROP TABLE account_rate_limit_reset_attempts; + DROP TABLE account_rate_limit_reset_policies; + DROP INDEX IF EXISTS usage_poll_failures_identity_recent; + ALTER TABLE session_events DROP COLUMN projection_version; + ALTER TABLE usage_poll_failures DROP COLUMN account_fingerprint; + `); + const workTriggers = legacy.query( + `SELECT name FROM sqlite_master + WHERE type='trigger' + AND (name GLOB 'work_*' OR name GLOB 'works_*') + ORDER BY name`, + ).all() as Array<{ name: string }>; + for (const { name } of workTriggers) { + if (!/^(?:work|works)_[a-z_]+$/u.test(name)) { + throw new Error("Unexpected v26 work trigger name."); + } + legacy.exec(`DROP TRIGGER "${name}"`); + } + const workTables = legacy.query( + `SELECT name FROM sqlite_master + WHERE type='table' AND (name='works' OR name GLOB 'work_*') + ORDER BY name`, + ).all() as Array<{ name: string }>; + for (const { name } of workTables) { + if (!/^(?:works|work_[a-z_]+)$/u.test(name)) { + throw new Error("Unexpected v26 work table name."); + } + legacy.exec(`DROP TABLE "${name}"`); + } + legacy.exec(` + DELETE FROM migrations WHERE version>=25; + PRAGMA user_version=24; + PRAGMA foreign_keys=ON; + `); + } finally { + legacy.close(false); + } + + const store = new StateStore(value.paths, { now: () => now }); + stores.push(store); + expect(store.requireAccountRateLimitResetPolicy(added.account.id)).toMatchObject({ + state: "reconciliation_required", + accountFingerprint: null, + weeklyWindowResetsAt: null, + }); + const inspector = new Database(value.paths.database, { readonly: true, strict: true }); + try { + expect(inspector.query("PRAGMA user_version").get()).toEqual({ user_version: 28 }); + expect(inspector.query( + "SELECT version FROM migrations WHERE version>=25 ORDER BY version", + ).all()).toEqual([{ version: 25 }, { version: 26 }, { version: 27 }, { version: 28 }]); + } finally { + inspector.close(false); + } + + const codex = new FakeCodex(); + const migrated = new HraService({ + store, + paths: value.paths, + codex, + cloud: new FakeCloud(), + daemonAuthority: new FakeDaemonAuthority(), + now: () => now, + requestStop: () => undefined, + }); + codex.usageResult = { + revision: 1, + observedAt: 2_000, + payload: { rateLimits: { temporarilyUnavailable: true } }, + }; + await expect(migrated.execute({ + kind: "account.usage", + account: added.account.id, + refresh: true, + }, { signal })).resolves.toMatchObject({ + usage: [{ automaticReset: { + policy: { state: "reconciliation_required" }, + refresh: { state: "suppressed", reason: "reconciliation_required" }, + } }], + }); + + const eligiblePayload = { + rateLimits: { + primary: { + limitId: "codex", + primary: { + usedPercent: 99, + windowDurationMins: 10_080, + resetsAt: firstWindow / 1_000, + }, + secondary: null, + }, + byLimitId: null, + resetCreditsAvailable: 1, + }, + }; + codex.usageResult = { revision: 2, observedAt: 3_000, payload: eligiblePayload }; + await expect(migrated.execute({ + kind: "account.usage", + account: added.account.id, + refresh: true, + }, { signal })).resolves.toMatchObject({ + usage: [{ automaticReset: { + policy: { state: "window_suppressed" }, + refresh: { state: "suppressed", reason: "reconciliation_window" }, + } }], + }); + expect(codex.resetIdempotencyKeys).toEqual([]); + + await migrated.close(); + const restartedCodex = new FakeCodex(); + restartedCodex.usageResult = { + revision: 3, + observedAt: 4_000, + payload: eligiblePayload, + }; + const restarted = new HraService({ + store, + paths: value.paths, + codex: restartedCodex, + cloud: new FakeCloud(), + daemonAuthority: new FakeDaemonAuthority(), + now: () => now, + requestStop: () => undefined, + }); + await restarted.recover(); + const profile = store.requireProfileById(added.account.id); + const owned = profilePaths(value.paths, profile.id); + await restarted.observeCodexFact({ + id: profile.id, + generation: profile.processGeneration, + codexHome: owned.codexHome, + desktopUserData: owned.desktopUserData, + }, { type: "rateLimitsUpdated" }); + await restarted.settled(); + expect(restartedCodex.calls).toEqual(["readAccount", "usage", "readAccount"]); + expect(restartedCodex.resetIdempotencyKeys).toEqual([]); + expect(store.requireAccountRateLimitResetPolicy(profile.id)).toMatchObject({ + state: "window_suppressed", + weeklyWindowResetsAt: firstWindow, + }); + await restarted.close(); + }); + + test("fails before reset-attempt inspection when policy storage is unavailable", async () => { + const { service, codex, store } = await fixture(); + const added = await service.execute({ + kind: "account.add", + label: "Reset policy failure", + }, { signal }) as { account: { id: `acct_${string}` } }; + await service.execute({ + kind: "account.login", + account: added.account.id, + deviceCode: false, + }, { signal }); + codex.usageResult = { + revision: 1, + observedAt: 2_000, + payload: { + rateLimits: { + primary: { + limitId: "codex", + primary: { + usedPercent: 99, + windowDurationMins: 10_080, + resetsAt: automaticResetWindowResetsAtSeconds, + }, + secondary: null, + }, + byLimitId: null, + resetCreditsAvailable: 1, + }, + }, + }; + Object.defineProperty(store, "authorizeAccountRateLimitResetPolicy", { + configurable: true, + value: () => { throw new Error("injected reset policy failure"); }, + }); + await expect(service.execute({ + kind: "account.usage", + account: added.account.id, + refresh: true, + }, { signal })).rejects.toThrow("injected reset policy failure"); + expect(codex.resetIdempotencyKeys).toEqual([]); + }); + test("persists a background reset result for later passive usage status", async () => { const { service, codex, store, paths } = await fixture(); const added = await service.execute({ @@ -2451,7 +2978,7 @@ describe("HraService", () => { } }); - test("reconciles an indeterminate reset with the exact persisted key", async () => { + test("reconciles an indeterminate reset with its exact persisted key", async () => { const { service, codex, store } = await fixture(); const added = await service.execute({ kind: "account.add", label: "Reset retry" }, { signal }) as { account: { id: `acct_${string}` } }; await service.execute({ kind: "account.login", account: added.account.id, deviceCode: false }, { signal }); @@ -2516,10 +3043,10 @@ describe("HraService", () => { refresh: true, }, { signal }); expect(codex.resetIdempotencyKeys).toEqual([key, key]); - expect(store.readRecoverableAccountRateLimitReset( + expect(store.latestAccountRateLimitResetAttempt( added.account.id, createHash("sha256").update("person@example.com").digest("hex"), - )).toBeNull(); + )).toMatchObject({ idempotencyKey: key, state: "settled", outcome: "alreadyRedeemed" }); }); test("preserves an ambiguous reset key while its live weekly bucket is unavailable", async () => { @@ -2572,7 +3099,10 @@ describe("HraService", () => { account: added.account.id, refresh: true, }, { signal })).resolves.toMatchObject({ - usage: [{ automaticReset: { refresh: { state: "recovery_pending" } } }], + usage: [{ automaticReset: { refresh: { + state: "suppressed", + reason: "weekly_window_unavailable", + } } }], }); const fingerprint = createHash("sha256").update("person@example.com").digest("hex"); expect(store.readRecoverableAccountRateLimitReset(added.account.id, fingerprint)) @@ -2587,6 +3117,8 @@ describe("HraService", () => { refresh: true, }, { signal }); expect(codex.resetIdempotencyKeys).toEqual([key, key]); + expect(store.latestAccountRateLimitResetAttempt(added.account.id, fingerprint)) + .toMatchObject({ idempotencyKey: key, state: "settled", outcome: "alreadyRedeemed" }); }); test("never redispatches a terminal reset latch returned by preparation", async () => { @@ -2648,6 +3180,13 @@ describe("HraService", () => { }, { signal }); const profile = closed.store.requireProfileById(closedAccount.account.id); const fingerprint = createHash("sha256").update("person@example.com").digest("hex"); + expect(closed.store.authorizeAccountRateLimitResetPolicy({ + profileId: profile.id, + processGeneration: profile.processGeneration, + accountFingerprint: fingerprint, + weeklyWindowDurationMinutes: 10_080, + weeklyWindowResetsAt: automaticResetWindowResetsAt, + }).decision).toBe("allow"); const prepared = closed.store.prepareAccountRateLimitReset({ profileId: profile.id, processGeneration: profile.processGeneration, @@ -2751,8 +3290,16 @@ describe("HraService", () => { expect(codex.resetIdempotencyKeys).toEqual([key, key]); }); - test("closes an ambiguous reset when its weekly window advances without quarantining", async () => { - const { service, codex, store } = await fixture(); + test("reconciles an ambiguous reset with its original key in a later active window", async () => { + let now = 2_000_000_000; + const originalWindow = now + 3 * 24 * 60 * 60 * 1_000; + const laterWindow = originalWindow + 3 * 24 * 60 * 60 * 1_000; + const { service, codex, store } = await fixture( + undefined, + new FakeCloud(), + () => undefined, + () => now, + ); const added = await service.execute({ kind: "account.add", label: "Reset window", @@ -2762,25 +3309,25 @@ describe("HraService", () => { account: added.account.id, deviceCode: false, }, { signal }); - const payload = (resetsAt: number) => ({ + const payload = (resetsAt: number, usedPercent: number, credits: number) => ({ rateLimits: { primary: { limitId: "codex", primary: { - usedPercent: 99, + usedPercent, windowDurationMins: 10_080, resetsAt, }, secondary: null, }, byLimitId: null, - resetCreditsAvailable: 1, + resetCreditsAvailable: credits, }, }); codex.usageResult = { revision: 1, observedAt: 2_000, - payload: payload(automaticResetWindowResetsAtSeconds), + payload: payload(originalWindow / 1_000, 99, 1), }; codex.resetError = new IndeterminateCodexEffectError( "account/rateLimitResetCredit/consume", @@ -2791,30 +3338,39 @@ describe("HraService", () => { account: added.account.id, refresh: true, }, { signal }); + const key = codex.resetIdempotencyKeys[0]; + if (key === undefined) throw new Error("Expected an ambiguous reset key."); codex.resetError = undefined; + codex.resetOutcome = "alreadyRedeemed"; + now = originalWindow; codex.usageResult = { revision: 2, observedAt: 3_000, - payload: payload(automaticResetWindowResetsAtSeconds + 100_000), + payload: payload(laterWindow / 1_000, 0, 0), }; await expect(service.execute({ kind: "account.usage", account: added.account.id, refresh: true, }, { signal })).resolves.toMatchObject({ - usage: [{ automaticReset: { refresh: { state: "window_changed" } } }], + usage: [{ automaticReset: { + policy: { state: "active" }, + refresh: { state: "settled", outcome: "alreadyRedeemed" }, + } }], + }); + expect(codex.resetIdempotencyKeys).toEqual([key, key]); + expect(store.requireAccountRateLimitResetPolicy(added.account.id)).toMatchObject({ + state: "active_bound", + weeklyWindowResetsAt: laterWindow, }); - expect(codex.resetIdempotencyKeys).toHaveLength(1); expect(store.requireProfileById(added.account.id).state).toBe("signed_in"); - await service.execute({ - kind: "account.usage", - account: added.account.id, - refresh: true, - }, { signal }); - expect(codex.resetIdempotencyKeys).toHaveLength(2); + expect(store.latestAccountRateLimitResetAttempt( + added.account.id, + createHash("sha256").update("person@example.com").digest("hex"), + )).toMatchObject({ idempotencyKey: key, state: "settled", outcome: "alreadyRedeemed" }); }); - test("rebinds an ambiguous reset across a replacement app-server generation", async () => { + test("rebinds and reconciles an ambiguous reset across an app-server generation", async () => { const value = await fixture(); const added = await value.service.execute({ kind: "account.add", @@ -2881,10 +3437,15 @@ describe("HraService", () => { expect(replacementCodex.resetIdempotencyKeys).toEqual([key]); expect(value.store.listAccountRateLimitResetRebinds(key)).toEqual([ expect.objectContaining({ + idempotencyKey: key, fromProcessGeneration: originalGeneration, toProcessGeneration: replacementGeneration, }), ]); + expect(value.store.latestAccountRateLimitResetAttempt( + added.account.id, + createHash("sha256").update("person@example.com").digest("hex"), + )).toMatchObject({ idempotencyKey: key, state: "settled", outcome: "alreadyRedeemed" }); await replacement.close(); }); @@ -2902,6 +3463,13 @@ describe("HraService", () => { const profile = store.requireProfileById(added.account.id); const fingerprint = createHash("sha256").update("person@example.com").digest("hex"); const resetAt = automaticResetWindowResetsAt; + expect(store.authorizeAccountRateLimitResetPolicy({ + profileId: profile.id, + processGeneration: profile.processGeneration, + accountFingerprint: fingerprint, + weeklyWindowDurationMinutes: 10_080, + weeklyWindowResetsAt: resetAt, + }).decision).toBe("allow"); const prepared = store.prepareAccountRateLimitReset({ profileId: profile.id, processGeneration: profile.processGeneration, @@ -2938,7 +3506,7 @@ describe("HraService", () => { .toMatchObject({ idempotencyKey: prepared.idempotencyKey, state: "prepared" }); }); - test("closes an ambiguous reset and reconciles when fresh account identity changes", async () => { + test("keeps ambiguous recovery inert while a replacement identity reconciles", async () => { const { service, codex, store } = await fixture(); const added = await service.execute({ kind: "account.add", @@ -2991,6 +3559,15 @@ describe("HraService", () => { providerEmail: "someone-else@example.com", providerPlan: "Plus", }); + expect(store.requireAccountRateLimitResetPolicy(added.account.id)).toMatchObject({ + state: "reconciliation_required", + accountFingerprint: null, + weeklyWindowResetsAt: null, + }); + expect(store.latestAccountRateLimitResetAttempt( + added.account.id, + createHash("sha256").update("person@example.com").digest("hex"), + )).toMatchObject({ state: "closed", localResolution: "account_identity_changed" }); await expect(service.execute({ kind: "account.usage", account: added.account.id, @@ -3008,6 +3585,24 @@ describe("HraService", () => { snapshot: null, }], }); + + await expect(service.execute({ + kind: "account.usage", + account: added.account.id, + refresh: true, + }, { signal })).resolves.toMatchObject({ + usage: [{ automaticReset: { + policy: { state: "window_suppressed" }, + refresh: { state: "suppressed", reason: "reconciliation_window" }, + } }], + }); + expect(codex.resetIdempotencyKeys).toHaveLength(1); + expect(store.requireAccountRateLimitResetPolicy(added.account.id)).toMatchObject({ + state: "window_suppressed", + accountFingerprint: createHash("sha256") + .update("someone-else@example.com").digest("hex"), + weeklyWindowResetsAt: automaticResetWindowResetsAt, + }); }); test("account show clears a reset-era recovery quarantine without a generic mutation", async () => { diff --git a/src/daemon/service.ts b/src/daemon/service.ts index e925299..e1d4323 100644 --- a/src/daemon/service.ts +++ b/src/daemon/service.ts @@ -40,6 +40,7 @@ import { sessionEventPageSchema, type SessionEventBody, type SessionEventPage } import { AUTO_RATE_LIMIT_RESET_REMAINING_PERCENT, AUTO_RATE_LIMIT_RESET_USED_PERCENT, + CODEX_WEEKLY_RATE_LIMIT_WINDOW_MINUTES, accountUsageHistoryEntrySchema, accountUsageHistoryPageSchema, accountUsageCounterSamples, @@ -51,6 +52,7 @@ import { providerUsagePayload, storedAccountUsageSnapshotSchema, type AutomaticRateLimitResetLastAttempt, + type AutomaticRateLimitResetPolicyStatus, type AutomaticRateLimitResetRefreshStatus, type UsageVelocityWindow, } from "../domain/usage-metrics"; @@ -84,6 +86,7 @@ import { type MutationAttemptRecord, type MutationEffectEvidence, type AccountRateLimitResetAttemptRecord, + type AccountRateLimitResetPolicyRecord, type ProfileRecord, type SessionRecord, type StateStore, @@ -463,6 +466,29 @@ type AutomaticRateLimitResetAttemptResult = Readonly<{ authoritativeReread: boolean; refresh: AutomaticRateLimitResetRefreshStatus; }>; +const publicAutomaticRateLimitResetPolicy = ( + policy: AccountRateLimitResetPolicyRecord, + currentAccountFingerprint: string | null, +): AutomaticRateLimitResetPolicyStatus => { + if ( + policy.accountFingerprint !== null + && policy.accountFingerprint !== currentAccountFingerprint + ) return { state: "reconciliation_required" }; + switch (policy.state) { + case "active_unbound": + case "active_bound": return { state: "active" }; + case "reconciliation_required": return { state: "reconciliation_required" }; + case "window_suppressed": { + if (policy.weeklyWindowResetsAt === null) { + throw new Error("ACCOUNT_RATE_LIMIT_RESET_POLICY_WINDOW_MISSING"); + } + return { + state: "window_suppressed", + weeklyWindowResetsAt: policy.weeklyWindowResetsAt, + }; + } + } +}; const publicAutomaticRateLimitResetLastAttempt = ( attempt: AccountRateLimitResetAttemptRecord | null, ): AutomaticRateLimitResetLastAttempt | null => { @@ -1152,7 +1178,6 @@ export class HraService { async #recoverAdmitted(): Promise { await this.#cloud.supersedeTerminalCompactProjectionRecoveries(); await this.#daemonAuthority.assertCurrent(); - this.#store.recoverAccountRateLimitResetAttempts(); const recoveredMutations = this.#store.recoverEffectStartedMutations(); if (recoveredMutations.unresolved.length > 0) { throw new Error(`Daemon recovery cannot resolve ${String(recoveredMutations.unresolved.length)} effect-started mutation authorities.`); @@ -2812,11 +2837,12 @@ export class HraService { let automaticResetRefresh: AutomaticRateLimitResetRefreshStatus | undefined; if (refresh) { this.#assertSignedIn(profile); - const snapshot = await this.#readAndRecordUsage(profile, signal); + const observed = await this.#readAndRecordUsage(profile, signal); const refreshedProfile = this.#store.requireProfileById(profile.id); const reset = await this.#attemptAutomaticRateLimitReset( refreshedProfile, - snapshot.payload, + observed.accountFingerprint, + observed.snapshot.payload, signal, ); automaticResetRefresh = reset.refresh; @@ -2831,6 +2857,8 @@ export class HraService { } const now = this.#now(); const currentProfile = this.#store.requireProfileById(profile.id); + const automaticResetPolicy = this.#store + .requireAccountRateLimitResetPolicy(profile.id); const currentFingerprint = accountFingerprintForProfile(currentProfile); const latestRecorded = currentFingerprint === null ? null @@ -2875,6 +2903,10 @@ export class HraService { usage.push({ account: this.#publicProfile(currentProfile), automaticReset: automaticRateLimitResetStatusSchema.parse({ + policy: publicAutomaticRateLimitResetPolicy( + automaticResetPolicy, + currentFingerprint, + ), threshold: { remainingPercent: AUTO_RATE_LIMIT_RESET_REMAINING_PERCENT, usedPercent: AUTO_RATE_LIMIT_RESET_USED_PERCENT, @@ -2919,19 +2951,15 @@ export class HraService { async #readAndRecordUsage( profile: ProfileRecord, signal: AbortSignal, - ): Promise>> { + ): Promise>; + }>> { let verifiedProfile = this.#store.requireProfileById(profile.id); const expectedFingerprint = accountFingerprintForProfile(verifiedProfile); - const priorAttempt = expectedFingerprint === null - ? null - : this.#store.readRecoverableAccountRateLimitReset( - profile.id, - expectedFingerprint, - ); const accountFingerprint = await this.#proveUsageAccountIdentity({ profile: verifiedProfile, expectedFingerprint, - attempt: priorAttempt, signal, }); verifiedProfile = this.#store.requireProfileById(profile.id); @@ -2956,14 +2984,9 @@ export class HraService { throw error; } const receivedAt = this.#now(); - const attemptAfterRead = this.#store.readRecoverableAccountRateLimitReset( - profile.id, - accountFingerprint, - ); const confirmedFingerprint = await this.#proveUsageAccountIdentity({ profile: verifiedProfile, expectedFingerprint: accountFingerprint, - attempt: attemptAfterRead, signal, }); if (confirmedFingerprint !== accountFingerprint) { @@ -2985,118 +3008,106 @@ export class HraService { previousPayload: previous?.payload ?? null, }); this.#store.recordUsage(profile.id, sourceSequence, snapshot.observedAt, stored); - return snapshot; + return { accountFingerprint, snapshot }; } async #attemptAutomaticRateLimitReset( profile: ProfileRecord, + accountFingerprint: string, providerPayload: unknown, signal: AbortSignal, ): Promise { const now = this.#now(); - const storedFingerprint = accountFingerprintForProfile(profile); - let attempt = storedFingerprint === null - ? null - : this.#store.readRecoverableAccountRateLimitReset(profile.id, storedFingerprint); - const decision = automaticRateLimitResetDecision({ + const observation = automaticRateLimitResetObservation({ providerPayload, now, }); - if (attempt === null && !decision.eligible) { + const policyDecision = this.#store.authorizeAccountRateLimitResetPolicy({ + profileId: profile.id, + processGeneration: profile.processGeneration, + accountFingerprint, + weeklyWindowDurationMinutes: observation.available + ? CODEX_WEEKLY_RATE_LIMIT_WINDOW_MINUTES + : null, + weeklyWindowResetsAt: observation.available + ? observation.weeklyWindowResetsAt + : null, + }); + if (policyDecision.decision !== "allow") { + const reason = policyDecision.reason === "weekly_window_unavailable" + && policyDecision.policy.state === "reconciliation_required" + ? "reconciliation_required" as const + : policyDecision.reason; return { authoritativeReread: false, - refresh: { state: "not_eligible", reason: decision.reason }, + refresh: { state: "suppressed", reason }, }; } + if (!observation.available) { + throw new Error("ACCOUNT_RATE_LIMIT_RESET_POLICY_OBSERVATION_MISMATCH"); + } - const accountFingerprint = await this.#proveUsageAccountIdentity({ - profile, - expectedFingerprint: attempt?.accountFingerprint ?? storedFingerprint, - attempt, - signal, + this.#store.recoverAccountRateLimitResetAttempts({ + profileId: profile.id, + processGeneration: profile.processGeneration, + accountFingerprint, + weeklyWindowResetsAt: observation.weeklyWindowResetsAt, }); - attempt ??= this.#store.readRecoverableAccountRateLimitReset( + let attempt = this.#store.readRecoverableAccountRateLimitReset( profile.id, accountFingerprint, ); - if ( - attempt !== null - && attempt.currentProcessGeneration !== profile.processGeneration - ) { - attempt = this.#store.rebindAccountRateLimitReset({ - idempotencyKey: attempt.idempotencyKey, - expectedCurrentProcessGeneration: attempt.currentProcessGeneration, - nextProcessGeneration: profile.processGeneration, - accountFingerprint, - }); + if (attempt?.state === "effect_started") { + return { + authoritativeReread: false, + refresh: { state: "recovery_pending" }, + }; } + const decisionNow = this.#now(); + const decision = automaticRateLimitResetDecision({ + providerPayload, + now: decisionNow, + }); + if (attempt === null && !decision.eligible) { + return { + authoritativeReread: false, + refresh: { state: "not_eligible", reason: decision.reason }, + }; + } if (attempt !== null) { - if (attempt.state === "settled") { - if (attempt.outcome === null) { - throw new Error("ACCOUNT_RATE_LIMIT_RESET_SETTLED_OUTCOME_MISSING"); - } - return { - authoritativeReread: false, - refresh: { state: "latched", outcome: attempt.outcome }, - }; - } - if (attempt.state === "closed") { - if (attempt.localResolution === null) { - throw new Error("ACCOUNT_RATE_LIMIT_RESET_CLOSED_RESOLUTION_MISSING"); + // An ambiguous attempt represents an upstream effect that may already + // have succeeded. Reconcile only that durable idempotency key after the + // policy admits a fresh observation; current credits, usage, and window + // cannot prove whether the earlier dispatch committed. + if (attempt.state !== "ambiguous") { + if ( + decisionNow >= attempt.weeklyWindowResetsAt + || observation.weeklyWindowResetsAt !== attempt.weeklyWindowResetsAt + ) { + this.#store.closeAccountRateLimitReset( + attempt.idempotencyKey, + "weekly_window_changed", + ); + return { + authoritativeReread: false, + refresh: { state: "window_changed" }, + }; } - return { - authoritativeReread: false, - refresh: { state: "latched", reason: attempt.localResolution }, - }; - } - const observation = automaticRateLimitResetObservation({ - providerPayload, - now, - }); - if ( - now >= attempt.weeklyWindowResetsAt - || ( - observation.available - && observation.weeklyWindowResetsAt !== attempt.weeklyWindowResetsAt - ) - ) { - this.#store.closeAccountRateLimitReset( - attempt.idempotencyKey, - "weekly_window_changed", - ); - return { - authoritativeReread: false, - refresh: { state: "window_changed" }, - }; - } - if (!observation.available) { - return { - authoritativeReread: false, - refresh: attempt.state === "ambiguous" - ? { state: "recovery_pending" } - : { - state: "not_eligible", - reason: "weekly_window_unavailable", - }, - }; - } - if ( - attempt.state !== "ambiguous" - && ( + if ( observation.creditsAvailable < 1 || observation.usedPercent < AUTO_RATE_LIMIT_RESET_USED_PERCENT - ) - ) { - return { - authoritativeReread: false, - refresh: { - state: "waiting", - reason: observation.creditsAvailable < 1 - ? "credits_unavailable" - : "below_threshold", - }, - }; + ) { + return { + authoritativeReread: false, + refresh: { + state: "waiting", + reason: observation.creditsAvailable < 1 + ? "credits_unavailable" + : "below_threshold", + }, + }; + } } } else { if (!decision.eligible) { @@ -3105,9 +3116,58 @@ export class HraService { refresh: { state: "not_eligible", reason: decision.reason }, }; } + } + + await this.#daemonAuthority.assertCurrent(); + if (signal.aborted) throw signal.reason; + const confirmedFingerprint = await this.#proveUsageAccountIdentity({ + profile, + expectedFingerprint: accountFingerprint, + signal, + }); + if (confirmedFingerprint !== accountFingerprint) { + throw new Error("ACCOUNT_RATE_LIMIT_RESET_IDENTITY_PROOF_CHANGED_WITHOUT_CONFLICT"); + } + const dispatchProfile = this.#store.requireProfileById(profile.id); + if ( + dispatchProfile.processGeneration !== profile.processGeneration + || accountFingerprintForProfile(dispatchProfile) !== accountFingerprint + ) throw new Error("ACCOUNT_RATE_LIMIT_RESET_AUTHORITY_CHANGED"); + const dispatchPolicyDecision = this.#store.authorizeAccountRateLimitResetPolicy({ + profileId: dispatchProfile.id, + processGeneration: dispatchProfile.processGeneration, + accountFingerprint, + weeklyWindowDurationMinutes: CODEX_WEEKLY_RATE_LIMIT_WINDOW_MINUTES, + weeklyWindowResetsAt: observation.weeklyWindowResetsAt, + }); + if (dispatchPolicyDecision.decision !== "allow") { + const reason = dispatchPolicyDecision.reason === "weekly_window_unavailable" + && dispatchPolicyDecision.policy.state === "reconciliation_required" + ? "reconciliation_required" as const + : dispatchPolicyDecision.reason; + return { + authoritativeReread: false, + refresh: { state: "suppressed", reason }, + }; + } + if ( + attempt !== null + && attempt.currentProcessGeneration !== dispatchProfile.processGeneration + ) { + attempt = this.#store.rebindAccountRateLimitReset({ + idempotencyKey: attempt.idempotencyKey, + expectedCurrentProcessGeneration: attempt.currentProcessGeneration, + nextProcessGeneration: dispatchProfile.processGeneration, + accountFingerprint, + }); + } + if (attempt === null) { + if (!decision.eligible) { + throw new Error("ACCOUNT_RATE_LIMIT_RESET_DECISION_CHANGED_WITHOUT_ASYNC_GAP"); + } attempt = this.#store.prepareAccountRateLimitReset({ - profileId: profile.id, - processGeneration: profile.processGeneration, + profileId: dispatchProfile.id, + processGeneration: dispatchProfile.processGeneration, accountFingerprint, weeklyWindowResetsAt: decision.weeklyWindowResetsAt, observedUsedPercent: decision.usedPercent, @@ -3135,16 +3195,21 @@ export class HraService { }; } if (attempt.state === "effect_started") { - throw new Error("ACCOUNT_RATE_LIMIT_RESET_EFFECT_ALREADY_STARTED"); + return { + authoritativeReread: false, + refresh: { state: "recovery_pending" }, + }; } - await this.#daemonAuthority.assertCurrent(); - if (signal.aborted) throw signal.reason; - this.#store.beginAccountRateLimitReset(attempt.idempotencyKey); + signal.throwIfAborted(); + const begun = this.#store.beginAccountRateLimitReset(attempt.idempotencyKey); + if (begun.state !== "effect_started") { + throw new Error("ACCOUNT_RATE_LIMIT_RESET_BEGIN_STATE_INVALID"); + } let outcome: Awaited>; try { outcome = await this.#codex.consumeRateLimitReset({ - authority: authorityFor(this.#paths, profile), + authority: authorityFor(this.#paths, dispatchProfile), idempotencyKey: attempt.idempotencyKey, signal, }); @@ -3153,9 +3218,9 @@ export class HraService { ? "ambiguous" : "retryable"; try { - // Only the client's explicit indeterminate-effect error can bypass a - // later eligibility recheck. Definite rejections and pre-dispatch - // failures retain the same key but return to the retryable gate. + // Every failure retains the original key. An indeterminate effect can + // bypass ordinary eligibility only after durable policy authorization; + // determinate failures return through the ordinary window gates. this.#store.deferAccountRateLimitReset(attempt.idempotencyKey, retryState); } catch (journalError: unknown) { this.#failStopAfterResetJournalFailure( @@ -3166,8 +3231,8 @@ export class HraService { "An automatic reset may have reached Codex and its recovery state could not be committed.", ); } - // A successful usage read remains successful. The next serialized poll - // retries this exact logical redemption with the same upstream key. + // A successful usage read remains successful. A later refresh can retry + // only this exact durable upstream key after policy authorization. return { authoritativeReread: false, refresh: { @@ -3195,7 +3260,6 @@ export class HraService { async #proveUsageAccountIdentity(input: { profile: ProfileRecord; expectedFingerprint: string | null; - attempt: AccountRateLimitResetAttemptRecord | null; signal: AbortSignal; }): Promise { const account = await this.#fencedEffect(async () => @@ -3222,12 +3286,6 @@ export class HraService { || (persistedFingerprint !== null && actualFingerprint !== persistedFingerprint); if (identityChanged) { - if (input.attempt !== null) { - this.#store.closeAccountRateLimitReset( - input.attempt.idempotencyKey, - "account_identity_changed", - ); - } const stateChange = this.#store.setProfileStateWithWorkRetirement( input.profile.id, input.profile.processGeneration, diff --git a/src/domain/usage-metrics.test.ts b/src/domain/usage-metrics.test.ts index 54122cf..55b880b 100644 --- a/src/domain/usage-metrics.test.ts +++ b/src/domain/usage-metrics.test.ts @@ -177,6 +177,7 @@ describe("observedAccountTokenVelocity", () => { describe("automaticRateLimitResetStatusSchema", () => { const base = { threshold: { remainingPercent: 1, usedPercent: 99 }, + policy: { state: "active" as const }, observation: { state: "unavailable" as const, reason: "weekly_window_unavailable" as const, @@ -199,6 +200,12 @@ describe("automaticRateLimitResetStatusSchema", () => { lastAttempt, }).success).toBe(true); } + expect(automaticRateLimitResetStatusSchema.safeParse({ + ...base, + policy: { state: "window_suppressed", weeklyWindowResetsAt }, + lastAttempt: null, + refresh: { state: "suppressed", reason: "reconciliation_window" }, + }).success).toBe(true); }); test("rejects private extras and invalid state-field combinations", () => { @@ -246,6 +253,15 @@ describe("automaticRateLimitResetStatusSchema", () => { lastAttempt: null, refresh: { state: "settled", reason: "weekly_window_changed" }, }, + { + ...base, + policy: { + state: "window_suppressed", + weeklyWindowResetsAt, + accountFingerprint: digest, + }, + lastAttempt: null, + }, ]) { expect(automaticRateLimitResetStatusSchema.safeParse(value).success).toBe(false); } diff --git a/src/domain/usage-metrics.ts b/src/domain/usage-metrics.ts index 1cdf7c8..a31fda2 100644 --- a/src/domain/usage-metrics.ts +++ b/src/domain/usage-metrics.ts @@ -85,6 +85,16 @@ export const automaticRateLimitResetRefreshStatusSchema = z.union([ }).strict(), z.object({ state: z.literal("retry_pending") }).strict(), z.object({ state: z.literal("recovery_pending") }).strict(), + z.object({ + state: z.literal("suppressed"), + reason: z.enum([ + "reconciliation_required", + "reconciliation_window", + "weekly_window_unavailable", + "weekly_window_nonmonotonic", + "account_identity_changed", + ]), + }).strict(), z.object({ state: z.literal("settled"), outcome: accountRateLimitResetOutcomeSchema, @@ -95,11 +105,25 @@ export type AutomaticRateLimitResetRefreshStatus = z.infer< typeof automaticRateLimitResetRefreshStatusSchema >; +export const automaticRateLimitResetPolicyStatusSchema = z.discriminatedUnion("state", [ + z.object({ state: z.literal("active") }).strict(), + z.object({ state: z.literal("reconciliation_required") }).strict(), + z.object({ + state: z.literal("window_suppressed"), + weeklyWindowResetsAt: unixMillisecondsSchema, + }).strict(), +]); + +export type AutomaticRateLimitResetPolicyStatus = z.infer< + typeof automaticRateLimitResetPolicyStatusSchema +>; + export const automaticRateLimitResetStatusSchema = z.object({ threshold: z.object({ remainingPercent: z.literal(AUTO_RATE_LIMIT_RESET_REMAINING_PERCENT), usedPercent: z.literal(AUTO_RATE_LIMIT_RESET_USED_PERCENT), }).strict(), + policy: automaticRateLimitResetPolicyStatusSchema, observation: z.union([ z.object({ state: z.literal("available"), diff --git a/src/storage/state-store.test.ts b/src/storage/state-store.test.ts index 0d86b6c..50ec0ba 100644 --- a/src/storage/state-store.test.ts +++ b/src/storage/state-store.test.ts @@ -76,6 +76,20 @@ const usageFingerprint = "a".repeat(64); const resetAccountFingerprint = (email: string): string => createHash("sha256").update(email.trim().toLowerCase()).digest("hex"); +const prepareAuthorizedReset = ( + store: StateStore, + input: Parameters[0], +) => { + expect(store.authorizeAccountRateLimitResetPolicy({ + profileId: input.profileId, + processGeneration: input.processGeneration, + accountFingerprint: input.accountFingerprint, + weeklyWindowDurationMinutes: 10_080, + weeklyWindowResetsAt: input.weeklyWindowResetsAt, + }).decision).toBe("allow"); + return store.prepareAccountRateLimitReset(input); +}; + function usageSnapshot(input: Readonly<{ accountFingerprint?: string; fillerBytes?: number; @@ -1002,6 +1016,8 @@ describe("StateStore", () => { title: "Retained history", profileId: profile.id, }); + expect(() => store.requireAccountRateLimitResetPolicy(profile.id)) + .toThrow("ACCOUNT_RATE_LIMIT_RESET_POLICY_MISSING"); }); test("enforces every queue transition at both the store and SQLite boundaries", async () => { @@ -1557,7 +1573,7 @@ describe("StateStore", () => { const inspector = new Database(paths.database, { readonly: true, strict: true }); try { - expect(inspector.query("PRAGMA user_version").get()).toEqual({ user_version: 27 }); + expect(inspector.query("PRAGMA user_version").get()).toEqual({ user_version: 28 }); expect(inspector.query( "SELECT applied_at FROM migrations WHERE version=23", ).get()).toEqual({ applied_at: 3_000 }); @@ -4217,7 +4233,606 @@ describe("StateStore", () => { }); }); - test("journals automatic weekly reset redemption before dispatch and reuses uncertainty keys", async () => { + test("creates new profiles with explicit active reset policy atomically", async () => { + const { store } = await fixture(); + const profile = store.createProfile("Policy active"); + expect(store.requireAccountRateLimitResetPolicy(profile.id)).toMatchObject({ + state: "active_unbound", + accountFingerprint: null, + weeklyWindowResetsAt: null, + revision: 1, + }); + + const writer = new Database(store.paths.database, { create: false, strict: true }); + try { + writer.exec(` + CREATE TRIGGER test_reset_policy_insert_failure + BEFORE INSERT ON account_rate_limit_reset_policies + WHEN NEW.profile_id IN (SELECT id FROM profiles WHERE label='Policy rollback') + BEGIN SELECT RAISE(ABORT, 'injected policy insert failure'); END; + `); + } finally { + writer.close(false); + } + expect(() => store.createProfile("Policy rollback")) + .toThrow("injected policy insert failure"); + expect(store.listProfiles().map((candidate) => candidate.label)) + .not.toContain("Policy rollback"); + }); + + test("migrates every nonremoved v27 profile into fail-closed reconciliation", async () => { + const { store } = await fixture(); + const email = "legacy-policy@example.com"; + const signedIn = signInProfile(store, "Legacy signed in", email); + const signedOut = store.createProfile("Legacy signed out"); + const removed = store.createProfile("Legacy removed"); + store.removeProfile(removed.id); + const accountFingerprint = resetAccountFingerprint(email); + const prepared = prepareAuthorizedReset(store, { + profileId: signedIn.id, + processGeneration: signedIn.processGeneration, + accountFingerprint, + weeklyWindowResetsAt: 500_000_000, + observedUsedPercent: 99, + }); + expect(store.beginAccountRateLimitReset(prepared.idempotencyKey).state) + .toBe("effect_started"); + const paths = store.paths; + store.close(); + stores.splice(stores.indexOf(store), 1); + + const legacy = new Database(paths.database, { create: false, strict: true }); + try { + legacy.exec(` + DROP TRIGGER account_rate_limit_reset_attempt_policy_insert_guard; + DROP TRIGGER account_rate_limit_reset_attempt_policy_begin_guard; + DROP TRIGGER account_rate_limit_reset_attempt_policy_close_guard; + DROP TRIGGER account_rate_limit_reset_rebind_policy_guard; + DROP TRIGGER account_rate_limit_reset_policy_insert_guard; + DROP TRIGGER account_rate_limit_reset_policy_transition_guard; + DROP TRIGGER account_rate_limit_reset_policy_delete_guard; + DROP TABLE account_rate_limit_reset_policies; + DELETE FROM migrations WHERE version=28; + PRAGMA user_version=27; + `); + } finally { + legacy.close(false); + } + + const migrated = new StateStore(paths, { now: () => 2_000 }); + stores.push(migrated); + for (const profileId of [signedIn.id, signedOut.id]) { + expect(migrated.requireAccountRateLimitResetPolicy(profileId)).toMatchObject({ + state: "reconciliation_required", + accountFingerprint: null, + weeklyWindowResetsAt: null, + revision: 1, + }); + } + expect(() => migrated.requireAccountRateLimitResetPolicy(removed.id)) + .toThrow("ACCOUNT_RATE_LIMIT_RESET_POLICY_MISSING"); + expect(migrated.readRecoverableAccountRateLimitReset( + signedIn.id, + accountFingerprint, + )).toMatchObject({ + idempotencyKey: prepared.idempotencyKey, + outcome: null, + state: "effect_started", + }); + expect(() => migrated.prepareAccountRateLimitReset({ + profileId: signedIn.id, + processGeneration: signedIn.processGeneration, + accountFingerprint, + weeklyWindowResetsAt: 500_000_000, + observedUsedPercent: 99, + })).toThrow("ACCOUNT_RATE_LIMIT_RESET_POLICY_NOT_ACTIVE"); + const replacementEmail = "legacy-policy-replacement@example.com"; + expect(migrated.setProfileState( + signedIn.id, + signedIn.processGeneration, + "signed_in", + { email: replacementEmail, plan: "Plus" }, + )).toBe(true); + const replacementFingerprint = resetAccountFingerprint(replacementEmail); + expect(migrated.authorizeAccountRateLimitResetPolicy({ + profileId: signedIn.id, + processGeneration: signedIn.processGeneration, + accountFingerprint: replacementFingerprint, + weeklyWindowDurationMinutes: 10_080, + weeklyWindowResetsAt: 500_100_000, + })).toMatchObject({ + decision: "suppress", + policy: { + accountFingerprint: replacementFingerprint, + state: "window_suppressed", + }, + }); + expect(migrated.readRecoverableAccountRateLimitReset( + signedIn.id, + accountFingerprint, + )).toBeNull(); + expect(migrated.latestAccountRateLimitResetAttempt( + signedIn.id, + accountFingerprint, + )).toMatchObject({ + idempotencyKey: prepared.idempotencyKey, + localResolution: "account_identity_changed", + outcome: null, + state: "closed", + }); + const inspector = new Database(paths.database, { readonly: true, strict: true }); + try { + expect(inspector.query("PRAGMA user_version").get()).toEqual({ user_version: 28 }); + expect(inspector.query( + "SELECT COUNT(*) AS count FROM account_rate_limit_reset_attempts", + ).get()).toEqual({ count: 1 }); + } finally { + inspector.close(false); + } + }); + + test("reconciles retained partial-v28 policies when user_version is still 27", async () => { + const { store } = await fixture(); + const email = "partial-policy@example.com"; + const profile = signInProfile(store, "Partial reset policy", email); + const accountFingerprint = resetAccountFingerprint(email); + const prepared = prepareAuthorizedReset(store, { + profileId: profile.id, + processGeneration: profile.processGeneration, + accountFingerprint, + weeklyWindowResetsAt: 500_000_000, + observedUsedPercent: 99, + }); + store.beginAccountRateLimitReset(prepared.idempotencyKey); + const previousPolicy = store.requireAccountRateLimitResetPolicy(profile.id); + const paths = store.paths; + store.close(); + stores.splice(stores.indexOf(store), 1); + + const partial = new Database(paths.database, { create: false, strict: true }); + try { + partial.exec("DELETE FROM migrations WHERE version=28; PRAGMA user_version=27"); + } finally { + partial.close(false); + } + + const migrated = new StateStore(paths, { now: () => 2_000 }); + stores.push(migrated); + expect(migrated.requireAccountRateLimitResetPolicy(profile.id)).toMatchObject({ + state: "reconciliation_required", + accountFingerprint: null, + weeklyWindowResetsAt: null, + revision: previousPolicy.revision + 1, + }); + expect(migrated.readRecoverableAccountRateLimitReset( + profile.id, + accountFingerprint, + )).toMatchObject({ + idempotencyKey: prepared.idempotencyKey, + outcome: null, + state: "effect_started", + }); + }); + + test("persists reset-policy reconciliation until the suppressed boundary has elapsed", async () => { + const home = await realpath(await mkdtemp(join(tmpdir(), "hra-policy-boundary-"))); + const paths = resolveStatePaths({ homeDirectory: home, platform: "darwin" }); + await initializeStatePaths(paths); + let now = 1_000; + const store = new StateStore(paths, { now: () => now }); + stores.push(store); + const firstEmail = "policy-first@example.com"; + const profile = signInProfile(store, "Policy transitions", firstEmail); + const firstFingerprint = resetAccountFingerprint(firstEmail); + const firstWindow = 10_000; + const first = store.authorizeAccountRateLimitResetPolicy({ + profileId: profile.id, + processGeneration: profile.processGeneration, + accountFingerprint: firstFingerprint, + weeklyWindowDurationMinutes: 10_080, + weeklyWindowResetsAt: firstWindow, + }); + expect(first).toMatchObject({ decision: "allow", policy: { state: "active_bound" } }); + expect(store.authorizeAccountRateLimitResetPolicy({ + profileId: profile.id, + processGeneration: profile.processGeneration, + accountFingerprint: firstFingerprint, + weeklyWindowDurationMinutes: 10_080, + weeklyWindowResetsAt: firstWindow - 1, + })).toMatchObject({ + decision: "block", + reason: "weekly_window_nonmonotonic", + policy: { revision: first.policy.revision }, + }); + expect(store.authorizeAccountRateLimitResetPolicy({ + profileId: profile.id, + processGeneration: profile.processGeneration, + accountFingerprint: firstFingerprint, + weeklyWindowDurationMinutes: 10_080, + weeklyWindowResetsAt: firstWindow + 1_000, + })).toMatchObject({ + decision: "allow", + policy: { state: "active_bound", weeklyWindowResetsAt: firstWindow + 1_000 }, + }); + + const secondEmail = "policy-second@example.com"; + expect(store.setProfileState( + profile.id, + profile.processGeneration, + "signed_in", + { email: secondEmail, plan: "Plus" }, + )).toBe(true); + expect(store.requireAccountRateLimitResetPolicy(profile.id)).toMatchObject({ + state: "reconciliation_required", + accountFingerprint: null, + weeklyWindowResetsAt: null, + }); + const secondFingerprint = resetAccountFingerprint(secondEmail); + const suppressedWindow = 20_000; + const suppressed = store.authorizeAccountRateLimitResetPolicy({ + profileId: profile.id, + processGeneration: profile.processGeneration, + accountFingerprint: secondFingerprint, + weeklyWindowDurationMinutes: 10_080, + weeklyWindowResetsAt: suppressedWindow, + }); + expect(suppressed).toMatchObject({ + decision: "suppress", + reason: "reconciliation_window", + policy: { state: "window_suppressed" }, + }); + store.nextDaemonGeneration(`boot_${"w".repeat(32)}`); + const restarted = store.requireProfileById(profile.id); + expect(store.authorizeAccountRateLimitResetPolicy({ + profileId: profile.id, + processGeneration: restarted.processGeneration, + accountFingerprint: secondFingerprint, + weeklyWindowDurationMinutes: 10_080, + weeklyWindowResetsAt: suppressedWindow, + })).toMatchObject({ + decision: "suppress", + policy: { revision: suppressed.policy.revision }, + }); + expect(store.authorizeAccountRateLimitResetPolicy({ + profileId: profile.id, + processGeneration: restarted.processGeneration, + accountFingerprint: secondFingerprint, + weeklyWindowDurationMinutes: 10_080, + weeklyWindowResetsAt: suppressedWindow - 1_000, + })).toMatchObject({ + decision: "block", + reason: "weekly_window_nonmonotonic", + policy: { revision: suppressed.policy.revision }, + }); + expect(store.authorizeAccountRateLimitResetPolicy({ + profileId: profile.id, + processGeneration: restarted.processGeneration, + accountFingerprint: secondFingerprint, + weeklyWindowDurationMinutes: 10_080, + weeklyWindowResetsAt: suppressedWindow + 1_000, + })).toMatchObject({ + decision: "block", + reason: "weekly_window_nonmonotonic", + policy: { revision: suppressed.policy.revision }, + }); + now = suppressedWindow - 1; + expect(store.authorizeAccountRateLimitResetPolicy({ + profileId: profile.id, + processGeneration: restarted.processGeneration, + accountFingerprint: secondFingerprint, + weeklyWindowDurationMinutes: 10_080, + weeklyWindowResetsAt: suppressedWindow, + })).toMatchObject({ + decision: "suppress", + policy: { revision: suppressed.policy.revision }, + }); + expect(store.authorizeAccountRateLimitResetPolicy({ + profileId: profile.id, + processGeneration: restarted.processGeneration, + accountFingerprint: secondFingerprint, + weeklyWindowDurationMinutes: 10_080, + weeklyWindowResetsAt: suppressedWindow + 1_000, + })).toMatchObject({ + decision: "block", + reason: "weekly_window_nonmonotonic", + policy: { revision: suppressed.policy.revision }, + }); + now = suppressedWindow; + const activated = store.authorizeAccountRateLimitResetPolicy({ + profileId: profile.id, + processGeneration: restarted.processGeneration, + accountFingerprint: secondFingerprint, + weeklyWindowDurationMinutes: 10_080, + weeklyWindowResetsAt: suppressedWindow + 1_000, + }); + expect(activated).toMatchObject({ + decision: "allow", + policy: { state: "active_bound", weeklyWindowResetsAt: suppressedWindow + 1_000 }, + }); + expect(store.authorizeAccountRateLimitResetPolicy({ + profileId: profile.id, + processGeneration: restarted.processGeneration, + accountFingerprint: secondFingerprint, + weeklyWindowDurationMinutes: 10_080, + weeklyWindowResetsAt: suppressedWindow + 2_000, + })).toMatchObject({ + decision: "allow", + policy: { state: "active_bound", weeklyWindowResetsAt: suppressedWindow + 2_000 }, + }); + + const thirdEmail = "policy-third@example.com"; + expect(store.setProfileState( + profile.id, + restarted.processGeneration, + "signed_in", + { email: thirdEmail, plan: "Plus" }, + )).toBe(true); + expect(store.requireAccountRateLimitResetPolicy(profile.id)).toMatchObject({ + state: "reconciliation_required", + accountFingerprint: null, + weeklyWindowResetsAt: null, + }); + const thirdFingerprint = resetAccountFingerprint(thirdEmail); + const pending = store.authorizeAccountRateLimitResetPolicy({ + profileId: profile.id, + processGeneration: restarted.processGeneration, + accountFingerprint: thirdFingerprint, + weeklyWindowDurationMinutes: 300, + weeklyWindowResetsAt: suppressedWindow + 3_000, + }); + expect(pending).toMatchObject({ + decision: "block", + reason: "weekly_window_unavailable", + policy: { state: "reconciliation_required" }, + }); + expect(store.authorizeAccountRateLimitResetPolicy({ + profileId: profile.id, + processGeneration: restarted.processGeneration, + accountFingerprint: thirdFingerprint, + weeklyWindowDurationMinutes: 300, + weeklyWindowResetsAt: suppressedWindow + 3_000, + })).toMatchObject({ + decision: "block", + reason: "weekly_window_unavailable", + policy: { revision: pending.policy.revision, state: "reconciliation_required" }, + }); + }); + + test("re-pends bound identity drift and closes every old-identity recoverable state", async () => { + const { store } = await fixture(); + const recoverableStates = [ + "prepared", + "retryable", + "ambiguous", + "effect_started", + ] as const; + + for (const [index, recoverableState] of recoverableStates.entries()) { + const firstEmail = `identity-${recoverableState}@example.com`; + const profile = signInProfile( + store, + `Identity ${recoverableState}`, + firstEmail, + ); + expect(store.requireAccountRateLimitResetPolicy(profile.id)).toMatchObject({ + state: "active_unbound", + accountFingerprint: null, + }); + const firstFingerprint = resetAccountFingerprint(firstEmail); + const prepared = prepareAuthorizedReset(store, { + profileId: profile.id, + processGeneration: profile.processGeneration, + accountFingerprint: firstFingerprint, + weeklyWindowResetsAt: 500_000_000 + index, + observedUsedPercent: 99, + }); + if (recoverableState !== "prepared") { + store.beginAccountRateLimitReset(prepared.idempotencyKey); + } + if (recoverableState === "retryable" || recoverableState === "ambiguous") { + store.deferAccountRateLimitReset(prepared.idempotencyKey, recoverableState); + } + const policyBeforeDrift = store.requireAccountRateLimitResetPolicy(profile.id); + const secondEmail = `replacement-${recoverableState}@example.com`; + expect(store.setProfileState( + profile.id, + profile.processGeneration, + "signed_in", + { email: secondEmail, plan: "Plus" }, + )).toBe(true); + + expect(store.requireAccountRateLimitResetPolicy(profile.id)).toMatchObject({ + state: "reconciliation_required", + accountFingerprint: null, + weeklyWindowResetsAt: null, + revision: policyBeforeDrift.revision + 1, + }); + expect(store.latestAccountRateLimitResetAttempt(profile.id, firstFingerprint)) + .toMatchObject({ + idempotencyKey: prepared.idempotencyKey, + localResolution: "account_identity_changed", + outcome: null, + state: "closed", + }); + expect(store.readRecoverableAccountRateLimitReset(profile.id, firstFingerprint)) + .toBeNull(); + + const secondFingerprint = resetAccountFingerprint(secondEmail); + expect(store.authorizeAccountRateLimitResetPolicy({ + profileId: profile.id, + processGeneration: profile.processGeneration, + accountFingerprint: secondFingerprint, + weeklyWindowDurationMinutes: 10_080, + weeklyWindowResetsAt: 500_100_000 + index, + })).toMatchObject({ + decision: "suppress", + reason: "reconciliation_window", + policy: { + accountFingerprint: secondFingerprint, + state: "window_suppressed", + }, + }); + } + }); + + test("refuses to reopen a current database with missing reset policy authority", async () => { + const { store } = await fixture(); + const profile = store.createProfile("Missing reset policy"); + const paths = store.paths; + store.close(); + stores.splice(stores.indexOf(store), 1); + const damaged = new Database(paths.database, { create: false, strict: true }); + try { + damaged.exec("DROP TRIGGER account_rate_limit_reset_policy_delete_guard"); + damaged.query( + "DELETE FROM account_rate_limit_reset_policies WHERE profile_id=?", + ).run(profile.id); + } finally { + damaged.close(false); + } + expect(() => new StateStore(paths)) + .toThrow("STATE_ACCOUNT_RATE_LIMIT_RESET_POLICY_MISSING"); + }); + + test("readonly open rejects a stale same-name reset-policy guard", async () => { + const { store } = await fixture(); + const paths = store.paths; + store.close(); + stores.splice(stores.indexOf(store), 1); + const damaged = new Database(paths.database, { create: false, strict: true }); + try { + damaged.exec(` + DROP TRIGGER account_rate_limit_reset_policy_transition_guard; + CREATE TRIGGER account_rate_limit_reset_policy_transition_guard + BEFORE UPDATE ON account_rate_limit_reset_policies + BEGIN SELECT 1; END; + `); + } finally { + damaged.close(false); + } + + expect(() => new StateStore(paths, { readonly: true })) + .toThrow("STATE_SCHEMA_V28_STRUCTURE_INVALID"); + }); + + test("readonly open rejects a weakened same-name reset-policy table", async () => { + const { store } = await fixture(); + const paths = store.paths; + store.close(); + stores.splice(stores.indexOf(store), 1); + const damaged = new Database(paths.database, { create: false, strict: true }); + try { + const triggers = damaged.query( + `SELECT name,sql FROM sqlite_master + WHERE type='trigger' AND ( + name LIKE 'account_rate_limit_reset_policy_%' + OR name='account_rate_limit_reset_attempt_transition_guard' + OR name LIKE 'account_rate_limit_reset_attempt_policy_%' + OR name IN ( + 'account_rate_limit_reset_rebind_policy_guard', + 'account_rate_limit_reset_rebind_insert_guard' + ) + ) ORDER BY name`, + ).all().map((row) => z.object({ name: z.string(), sql: z.string() }) + .strict().parse(row)); + for (const trigger of triggers) { + damaged.exec(`DROP TRIGGER ${trigger.name}`); + } + damaged.exec(` + ALTER TABLE account_rate_limit_reset_policies + RENAME TO account_rate_limit_reset_policies_strict; + CREATE TABLE account_rate_limit_reset_policies ( + profile_id TEXT PRIMARY KEY, + state TEXT NOT NULL, + account_fingerprint TEXT, + weekly_window_resets_at INTEGER, + revision INTEGER NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ); + INSERT INTO account_rate_limit_reset_policies + SELECT * FROM account_rate_limit_reset_policies_strict; + DROP TABLE account_rate_limit_reset_policies_strict; + `); + for (const trigger of triggers) damaged.exec(trigger.sql); + } finally { + damaged.close(false); + } + + expect(() => new StateStore(paths, { readonly: true })) + .toThrow("STATE_SCHEMA_V28_STRUCTURE_INVALID"); + }); + + test("readonly open rejects a corrupt reset-policy row under exact guards", async () => { + const { store } = await fixture(); + const profile = store.createProfile("Corrupt reset policy"); + const paths = store.paths; + store.close(); + stores.splice(stores.indexOf(store), 1); + const damaged = new Database(paths.database, { create: false, strict: true }); + try { + const transition = z.object({ sql: z.string() }).strict().parse( + damaged.query( + `SELECT sql FROM sqlite_master + WHERE type='trigger' + AND name='account_rate_limit_reset_policy_transition_guard'`, + ).get(), + ).sql; + damaged.exec(` + DROP TRIGGER account_rate_limit_reset_policy_transition_guard; + PRAGMA ignore_check_constraints=ON; + `); + damaged.query( + `UPDATE account_rate_limit_reset_policies + SET state='active_bound',account_fingerprint=NULL, + weekly_window_resets_at=NULL,revision=revision+1 + WHERE profile_id=?`, + ).run(profile.id); + damaged.exec(transition); + damaged.exec("PRAGMA ignore_check_constraints=OFF"); + } finally { + damaged.close(false); + } + + expect(() => new StateStore(paths, { readonly: true })) + .toThrow("STATE_ACCOUNT_RATE_LIMIT_RESET_POLICY_INVALID"); + }); + + test("readonly open rejects reset policy authority outside the live profile set", async () => { + const { store } = await fixture(); + const paths = store.paths; + store.close(); + stores.splice(stores.indexOf(store), 1); + const damaged = new Database(paths.database, { create: false, strict: true }); + try { + const insertGuard = z.object({ sql: z.string() }).strict().parse( + damaged.query( + `SELECT sql FROM sqlite_master + WHERE type='trigger' + AND name='account_rate_limit_reset_policy_insert_guard'`, + ).get(), + ).sql; + damaged.exec(` + DROP TRIGGER account_rate_limit_reset_policy_insert_guard; + PRAGMA foreign_keys=OFF; + `); + damaged.query( + `INSERT INTO account_rate_limit_reset_policies( + profile_id,state,account_fingerprint,weekly_window_resets_at, + revision,created_at,updated_at + ) VALUES (?,'active_unbound',NULL,NULL,1,1000,1000)`, + ).run("acct_00000000000000000000000000000028"); + damaged.exec(insertGuard); + damaged.exec("PRAGMA foreign_keys=ON"); + } finally { + damaged.close(false); + } + + expect(() => new StateStore(paths, { readonly: true })) + .toThrow("STATE_ACCOUNT_RATE_LIMIT_RESET_POLICY_ORPHANED"); + }); + + test("journals automatic weekly reset redemption and retries only the same indeterminate key", async () => { const { store } = await fixture(); const email = "reset@example.com"; const profile = signInProfile(store, "Reset journal", email); @@ -4225,10 +4840,10 @@ describe("StateStore", () => { profileId: profile.id, processGeneration: profile.processGeneration, accountFingerprint: resetAccountFingerprint(email), - weeklyWindowResetsAt: 2_000_000_000_000, + weeklyWindowResetsAt: 500_000_000, observedUsedPercent: 99, }; - const prepared = store.prepareAccountRateLimitReset(input); + const prepared = prepareAuthorizedReset(store, input); expect(prepared).toMatchObject({ profileId: input.profileId, originProcessGeneration: input.processGeneration, @@ -4249,21 +4864,27 @@ describe("StateStore", () => { .toBe("effect_started"); expect(store.deferAccountRateLimitReset(prepared.idempotencyKey, "ambiguous").state) .toBe("ambiguous"); + expect(() => store.closeAccountRateLimitReset( + prepared.idempotencyKey, + "weekly_window_changed", + )).toThrow("ACCOUNT_RATE_LIMIT_RESET_CLOSE_RESOLUTION_INVALID"); expect(store.readRecoverableAccountRateLimitReset( profile.id, input.accountFingerprint, )?.idempotencyKey).toBe(prepared.idempotencyKey); - expect(store.beginAccountRateLimitReset(prepared.idempotencyKey).state) - .toBe("effect_started"); - expect(store.settleAccountRateLimitReset(prepared.idempotencyKey, "alreadyRedeemed")) - .toMatchObject({ state: "settled", outcome: "alreadyRedeemed" }); + expect(store.beginAccountRateLimitReset(prepared.idempotencyKey)).toMatchObject({ + idempotencyKey: prepared.idempotencyKey, + state: "effect_started", + }); + expect(store.deferAccountRateLimitReset(prepared.idempotencyKey, "ambiguous")) + .toMatchObject({ idempotencyKey: prepared.idempotencyKey, state: "ambiguous" }); expect(store.latestAccountRateLimitResetAttempt( profile.id, input.accountFingerprint, )).toMatchObject({ idempotencyKey: prepared.idempotencyKey, - state: "settled", - outcome: "alreadyRedeemed", + state: "ambiguous", + outcome: null, }); expect(store.latestAccountRateLimitResetAttempt( profile.id, @@ -4277,7 +4898,133 @@ describe("StateStore", () => { expect(store.readRecoverableAccountRateLimitReset( profile.id, input.accountFingerprint, - )).toBeNull(); + )).toMatchObject({ idempotencyKey: prepared.idempotencyKey, state: "ambiguous" }); + }); + + test("enforces reset-policy dispatch and ambiguous-effect guards in SQLite", async () => { + const { store } = await fixture(); + const firstEmail = "raw-policy@example.com"; + const first = signInProfile(store, "Raw policy guard", firstEmail); + const firstFingerprint = resetAccountFingerprint(firstEmail); + const prepared = prepareAuthorizedReset(store, { + profileId: first.id, + processGeneration: first.processGeneration, + accountFingerprint: firstFingerprint, + weeklyWindowResetsAt: 500_000_000, + observedUsedPercent: 99, + }); + const changedEmail = "raw-policy-changed@example.com"; + expect(store.setProfileState( + first.id, + first.processGeneration, + "signed_in", + { email: changedEmail, plan: "Plus" }, + )).toBe(true); + expect(store.latestAccountRateLimitResetAttempt(first.id, firstFingerprint)) + .toMatchObject({ idempotencyKey: prepared.idempotencyKey, state: "closed" }); + expect(store.authorizeAccountRateLimitResetPolicy({ + profileId: first.id, + processGeneration: first.processGeneration, + accountFingerprint: resetAccountFingerprint(changedEmail), + weeklyWindowDurationMinutes: null, + weeklyWindowResetsAt: null, + })).toMatchObject({ + decision: "block", + reason: "weekly_window_unavailable", + policy: { state: "reconciliation_required" }, + }); + + const secondEmail = "raw-ambiguous@example.com"; + const second = signInProfile(store, "Raw ambiguous guard", secondEmail); + const secondFingerprint = resetAccountFingerprint(secondEmail); + const ambiguous = prepareAuthorizedReset(store, { + profileId: second.id, + processGeneration: second.processGeneration, + accountFingerprint: secondFingerprint, + weeklyWindowResetsAt: 500_000_000, + observedUsedPercent: 99, + }); + store.beginAccountRateLimitReset(ambiguous.idempotencyKey); + store.deferAccountRateLimitReset(ambiguous.idempotencyKey, "ambiguous"); + + const inspector = new Database(store.paths.database, { create: false, strict: true }); + try { + expect(() => inspector.query( + `UPDATE account_rate_limit_reset_attempts + SET state='closed',local_resolution='weekly_window_changed' + WHERE idempotency_key=?`, + ).run(ambiguous.idempotencyKey)) + .toThrow("illegal account rate-limit reset transition"); + inspector.query( + `UPDATE account_rate_limit_reset_policies + SET state='reconciliation_required',account_fingerprint=NULL, + weekly_window_resets_at=NULL,revision=revision+1, + updated_at=MAX(updated_at,?) + WHERE profile_id=?`, + ).run(2_000, second.id); + expect(() => inspector.query( + `INSERT INTO account_rate_limit_reset_attempts( + idempotency_key,profile_id,origin_process_generation, + current_process_generation,account_fingerprint,weekly_window_resets_at, + observed_used_percent,state,created_at,updated_at + ) VALUES (?,?,?,?,?,?,?,'prepared',?,?)`, + ).run( + "00000000-0000-4000-8000-000000000028", + first.id, + first.processGeneration, + first.processGeneration, + resetAccountFingerprint(changedEmail), + 500_000_001, + 99, + 2_000, + 2_000, + )).toThrow("policy does not authorize preparation"); + expect(() => inspector.query( + `UPDATE account_rate_limit_reset_attempts + SET state='effect_started' WHERE idempotency_key=?`, + ).run(ambiguous.idempotencyKey)) + .toThrow("policy does not authorize dispatch"); + expect(() => inspector.query( + `INSERT INTO account_rate_limit_reset_rebinds( + idempotency_key,from_process_generation,to_process_generation, + account_fingerprint,created_at + ) VALUES (?,?,?,?,?)`, + ).run( + ambiguous.idempotencyKey, + ambiguous.currentProcessGeneration, + ambiguous.currentProcessGeneration + 1, + secondFingerprint, + 2_000, + )).toThrow("policy does not authorize rebind"); + } finally { + inspector.close(false); + } + }); + + test("refuses to begin a reset after its authorized window expires", async () => { + const home = await realpath(await mkdtemp(join(tmpdir(), "hra-reset-expired-begin-"))); + const paths = resolveStatePaths({ homeDirectory: home, platform: "darwin" }); + await initializeStatePaths(paths); + let now = 1_000; + const store = new StateStore(paths, { now: () => now }); + stores.push(store); + const email = "expired-begin@example.com"; + const profile = signInProfile(store, "Expired reset begin", email); + const prepared = prepareAuthorizedReset(store, { + profileId: profile.id, + processGeneration: profile.processGeneration, + accountFingerprint: resetAccountFingerprint(email), + weeklyWindowResetsAt: 5_000, + observedUsedPercent: 99, + }); + + now = prepared.weeklyWindowResetsAt; + expect(() => store.beginAccountRateLimitReset(prepared.idempotencyKey)) + .toThrow("ACCOUNT_RATE_LIMIT_RESET_WINDOW_NOT_FRESH"); + expect(store.readRecoverableAccountRateLimitReset( + profile.id, + prepared.accountFingerprint, + )).toMatchObject({ idempotencyKey: prepared.idempotencyKey, state: "prepared" }); }); test("orders the most recent reset attempt by a durable sequence across clock rollback and vacuum", async () => { @@ -4290,22 +5037,22 @@ describe("StateStore", () => { const email = "ordered-reset@example.com"; const profile = signInProfile(store, "Ordered reset", email); const accountFingerprint = resetAccountFingerprint(email); - const first = store.prepareAccountRateLimitReset({ + const first = prepareAuthorizedReset(store, { profileId: profile.id, processGeneration: profile.processGeneration, accountFingerprint, - weeklyWindowResetsAt: 2_000_000_000_000, + weeklyWindowResetsAt: 500_000_000, observedUsedPercent: 99, }); store.beginAccountRateLimitReset(first.idempotencyKey); store.settleAccountRateLimitReset(first.idempotencyKey, "reset"); now = 9_000; - const second = store.prepareAccountRateLimitReset({ + const second = prepareAuthorizedReset(store, { profileId: profile.id, processGeneration: profile.processGeneration, accountFingerprint, - weeklyWindowResetsAt: 2_000_100_000_000, + weeklyWindowResetsAt: 500_100_000, observedUsedPercent: 99, }); expect(second.attemptSequence).toBeGreaterThan(first.attemptSequence); @@ -4344,9 +5091,9 @@ describe("StateStore", () => { profileId: profile.id, processGeneration: profile.processGeneration, accountFingerprint: resetAccountFingerprint(email), - weeklyWindowResetsAt: 2_000_000_000_000, + weeklyWindowResetsAt: 500_000_000, }; - const first = store.prepareAccountRateLimitReset({ + const first = prepareAuthorizedReset(store, { ...base, observedUsedPercent: 99, }); @@ -4369,15 +5116,20 @@ describe("StateStore", () => { const email = "recovery@example.com"; const profile = signInProfile(store, "Reset recovery", email); const accountFingerprint = resetAccountFingerprint(email); - const prepared = store.prepareAccountRateLimitReset({ + const prepared = prepareAuthorizedReset(store, { profileId: profile.id, processGeneration: profile.processGeneration, accountFingerprint, - weeklyWindowResetsAt: 2_000_000_000_000, + weeklyWindowResetsAt: 500_000_000, observedUsedPercent: 99, }); store.beginAccountRateLimitReset(prepared.idempotencyKey); - expect(store.recoverAccountRateLimitResetAttempts()) + expect(store.recoverAccountRateLimitResetAttempts({ + profileId: profile.id, + processGeneration: profile.processGeneration, + accountFingerprint, + weeklyWindowResetsAt: prepared.weeklyWindowResetsAt, + })) .toEqual([prepared.idempotencyKey]); expect(store.readRecoverableAccountRateLimitReset( profile.id, @@ -4389,20 +5141,53 @@ describe("StateStore", () => { expect(store.requireProfileById(profile.id).state).toBe("signed_in"); }); - test("rebinds one recoverable reset across daemon generations with the same key", async () => { - const { store } = await fixture(); + test("rebinds and retries an ambiguous key only after a later policy window activates", async () => { + const home = await realpath(await mkdtemp(join(tmpdir(), "hra-reset-later-recovery-"))); + const paths = resolveStatePaths({ homeDirectory: home, platform: "darwin" }); + await initializeStatePaths(paths); + let now = 1_000; + const store = new StateStore(paths, { now: () => now }); + stores.push(store); const email = "restart-reset@example.com"; const profile = signInProfile(store, "Reset restart", email); const accountFingerprint = resetAccountFingerprint(email); - const prepared = store.prepareAccountRateLimitReset({ + const firstWindow = 10_000; + const prepared = prepareAuthorizedReset(store, { profileId: profile.id, processGeneration: profile.processGeneration, accountFingerprint, - weeklyWindowResetsAt: 2_000_000_000_000, + weeklyWindowResetsAt: firstWindow, observedUsedPercent: 99, }); store.beginAccountRateLimitReset(prepared.idempotencyKey); - store.recoverAccountRateLimitResetAttempts(); + store.recoverAccountRateLimitResetAttempts({ + profileId: profile.id, + processGeneration: profile.processGeneration, + accountFingerprint, + weeklyWindowResetsAt: prepared.weeklyWindowResetsAt, + }); + + const migration = new Database(store.paths.database, { create: false, strict: true }); + try { + migration.query( + `UPDATE account_rate_limit_reset_policies + SET state='reconciliation_required',account_fingerprint=NULL, + weekly_window_resets_at=NULL,revision=revision+1, + updated_at=MAX(updated_at,?) + WHERE profile_id=?`, + ).run(now, profile.id); + } finally { + migration.close(false); + } + expect(store.authorizeAccountRateLimitResetPolicy({ + profileId: profile.id, + processGeneration: profile.processGeneration, + accountFingerprint, + weeklyWindowDurationMinutes: 10_080, + weeklyWindowResetsAt: firstWindow, + })).toMatchObject({ decision: "suppress", policy: { state: "window_suppressed" } }); + expect(() => store.beginAccountRateLimitReset(prepared.idempotencyKey)) + .toThrow("ACCOUNT_RATE_LIMIT_RESET_POLICY_NOT_ACTIVE"); store.nextDaemonGeneration(`boot_${"r".repeat(32)}`); const restarted = store.requireProfileById(profile.id); @@ -4416,45 +5201,95 @@ describe("StateStore", () => { currentProcessGeneration: profile.processGeneration, state: "ambiguous", }); - const rebound = store.rebindAccountRateLimitReset({ + expect(() => store.rebindAccountRateLimitReset({ idempotencyKey: prepared.idempotencyKey, expectedCurrentProcessGeneration: profile.processGeneration, nextProcessGeneration: restarted.processGeneration, accountFingerprint, + })).toThrow("ACCOUNT_RATE_LIMIT_RESET_POLICY_NOT_ACTIVE"); + expect(store.listAccountRateLimitResetRebinds(prepared.idempotencyKey)).toEqual([]); + now = firstWindow; + const laterWindow = 20_000; + expect(store.authorizeAccountRateLimitResetPolicy({ + profileId: profile.id, + processGeneration: restarted.processGeneration, + accountFingerprint, + weeklyWindowDurationMinutes: 10_080, + weeklyWindowResetsAt: laterWindow, + })).toMatchObject({ + decision: "allow", + policy: { state: "active_bound", weeklyWindowResetsAt: laterWindow }, }); - expect(rebound).toMatchObject({ + expect(store.rebindAccountRateLimitReset({ + idempotencyKey: prepared.idempotencyKey, + expectedCurrentProcessGeneration: profile.processGeneration, + nextProcessGeneration: restarted.processGeneration, + accountFingerprint, + })).toMatchObject({ idempotencyKey: prepared.idempotencyKey, - originProcessGeneration: profile.processGeneration, - currentProcessGeneration: restarted.processGeneration, state: "ambiguous", + weeklyWindowResetsAt: firstWindow, }); - expect(store.listAccountRateLimitResetRebinds(prepared.idempotencyKey)).toEqual([{ - sequence: 1, + expect(store.beginAccountRateLimitReset(prepared.idempotencyKey)).toMatchObject({ idempotencyKey: prepared.idempotencyKey, - fromProcessGeneration: profile.processGeneration, - toProcessGeneration: restarted.processGeneration, - accountFingerprint, - createdAt: expect.any(Number), - }]); - const inspector = new Database(store.paths.database, { create: false, strict: true }); - try { - expect(() => inspector.query( - `UPDATE account_rate_limit_reset_rebinds SET created_at=created_at+1 - WHERE idempotency_key=?`, - ).run(prepared.idempotencyKey)).toThrow("append-only"); - expect(() => inspector.query( - "DELETE FROM account_rate_limit_reset_rebinds WHERE idempotency_key=?", - ).run(prepared.idempotencyKey)).toThrow("append-only"); - } finally { - inspector.close(false); + state: "effect_started", + weeklyWindowResetsAt: firstWindow, + }); + expect(store.listAccountRateLimitResetRebinds(prepared.idempotencyKey)) + .toHaveLength(1); + }); + + test("keeps prepared and retryable attempts bound to their exact active window", async () => { + const { store } = await fixture(); + const attempts: Array<{ + accountFingerprint: string; + idempotencyKey: string; + profileId: ReturnType["id"]; + processGeneration: number; + }> = []; + for (const [index, state] of (["prepared", "retryable"] as const).entries()) { + const email = `exact-window-${state}@example.com`; + const profile = signInProfile(store, `Exact window ${state}`, email); + const accountFingerprint = resetAccountFingerprint(email); + const weeklyWindowResetsAt = 500_000_000 + index * 10_000; + const prepared = prepareAuthorizedReset(store, { + profileId: profile.id, + processGeneration: profile.processGeneration, + accountFingerprint, + weeklyWindowResetsAt, + observedUsedPercent: 99, + }); + if (state === "retryable") { + store.beginAccountRateLimitReset(prepared.idempotencyKey); + store.deferAccountRateLimitReset(prepared.idempotencyKey, "retryable"); + } + expect(store.authorizeAccountRateLimitResetPolicy({ + profileId: profile.id, + processGeneration: profile.processGeneration, + accountFingerprint, + weeklyWindowDurationMinutes: 10_080, + weeklyWindowResetsAt: weeklyWindowResetsAt + 1_000, + })).toMatchObject({ decision: "allow", policy: { state: "active_bound" } }); + expect(() => store.beginAccountRateLimitReset(prepared.idempotencyKey)) + .toThrow("ACCOUNT_RATE_LIMIT_RESET_POLICY_NOT_ACTIVE"); + attempts.push({ + accountFingerprint, + idempotencyKey: prepared.idempotencyKey, + profileId: profile.id, + processGeneration: profile.processGeneration, + }); + } + + store.nextDaemonGeneration(`boot_${"e".repeat(32)}`); + for (const attempt of attempts) { + const restarted = store.requireProfileById(attempt.profileId); + expect(() => store.rebindAccountRateLimitReset({ + idempotencyKey: attempt.idempotencyKey, + expectedCurrentProcessGeneration: attempt.processGeneration, + nextProcessGeneration: restarted.processGeneration, + accountFingerprint: attempt.accountFingerprint, + })).toThrow("ACCOUNT_RATE_LIMIT_RESET_POLICY_NOT_ACTIVE"); } - expect(store.prepareAccountRateLimitReset({ - profileId: restarted.id, - processGeneration: restarted.processGeneration, - accountFingerprint, - weeklyWindowResetsAt: prepared.weeklyWindowResetsAt, - observedUsedPercent: 99.9, - }).idempotencyKey).toBe(prepared.idempotencyKey); }); test("cascades rebind evidence when expired parent history is pruned", async () => { @@ -4467,8 +5302,8 @@ describe("StateStore", () => { const email = "pruned-rebind-reset@example.com"; const profile = signInProfile(store, "Reset rebind prune", email); const accountFingerprint = resetAccountFingerprint(email); - const expiringWindowResetsAt = 2_000; - const prepared = store.prepareAccountRateLimitReset({ + const expiringWindowResetsAt = 500_000; + const prepared = prepareAuthorizedReset(store, { profileId: profile.id, processGeneration: profile.processGeneration, accountFingerprint, @@ -4488,9 +5323,8 @@ describe("StateStore", () => { expect(store.listAccountRateLimitResetRebinds(prepared.idempotencyKey)) .toHaveLength(1); - now = 1_000_000; for (let index = 0; index < 129; index += 1) { - const historical = store.prepareAccountRateLimitReset({ + const historical = prepareAuthorizedReset(store, { profileId: restarted.id, processGeneration: restarted.processGeneration, accountFingerprint, @@ -4500,6 +5334,14 @@ describe("StateStore", () => { store.beginAccountRateLimitReset(historical.idempotencyKey); store.settleAccountRateLimitReset(historical.idempotencyKey, "noCredit"); } + now = 1_000_000; + prepareAuthorizedReset(store, { + profileId: restarted.id, + processGeneration: restarted.processGeneration, + accountFingerprint, + weeklyWindowResetsAt: 1_500_000, + observedUsedPercent: 99, + }); const inspector = new Database(store.paths.database, { create: false, strict: true }); try { @@ -4523,10 +5365,10 @@ describe("StateStore", () => { profileId: profile.id, processGeneration: profile.processGeneration, accountFingerprint, - weeklyWindowResetsAt: 2_000_000_000_000, + weeklyWindowResetsAt: 500_000_000, observedUsedPercent: 99, }; - const prepared = store.prepareAccountRateLimitReset(input); + const prepared = prepareAuthorizedReset(store, input); store.beginAccountRateLimitReset(prepared.idempotencyKey); store.settleAccountRateLimitReset(prepared.idempotencyKey, "reset"); store.nextDaemonGeneration(`boot_${"s".repeat(32)}`); @@ -4554,15 +5396,15 @@ describe("StateStore", () => { accountFingerprint: resetAccountFingerprint(email), observedUsedPercent: 99, }; - const settled = store.prepareAccountRateLimitReset({ + const settled = prepareAuthorizedReset(store, { ...base, - weeklyWindowResetsAt: 2_000_000_000_000, + weeklyWindowResetsAt: 500_000_000, }); store.beginAccountRateLimitReset(settled.idempotencyKey); store.settleAccountRateLimitReset(settled.idempotencyKey, "reset"); - const closed = store.prepareAccountRateLimitReset({ + const closed = prepareAuthorizedReset(store, { ...base, - weeklyWindowResetsAt: 2_000_000_001_000, + weeklyWindowResetsAt: 500_001_000, }); store.closeAccountRateLimitReset(closed.idempotencyKey, "weekly_window_changed"); @@ -4578,14 +5420,12 @@ describe("StateStore", () => { } finally { inspector.close(false); } - expect(store.prepareAccountRateLimitReset({ - ...base, - weeklyWindowResetsAt: 2_000_000_000_000, - observedUsedPercent: 100, - })).toMatchObject({ - idempotencyKey: settled.idempotencyKey, - outcome: "reset", - state: "settled", + expect(store.latestAccountRateLimitResetAttempt( + profile.id, + base.accountFingerprint, + )).toMatchObject({ + idempotencyKey: closed.idempotencyKey, + state: "closed", }); }); @@ -4596,24 +5436,18 @@ describe("StateStore", () => { let now = 1_000; const store = new StateStore(paths, { now: () => now++ }); stores.push(store); - const firstEmail = "retained-reset-000@example.com"; - const profile = signInProfile(store, "Reset retention", firstEmail); - const liveWindowResetsAt = 1_000_000; + const email = "retained-reset@example.com"; + const profile = signInProfile(store, "Reset retention", email); + const accountFingerprint = resetAccountFingerprint(email); + const firstWindowResetsAt = 500_000; let firstKey: string | null = null; for (let index = 0; index < 130; index += 1) { - const email = `retained-reset-${String(index).padStart(3, "0")}@example.com`; - expect(store.setProfileState( - profile.id, - profile.processGeneration, - "signed_in", - { email, plan: "Plus" }, - )).toBe(true); - const prepared = store.prepareAccountRateLimitReset({ + const prepared = prepareAuthorizedReset(store, { profileId: profile.id, processGeneration: profile.processGeneration, - accountFingerprint: resetAccountFingerprint(email), - weeklyWindowResetsAt: liveWindowResetsAt, + accountFingerprint, + weeklyWindowResetsAt: firstWindowResetsAt + index, observedUsedPercent: 99, }); firstKey ??= prepared.idempotencyKey; @@ -4621,34 +5455,15 @@ describe("StateStore", () => { store.settleAccountRateLimitReset(prepared.idempotencyKey, "reset"); } if (firstKey === null) throw new Error("Expected the first reset latch."); + expect(store.latestAccountRateLimitResetAttempt(profile.id, accountFingerprint)) + .not.toBeNull(); - expect(store.setProfileState( - profile.id, - profile.processGeneration, - "signed_in", - { email: firstEmail, plan: "Plus" }, - )).toBe(true); - expect(store.prepareAccountRateLimitReset({ - profileId: profile.id, - processGeneration: profile.processGeneration, - accountFingerprint: resetAccountFingerprint(firstEmail), - weeklyWindowResetsAt: liveWindowResetsAt, - observedUsedPercent: 100, - }).idempotencyKey).toBe(firstKey); - - now = liveWindowResetsAt + 1; - const currentEmail = "retained-reset-current@example.com"; - expect(store.setProfileState( - profile.id, - profile.processGeneration, - "signed_in", - { email: currentEmail, plan: "Plus" }, - )).toBe(true); - store.prepareAccountRateLimitReset({ + now = firstWindowResetsAt + 1_000; + prepareAuthorizedReset(store, { profileId: profile.id, processGeneration: profile.processGeneration, - accountFingerprint: resetAccountFingerprint(currentEmail), - weeklyWindowResetsAt: liveWindowResetsAt + 1_000_000, + accountFingerprint, + weeklyWindowResetsAt: now + 500_000, observedUsedPercent: 99, }); @@ -4658,13 +5473,16 @@ describe("StateStore", () => { `SELECT COUNT(*) AS count FROM account_rate_limit_reset_attempts WHERE profile_id=? AND state IN ('settled','closed') AND weekly_window_resets_at<=?`, - ).get(profile.id, liveWindowResetsAt)).toEqual({ count: 128 }); + ).get(profile.id, now)).toEqual({ count: 128 }); + expect(inspector.query( + "SELECT 1 FROM account_rate_limit_reset_attempts WHERE idempotency_key=?", + ).get(firstKey)).toBeNull(); } finally { inspector.close(false); } }); - test("rejects identity-mismatched rebinds without minting a duplicate key", async () => { + test("closes identity-mismatched recovery without minting a duplicate key", async () => { const { store } = await fixture(); const email = "identity-reset@example.com"; const profile = signInProfile(store, "Reset identity", email); @@ -4673,10 +5491,10 @@ describe("StateStore", () => { profileId: profile.id, processGeneration: profile.processGeneration, accountFingerprint, - weeklyWindowResetsAt: 2_000_000_000_000, + weeklyWindowResetsAt: 500_000_000, observedUsedPercent: 99, }; - const prepared = store.prepareAccountRateLimitReset(input); + const prepared = prepareAuthorizedReset(store, input); store.nextDaemonGeneration(`boot_${"i".repeat(32)}`); const restarted = store.requireProfileById(profile.id); expect(store.setProfileState( @@ -4691,9 +5509,16 @@ describe("StateStore", () => { expectedCurrentProcessGeneration: profile.processGeneration, nextProcessGeneration: restarted.processGeneration, accountFingerprint, - })).toThrow("ACCOUNT_RATE_LIMIT_RESET_REBIND_IDENTITY_MISMATCH"); + })).toThrow("ACCOUNT_RATE_LIMIT_RESET_REBIND_STATE_INVALID"); expect(store.readRecoverableAccountRateLimitReset(profile.id, accountFingerprint)) - .toMatchObject({ idempotencyKey: prepared.idempotencyKey }); + .toBeNull(); + expect(store.latestAccountRateLimitResetAttempt(profile.id, accountFingerprint)) + .toMatchObject({ + idempotencyKey: prepared.idempotencyKey, + localResolution: "account_identity_changed", + outcome: null, + state: "closed", + }); expect(store.listAccountRateLimitResetRebinds(prepared.idempotencyKey)).toEqual([]); expect(() => store.prepareAccountRateLimitReset({ ...input, @@ -4709,9 +5534,9 @@ describe("StateStore", () => { profileId: profile.id, processGeneration: profile.processGeneration, accountFingerprint: resetAccountFingerprint(email), - weeklyWindowResetsAt: 2_000_000_000_000, + weeklyWindowResetsAt: 500_000_000, }; - const first = store.prepareAccountRateLimitReset({ + const first = prepareAuthorizedReset(store, { ...base, observedUsedPercent: 99, }); @@ -5206,8 +6031,8 @@ describe("StateStore", () => { const { store } = await fixture(); const inspector = new Database(store.paths.database, { readonly: true, strict: true }); try { - expect(inspector.query("PRAGMA user_version").get()).toEqual({ user_version: 27 }); - expect(inspector.query("SELECT version FROM migrations ORDER BY version").all()).toEqual([{ version: 1 }, { version: 2 }, { version: 3 }, { version: 4 }, { version: 5 }, { version: 6 }, { version: 7 }, { version: 8 }, { version: 9 }, { version: 10 }, { version: 11 }, { version: 12 }, { version: 13 }, { version: 14 }, { version: 15 }, { version: 16 }, { version: 17 }, { version: 18 }, { version: 19 }, { version: 20 }, { version: 21 }, { version: 22 }, { version: 23 }, { version: 24 }, { version: 25 }, { version: 26 }, { version: 27 }]); + expect(inspector.query("PRAGMA user_version").get()).toEqual({ user_version: 28 }); + expect(inspector.query("SELECT version FROM migrations ORDER BY version").all()).toEqual([{ version: 1 }, { version: 2 }, { version: 3 }, { version: 4 }, { version: 5 }, { version: 6 }, { version: 7 }, { version: 8 }, { version: 9 }, { version: 10 }, { version: 11 }, { version: 12 }, { version: 13 }, { version: 14 }, { version: 15 }, { version: 16 }, { version: 17 }, { version: 18 }, { version: 19 }, { version: 20 }, { version: 21 }, { version: 22 }, { version: 23 }, { version: 24 }, { version: 25 }, { version: 26 }, { version: 27 }, { version: 28 }]); expect(inspector.query("PRAGMA table_info(account_rate_limit_reset_attempts)").all()) .toContainEqual(expect.objectContaining({ name: "attempt_sequence", type: "INTEGER", pk: 1 })); expect(inspector.query("PRAGMA table_info(account_rate_limit_reset_attempts)").all()) @@ -5221,6 +6046,15 @@ describe("StateStore", () => { ]) expect(resetAttemptColumns).toContainEqual(expect.objectContaining(expected)); expect(inspector.query("PRAGMA table_info(account_rate_limit_reset_rebinds)").all()) .toContainEqual(expect.objectContaining({ name: "sequence", type: "INTEGER", pk: 1 })); + const resetPolicyColumns = inspector + .query("PRAGMA table_info(account_rate_limit_reset_policies)").all(); + for (const expected of [ + { name: "profile_id", type: "TEXT", notnull: 1, pk: 1 }, + { name: "state", type: "TEXT", notnull: 1 }, + { name: "account_fingerprint", type: "TEXT", notnull: 0 }, + { name: "weekly_window_resets_at", type: "INTEGER", notnull: 0 }, + { name: "revision", type: "INTEGER", notnull: 1 }, + ]) expect(resetPolicyColumns).toContainEqual(expect.objectContaining(expected)); expect(inspector.query("PRAGMA table_info(profiles)").all()) .toContainEqual(expect.objectContaining({ name: "label_key", type: "TEXT" })); expect(inspector.query("PRAGMA table_info(projects)").all()) @@ -5465,7 +6299,7 @@ describe("StateStore", () => { stores.push(migrated); const inspector = new Database(paths.database, { readonly: true, strict: true }); try { - expect(inspector.query("PRAGMA user_version").get()).toEqual({ user_version: 27 }); + expect(inspector.query("PRAGMA user_version").get()).toEqual({ user_version: 28 }); expect(inspector.query( `SELECT name FROM sqlite_master WHERE type='trigger' AND name IN ( @@ -5573,7 +6407,7 @@ describe("StateStore", () => { }); const inspector = new Database(paths.database, { readonly: true, strict: true }); try { - expect(inspector.query("PRAGMA user_version").get()).toEqual({ user_version: 27 }); + expect(inspector.query("PRAGMA user_version").get()).toEqual({ user_version: 28 }); expect(JSON.stringify(inspector.query( "SELECT display_json FROM provider_interactions ORDER BY public_id", ).all())).not.toContain("allowsSessionApproval"); @@ -5688,7 +6522,7 @@ describe("StateStore", () => { const inspector = new Database(paths.database, { readonly: true, strict: true }); try { - expect(inspector.query("PRAGMA user_version").get()).toEqual({ user_version: 27 }); + expect(inspector.query("PRAGMA user_version").get()).toEqual({ user_version: 28 }); expect(inspector.query( "SELECT revision,state FROM provider_interaction_transitions WHERE public_id=? ORDER BY revision", ).all(interactionId)).toEqual([ @@ -5745,7 +6579,7 @@ describe("StateStore", () => { const inspector = new Database(paths.database, { readonly: true, strict: true }); try { - expect(inspector.query("PRAGMA user_version").get()).toEqual({ user_version: 27 }); + expect(inspector.query("PRAGMA user_version").get()).toEqual({ user_version: 28 }); expect(inspector.query( "SELECT revision,state FROM provider_interaction_transitions WHERE public_id=? ORDER BY revision", ).all(interactionId)).toEqual([{ revision: 1, state: "pending" }]); @@ -5833,7 +6667,7 @@ describe("StateStore", () => { const inspector = new Database(paths.database, { readonly: true, strict: true }); try { - expect(inspector.query("PRAGMA user_version").get()).toEqual({ user_version: 27 }); + expect(inspector.query("PRAGMA user_version").get()).toEqual({ user_version: 28 }); expect(inspector.query( "SELECT enqueue_sequence FROM queue_entries ORDER BY enqueue_sequence", ).all()).toEqual([ @@ -5936,7 +6770,7 @@ describe("StateStore", () => { const inspector = new Database(paths.database, { readonly: true, strict: true }); try { - expect(inspector.query("PRAGMA user_version").get()).toEqual({ user_version: 27 }); + expect(inspector.query("PRAGMA user_version").get()).toEqual({ user_version: 28 }); expect(inspector.query( "SELECT reason,required_at FROM security_scrub_authority WHERE singleton=1", ).get()).toEqual({ reason: "mcp_url_redaction", required_at: 9_000 }); @@ -6050,7 +6884,7 @@ describe("StateStore", () => { expect("providerUpdatedAt" in preserved).toBe(false); const inspector = new Database(paths.database, { readonly: true, strict: true }); try { - expect(inspector.query("PRAGMA user_version").get()).toEqual({ user_version: 27 }); + expect(inspector.query("PRAGMA user_version").get()).toEqual({ user_version: 28 }); expect(inspector.query("SELECT version, applied_at FROM migrations ORDER BY version").all()).toEqual([ { version: 1, applied_at: 1000 }, { version: 2, applied_at: 2000 }, @@ -6079,6 +6913,7 @@ describe("StateStore", () => { { version: 25, applied_at: 2000 }, { version: 26, applied_at: 2000 }, { version: 27, applied_at: 2000 }, + { version: 28, applied_at: 2000 }, ]); expect(inspector.query("PRAGMA table_info(sessions)").all()).toContainEqual(expect.objectContaining({ name: "provider_updated_at" })); expect(inspector.query("SELECT label,label_key FROM profiles").get()).toEqual({ @@ -6125,7 +6960,7 @@ describe("StateStore", () => { }); const inspector = new Database(paths.database, { readonly: true, strict: true }); try { - expect(inspector.query("PRAGMA user_version").get()).toEqual({ user_version: 27 }); + expect(inspector.query("PRAGMA user_version").get()).toEqual({ user_version: 28 }); expect(inspector.query("SELECT applied_at FROM migrations WHERE version=3").get()).toEqual({ applied_at: 9_000, }); @@ -6145,9 +6980,9 @@ describe("StateStore", () => { const paths = resolveStatePaths({ homeDirectory: home, platform: "darwin" }); await initializeStatePaths(paths); const newer = new Database(paths.database, { create: true, strict: true }); - newer.exec("PRAGMA user_version = 28"); + newer.exec("PRAGMA user_version = 29"); newer.close(false); await chmod(paths.database, 0o600); - expect(() => new StateStore(paths)).toThrow("STATE_SCHEMA_NEWER:28:27"); + expect(() => new StateStore(paths)).toThrow("STATE_SCHEMA_NEWER:29:28"); }); }); diff --git a/src/storage/state-store.ts b/src/storage/state-store.ts index ea829dc..1f7d528 100644 --- a/src/storage/state-store.ts +++ b/src/storage/state-store.ts @@ -46,6 +46,7 @@ import { } from "../domain/runtime-profile"; import { ACCOUNT_USAGE_HISTORY_PAGE_LIMIT, + CODEX_WEEKLY_RATE_LIMIT_WINDOW_MINUTES, accountRateLimitResetOutcomeSchema, storedAccountUsageSnapshotSchema, type AccountRateLimitResetOutcome, @@ -353,6 +354,43 @@ export type AccountRateLimitResetRebindRecord = Readonly<{ createdAt: number; }>; +const accountRateLimitResetPolicyStateSchema = z.enum([ + "active_unbound", + "reconciliation_required", + "window_suppressed", + "active_bound", +]); + +export type AccountRateLimitResetPolicyRecord = Readonly<{ + profileId: ProfileId; + state: z.infer; + accountFingerprint: string | null; + weeklyWindowResetsAt: number | null; + revision: number; + createdAt: number; + updatedAt: number; +}>; + +export type AccountRateLimitResetPolicyDecision = + | Readonly<{ + decision: "allow"; + reason: "active"; + policy: AccountRateLimitResetPolicyRecord; + }> + | Readonly<{ + decision: "suppress"; + reason: "reconciliation_window"; + policy: AccountRateLimitResetPolicyRecord; + }> + | Readonly<{ + decision: "block"; + reason: + | "weekly_window_unavailable" + | "weekly_window_nonmonotonic" + | "account_identity_changed"; + policy: AccountRateLimitResetPolicyRecord; + }>; + const accountRateLimitResetAttemptRowSchema = z.object({ attempt_sequence: z.number().int().positive().max(Number.MAX_SAFE_INTEGER), idempotency_key: z.string().uuid(), @@ -378,6 +416,16 @@ const accountRateLimitResetRebindRowSchema = z.object({ created_at: unixMillisecondsSchema, }).strict(); +const accountRateLimitResetPolicyRowSchema = z.object({ + profile_id: profileIdSchema, + state: accountRateLimitResetPolicyStateSchema, + account_fingerprint: z.string().regex(/^[a-f0-9]{64}$/u).nullable(), + weekly_window_resets_at: unixMillisecondsSchema.nullable(), + revision: z.number().int().positive().max(Number.MAX_SAFE_INTEGER), + created_at: unixMillisecondsSchema, + updated_at: unixMillisecondsSchema, +}).strict(); + const mapAccountRateLimitResetAttempt = ( value: unknown, ): AccountRateLimitResetAttemptRecord => { @@ -413,6 +461,32 @@ const mapAccountRateLimitResetRebind = ( }; }; +const mapAccountRateLimitResetPolicy = ( + value: unknown, +): AccountRateLimitResetPolicyRecord => { + const row = accountRateLimitResetPolicyRowSchema.parse(value); + const isUnbound = row.state === "active_unbound" + || row.state === "reconciliation_required"; + const hasNoBinding = row.account_fingerprint === null + && row.weekly_window_resets_at === null; + const hasCompleteBinding = row.account_fingerprint !== null + && row.weekly_window_resets_at !== null; + if ( + (isUnbound && !hasNoBinding) + || (!isUnbound && !hasCompleteBinding) + || row.updated_at < row.created_at + ) throw new Error("ACCOUNT_RATE_LIMIT_RESET_POLICY_SHAPE_INVALID"); + return { + profileId: row.profile_id, + state: row.state, + accountFingerprint: row.account_fingerprint, + weeklyWindowResetsAt: row.weekly_window_resets_at, + revision: row.revision, + createdAt: row.created_at, + updatedAt: row.updated_at, + }; +}; + export type QueueRecord = { id: QueueId; sessionId: SessionId; @@ -616,7 +690,7 @@ type DesktopSwitchPlan = diagnostic: string; }; -const currentSchemaVersion = 27; +const currentSchemaVersion = 28; const observationTitleMaximumBytes = 320; const safeObservationTitle = (value: string): string => { @@ -1728,7 +1802,12 @@ WHEN NOT ( (OLD.state='prepared' AND NEW.state='effect_started') OR (OLD.state='effect_started' AND NEW.state IN ('ambiguous','retryable','settled')) OR (OLD.state IN ('ambiguous','retryable') AND NEW.state='effect_started') OR - (OLD.state IN ('prepared','ambiguous','retryable') AND NEW.state='closed') OR + (OLD.state IN ('prepared','retryable') AND NEW.state='closed') OR + ( + OLD.state='ambiguous' + AND NEW.state='closed' + AND NEW.local_resolution='account_identity_changed' + ) OR OLD.state=NEW.state ) BEGIN SELECT RAISE(ABORT, 'illegal account rate-limit reset transition'); END; @@ -1790,6 +1869,259 @@ WHEN EXISTS ( BEGIN SELECT RAISE(ABORT, 'account rate-limit reset rebind evidence is append-only'); END; `; +const schemaVersion28 = ` +CREATE TABLE IF NOT EXISTS account_rate_limit_reset_policies ( + profile_id TEXT PRIMARY KEY REFERENCES profiles(id) ON DELETE CASCADE, + state TEXT NOT NULL CHECK(state IN ( + 'active_unbound','reconciliation_required','window_suppressed','active_bound' + )), + account_fingerprint TEXT CHECK( + account_fingerprint IS NULL OR ( + length(account_fingerprint)=64 + AND account_fingerprint NOT GLOB '*[^a-f0-9]*' + ) + ), + weekly_window_resets_at INTEGER CHECK( + weekly_window_resets_at IS NULL + OR weekly_window_resets_at BETWEEN 0 AND 9007199254740991 + ), + revision INTEGER NOT NULL CHECK(revision BETWEEN 1 AND 9007199254740991), + created_at INTEGER NOT NULL CHECK(created_at >= 0), + updated_at INTEGER NOT NULL CHECK(updated_at >= created_at), + CHECK( + ( + state IN ('active_unbound','reconciliation_required') + AND account_fingerprint IS NULL + AND weekly_window_resets_at IS NULL + ) OR ( + state IN ('window_suppressed','active_bound') + AND account_fingerprint IS NOT NULL + AND weekly_window_resets_at IS NOT NULL + ) + ) +) STRICT; +DROP TRIGGER IF EXISTS account_rate_limit_reset_policy_insert_guard; +CREATE TRIGGER account_rate_limit_reset_policy_insert_guard +BEFORE INSERT ON account_rate_limit_reset_policies +WHEN NEW.state NOT IN ('active_unbound','reconciliation_required') + OR NOT EXISTS ( + SELECT 1 FROM profiles p + WHERE p.id=NEW.profile_id AND p.state!='removed' + ) +BEGIN SELECT RAISE(ABORT, 'account rate-limit reset policy insert is invalid'); END; +DROP TRIGGER IF EXISTS account_rate_limit_reset_policy_transition_guard; +CREATE TRIGGER account_rate_limit_reset_policy_transition_guard +BEFORE UPDATE ON account_rate_limit_reset_policies +WHEN NEW.profile_id!=OLD.profile_id + OR NEW.created_at!=OLD.created_at + OR NEW.revision!=OLD.revision+1 + OR NEW.updated_atOLD.weekly_window_resets_at + AND NEW.updated_at>=OLD.weekly_window_resets_at + ) OR ( + OLD.state IN ( + 'active_unbound','reconciliation_required','window_suppressed','active_bound' + ) + AND NEW.state='reconciliation_required' + ) OR ( + OLD.state='active_bound' + AND NEW.state='active_bound' + AND NEW.account_fingerprint=OLD.account_fingerprint + AND NEW.weekly_window_resets_at>OLD.weekly_window_resets_at + ) + ) +BEGIN SELECT RAISE(ABORT, 'illegal account rate-limit reset policy transition'); END; +DROP TRIGGER IF EXISTS account_rate_limit_reset_policy_delete_guard; +CREATE TRIGGER account_rate_limit_reset_policy_delete_guard +BEFORE DELETE ON account_rate_limit_reset_policies +WHEN EXISTS ( + SELECT 1 FROM profiles p WHERE p.id=OLD.profile_id AND p.state!='removed' +) +BEGIN SELECT RAISE(ABORT, 'account rate-limit reset policy is required'); END; +DROP TRIGGER IF EXISTS account_rate_limit_reset_attempt_transition_guard; +CREATE TRIGGER account_rate_limit_reset_attempt_transition_guard +BEFORE UPDATE OF state ON account_rate_limit_reset_attempts +WHEN NOT ( + (OLD.state='prepared' AND NEW.state='effect_started') OR + (OLD.state='effect_started' AND NEW.state IN ('ambiguous','retryable','settled')) OR + (OLD.state IN ('ambiguous','retryable') AND NEW.state='effect_started') OR + (OLD.state IN ('prepared','retryable') AND NEW.state='closed') OR + ( + OLD.state='ambiguous' + AND NEW.state='closed' + AND NEW.local_resolution='account_identity_changed' + ) OR + OLD.state=NEW.state +) +BEGIN SELECT RAISE(ABORT, 'illegal account rate-limit reset transition'); END; +DROP TRIGGER IF EXISTS account_rate_limit_reset_attempt_policy_insert_guard; +CREATE TRIGGER account_rate_limit_reset_attempt_policy_insert_guard +BEFORE INSERT ON account_rate_limit_reset_attempts +WHEN NOT EXISTS ( + SELECT 1 FROM account_rate_limit_reset_policies p + WHERE p.profile_id=NEW.profile_id + AND p.state='active_bound' + AND p.account_fingerprint=NEW.account_fingerprint + AND p.weekly_window_resets_at=NEW.weekly_window_resets_at +) +BEGIN SELECT RAISE(ABORT, 'account rate-limit reset policy does not authorize preparation'); END; +DROP TRIGGER IF EXISTS account_rate_limit_reset_attempt_policy_begin_guard; +CREATE TRIGGER account_rate_limit_reset_attempt_policy_begin_guard +BEFORE UPDATE OF state ON account_rate_limit_reset_attempts +WHEN NEW.state='effect_started' + AND OLD.state!='effect_started' + AND NOT EXISTS ( + SELECT 1 FROM account_rate_limit_reset_policies p + WHERE p.profile_id=OLD.profile_id + AND p.state='active_bound' + AND p.account_fingerprint=OLD.account_fingerprint + AND ( + ( + OLD.state IN ('prepared','retryable') + AND p.weekly_window_resets_at=OLD.weekly_window_resets_at + ) OR ( + OLD.state='ambiguous' + AND p.weekly_window_resets_at>=OLD.weekly_window_resets_at + ) + ) + ) +BEGIN SELECT RAISE(ABORT, 'account rate-limit reset policy does not authorize dispatch'); END; +DROP TRIGGER IF EXISTS account_rate_limit_reset_attempt_policy_close_guard; +CREATE TRIGGER account_rate_limit_reset_attempt_policy_close_guard +BEFORE UPDATE OF state ON account_rate_limit_reset_attempts +WHEN NEW.state='closed' + AND OLD.state!='closed' + AND NOT EXISTS ( + SELECT 1 FROM account_rate_limit_reset_policies p + WHERE p.profile_id=OLD.profile_id + AND ( + ( + p.state='active_bound' + AND p.account_fingerprint=OLD.account_fingerprint + AND p.weekly_window_resets_at>=OLD.weekly_window_resets_at + ) OR ( + NEW.local_resolution='account_identity_changed' + AND p.state='window_suppressed' + ) OR ( + NEW.local_resolution='weekly_window_changed' + AND p.state='window_suppressed' + AND p.account_fingerprint=OLD.account_fingerprint + AND p.weekly_window_resets_at>OLD.weekly_window_resets_at + ) + ) + ) +BEGIN SELECT RAISE(ABORT, 'account rate-limit reset policy does not authorize closure'); END; +DROP TRIGGER IF EXISTS account_rate_limit_reset_rebind_policy_guard; +CREATE TRIGGER account_rate_limit_reset_rebind_policy_guard +BEFORE INSERT ON account_rate_limit_reset_rebinds +WHEN NOT EXISTS ( + SELECT 1 + FROM account_rate_limit_reset_attempts a + JOIN account_rate_limit_reset_policies p ON p.profile_id=a.profile_id + WHERE a.idempotency_key=NEW.idempotency_key + AND p.state='active_bound' + AND p.account_fingerprint=a.account_fingerprint + AND ( + ( + a.state IN ('prepared','retryable') + AND p.weekly_window_resets_at=a.weekly_window_resets_at + ) OR ( + a.state='ambiguous' + AND p.weekly_window_resets_at>=a.weekly_window_resets_at + ) + ) +) +BEGIN SELECT RAISE(ABORT, 'account rate-limit reset policy does not authorize rebind'); END; +DROP TRIGGER IF EXISTS account_rate_limit_reset_rebind_insert_guard; +CREATE TRIGGER account_rate_limit_reset_rebind_insert_guard +BEFORE INSERT ON account_rate_limit_reset_rebinds +WHEN NOT EXISTS ( + SELECT 1 FROM account_rate_limit_reset_attempts a + WHERE a.idempotency_key=NEW.idempotency_key + AND a.current_process_generation=NEW.from_process_generation + AND a.account_fingerprint=NEW.account_fingerprint + AND a.state IN ('prepared','ambiguous','retryable') +) +BEGIN SELECT RAISE(ABORT, 'account rate-limit reset rebind authority is invalid'); END; +`; + +const schemaVersion28Objects = [ + { + name: "account_rate_limit_reset_policies", + table: "account_rate_limit_reset_policies", + type: "table", + }, + { + name: "account_rate_limit_reset_policy_insert_guard", + table: "account_rate_limit_reset_policies", + type: "trigger", + }, + { + name: "account_rate_limit_reset_policy_transition_guard", + table: "account_rate_limit_reset_policies", + type: "trigger", + }, + { + name: "account_rate_limit_reset_policy_delete_guard", + table: "account_rate_limit_reset_policies", + type: "trigger", + }, + { + name: "account_rate_limit_reset_attempt_transition_guard", + table: "account_rate_limit_reset_attempts", + type: "trigger", + }, + { + name: "account_rate_limit_reset_attempt_policy_insert_guard", + table: "account_rate_limit_reset_attempts", + type: "trigger", + }, + { + name: "account_rate_limit_reset_attempt_policy_begin_guard", + table: "account_rate_limit_reset_attempts", + type: "trigger", + }, + { + name: "account_rate_limit_reset_attempt_policy_close_guard", + table: "account_rate_limit_reset_attempts", + type: "trigger", + }, + { + name: "account_rate_limit_reset_rebind_policy_guard", + table: "account_rate_limit_reset_rebinds", + type: "trigger", + }, + { + name: "account_rate_limit_reset_rebind_insert_guard", + table: "account_rate_limit_reset_rebinds", + type: "trigger", + }, +] as const; + +const schemaVersion28ObjectSql = ( + object: (typeof schemaVersion28Objects)[number], +): string => { + const marker = object.type === "table" + ? `CREATE TABLE IF NOT EXISTS ${object.name}` + : `CREATE TRIGGER ${object.name}`; + const start = schemaVersion28.indexOf(marker); + const terminator = object.type === "table" ? ") STRICT;" : "END;"; + const end = schemaVersion28.indexOf(terminator, start); + if (start < 0 || end < 0) throw new Error("STATE_SCHEMA_V28_DEFINITION_INVALID"); + return schemaVersion28.slice(start, end + terminator.length); +}; + const schemaVersion24Objects = [ { name: "profiles_label_key_active", @@ -1863,7 +2195,7 @@ const sqliteSchemaObjectRowSchema = z.object({ name: z.string(), sql: z.string(), tbl_name: z.string(), - type: z.enum(["index", "trigger"]), + type: z.enum(["index", "table", "trigger"]), }).strict(); const normalizeSqlStructure = (sql: string): string => @@ -1889,6 +2221,53 @@ const assertSchemaVersion24Objects = (database: Database): void => { } }; +const assertSchemaVersion28Objects = (database: Database): void => { + const names = schemaVersion28Objects.map((object) => `'${object.name}'`).join(","); + const rows = database.query( + `SELECT type,name,tbl_name,sql FROM sqlite_master + WHERE name IN (${names}) ORDER BY name`, + ).all().map((row) => sqliteSchemaObjectRowSchema.parse(row)); + if (rows.length !== schemaVersion28Objects.length) { + throw new Error("STATE_SCHEMA_V28_STRUCTURE_INVALID"); + } + for (const expected of schemaVersion28Objects) { + const observed = rows.find((row) => row.name === expected.name); + const observedSql = observed?.sql.replace(/\bIF NOT EXISTS\b/giu, ""); + const expectedSql = schemaVersion28ObjectSql(expected) + .replace(/\bIF NOT EXISTS\b/giu, ""); + if ( + observed === undefined + || observed.type !== expected.type + || observed.tbl_name !== expected.table + || normalizeSqlStructure(observedSql ?? "") + !== normalizeSqlStructure(expectedSql) + ) throw new Error("STATE_SCHEMA_V28_STRUCTURE_INVALID"); + } +}; + +const assertAccountRateLimitResetPolicies = (database: Database): void => { + assertSchemaVersion28Objects(database); + let policies: readonly AccountRateLimitResetPolicyRecord[]; + try { + policies = database.query( + "SELECT * FROM account_rate_limit_reset_policies ORDER BY profile_id", + ).all().map(mapAccountRateLimitResetPolicy); + } catch (error: unknown) { + throw new Error("STATE_ACCOUNT_RATE_LIMIT_RESET_POLICY_INVALID", { cause: error }); + } + const policyProfileIds = new Set(policies.map((policy) => policy.profileId)); + const activeProfileIds = database.query( + "SELECT id FROM profiles WHERE state!='removed' ORDER BY id", + ).all().map((row) => z.object({ id: profileIdSchema }).strict().parse(row).id); + if (activeProfileIds.some((profileId) => !policyProfileIds.has(profileId))) { + throw new Error("STATE_ACCOUNT_RATE_LIMIT_RESET_POLICY_MISSING"); + } + const activeProfileIdSet = new Set(activeProfileIds); + if (policies.some((policy) => !activeProfileIdSet.has(policy.profileId))) { + throw new Error("STATE_ACCOUNT_RATE_LIMIT_RESET_POLICY_ORPHANED"); + } +}; + const ensureQueueMessageScrubGeneration = (database: Database): void => { if (!hasTableColumn(database, "queue_message_scrub_authority", "generation")) { database.exec( @@ -2966,6 +3345,37 @@ const migrateWritableDatabase = (database: Database, now: () => number): void => version = 27; } + if (version < 28) { + database.exec(schemaVersion28); + const migratedAt = unixMillisecondsSchema.parse(now()); + database.query( + `INSERT INTO account_rate_limit_reset_policies( + profile_id,state,account_fingerprint,weekly_window_resets_at, + revision,created_at,updated_at + ) + SELECT id,'reconciliation_required',NULL,NULL,1,?,? + FROM profiles WHERE state!='removed' ORDER BY id + ON CONFLICT(profile_id) DO UPDATE SET + state='reconciliation_required', + account_fingerprint=NULL, + weekly_window_resets_at=NULL, + revision=account_rate_limit_reset_policies.revision+1, + updated_at=MAX(account_rate_limit_reset_policies.updated_at,excluded.updated_at)`, + ).run(migratedAt, migratedAt); + database.query( + `DELETE FROM account_rate_limit_reset_policies + WHERE NOT EXISTS ( + SELECT 1 FROM profiles p + WHERE p.id=account_rate_limit_reset_policies.profile_id + AND p.state!='removed' + )`, + ).run(); + assertAccountRateLimitResetPolicies(database); + database.query("INSERT OR IGNORE INTO migrations(version, applied_at) VALUES (?, ?)").run(28, migratedAt); + database.exec("PRAGMA user_version = 28"); + version = 28; + } + // Reapplying additive objects and idempotent authority backfills makes a // restart after any pre-release partial fixture safe without changing rows. database.exec(schemaVersion9); @@ -2996,6 +3406,8 @@ const migrateWritableDatabase = (database: Database, now: () => number): void => assertWorkSchema(database); database.exec(schemaVersion27); ensureUsagePollFailureAccountFingerprint(database); + database.exec(schemaVersion28); + assertAccountRateLimitResetPolicies(database); if (hasSettledQueueMessagesToScrub(database)) { requireQueueMessageScrub(database, now(), true); } @@ -3355,6 +3767,7 @@ export class StateStore { assertSchemaVersion24Objects(this.#database); assertWorkSchema(this.#database); assertCanonicalLabelKeys(this.#database); + assertAccountRateLimitResetPolicies(this.#database); assertStateDatabaseFile(paths.database, databaseFile); } catch (error) { this.#database.close(false); @@ -3398,9 +3811,18 @@ export class StateStore { const id = createProfileId(); const parsedLabel = labelSchema.parse(label); const labelKey = canonicalLabelIdentity(parsedLabel, "ACCOUNT").key; - const now = this.#now(); - this.#database.query("INSERT INTO profiles(id,label,label_key,state,process_generation,created_at,updated_at) VALUES (?,?,?,?,?,?,?)").run(id, parsedLabel, labelKey, "signed_out", 0, now, now); - return this.requireProfile(id); + const now = unixMillisecondsSchema.parse(this.#now()); + const create = this.#database.transaction(() => { + this.#database.query("INSERT INTO profiles(id,label,label_key,state,process_generation,created_at,updated_at) VALUES (?,?,?,?,?,?,?)").run(id, parsedLabel, labelKey, "signed_out", 0, now, now); + this.#database.query( + `INSERT INTO account_rate_limit_reset_policies( + profile_id,state,account_fingerprint,weekly_window_resets_at, + revision,created_at,updated_at + ) VALUES (?,'active_unbound',NULL,NULL,1,?,?)`, + ).run(id, now, now); + return mapProfile(this.#database.query("SELECT * FROM profiles WHERE id=?").get(id)); + }); + return create.immediate(); } listProfiles(options: { includeRemoved?: boolean } = {}): readonly ProfileRecord[] { @@ -3521,6 +3943,28 @@ export class StateStore { }; } + #closeRecoverableAccountRateLimitResetIdentityAttempts(input: { + profileId: ProfileId; + accountFingerprint: string; + selection: "matching" | "different"; + now: number; + }): void { + const fingerprintPredicate = input.selection === "matching" ? "=" : "!="; + this.#database.query( + `UPDATE account_rate_limit_reset_attempts + SET state='ambiguous',updated_at=MAX(updated_at,?) + WHERE profile_id=? AND account_fingerprint${fingerprintPredicate}? + AND state='effect_started'`, + ).run(input.now, input.profileId, input.accountFingerprint); + this.#database.query( + `UPDATE account_rate_limit_reset_attempts + SET state='closed',local_resolution='account_identity_changed', + updated_at=MAX(updated_at,?) + WHERE profile_id=? AND account_fingerprint${fingerprintPredicate}? + AND state IN ('prepared','ambiguous','retryable')`, + ).run(input.now, input.profileId, input.accountFingerprint); + } + #setProfileState( profileId: ProfileId, expectedGeneration: number, @@ -3540,6 +3984,13 @@ export class StateStore { ) { return { affectedWorkIds: [] as string[], changed: false }; } + const policy = this.requireAccountRateLimitResetPolicy(profileId); + const nextAccountFingerprint = state === "signed_in" && identity?.email !== undefined + ? canonicalAccountFingerprint(identity.email) + : null; + const changedBoundIdentity = nextAccountFingerprint !== null + && policy.accountFingerprint !== null + && policy.accountFingerprint !== nextAccountFingerprint; const affectedWorkIds = state === "signed_in" ? [] : [...(workStore?.prepareProfileAuthorityChange(profileId, expectedGeneration) ?? [])]; @@ -3552,7 +4003,29 @@ export class StateStore { AND (state!='recovery_required' OR ?='recovery_required')`, ) .run(state, identity?.email ?? null, identity?.plan ?? null, now, profileId, expectedGeneration, state); - if (result.changes === 1 && (state === "signed_in" || state === "signed_out")) { + if (result.changes !== 1) throw new Error("Profile state authority changed."); + if (changedBoundIdentity) { + const previousAccountFingerprint = policy.accountFingerprint; + this.#closeRecoverableAccountRateLimitResetIdentityAttempts({ + profileId, + accountFingerprint: previousAccountFingerprint, + selection: "matching", + now, + }); + const policyChanged = this.#database.query( + `UPDATE account_rate_limit_reset_policies + SET state='reconciliation_required',account_fingerprint=NULL, + weekly_window_resets_at=NULL,revision=revision+1, + updated_at=MAX(updated_at,?) + WHERE profile_id=? AND revision=? + AND state IN ('window_suppressed','active_bound') + AND account_fingerprint=?`, + ).run(now, profileId, policy.revision, previousAccountFingerprint); + if (policyChanged.changes !== 1) { + throw new Error("ACCOUNT_RATE_LIMIT_RESET_POLICY_CONFLICT"); + } + } + if (state === "signed_in" || state === "signed_out") { this.#database.query(`UPDATE provider_login_authorities SET state='settled',settlement=?,updated_at=? WHERE profile_id=? AND process_generation=? AND state='active'`).run( @@ -3562,7 +4035,6 @@ export class StateStore { expectedGeneration, ); } - if (result.changes !== 1) throw new Error("Profile state authority changed."); return { affectedWorkIds, changed: true }; }); return update.immediate(); @@ -3633,11 +4105,29 @@ export class StateStore { } removeProfile(profileId: ProfileId): void { - const active = this.#database.query("SELECT COUNT(*) AS count FROM sessions WHERE profile_id = ? AND state NOT IN ('terminal')").get(profileId) as { count: number } | null; - if ((active?.count ?? 0) !== 0) throw new Error("Profile still owns active sessions."); - const now = this.#now(); - const result = this.#database.query("UPDATE profiles SET state='removed', provider_email=NULL, provider_plan=NULL, updated_at=? WHERE id=? AND state!='removed'").run(now, profileId); - if (result.changes !== 1) throw new SelectionError("NOT_FOUND"); + const id = profileIdSchema.parse(profileId); + const remove = this.#database.transaction(() => { + const active = this.#database.query( + "SELECT COUNT(*) AS count FROM sessions WHERE profile_id=? AND state NOT IN ('terminal')", + ).get(id) as { count: number } | null; + if ((active?.count ?? 0) !== 0) { + throw new Error("Profile still owns active sessions."); + } + const now = unixMillisecondsSchema.parse(this.#now()); + const result = this.#database.query( + `UPDATE profiles + SET state='removed',provider_email=NULL,provider_plan=NULL,updated_at=? + WHERE id=? AND state!='removed'`, + ).run(now, id); + if (result.changes !== 1) throw new SelectionError("NOT_FOUND"); + const policy = this.#database.query( + "DELETE FROM account_rate_limit_reset_policies WHERE profile_id=?", + ).run(id); + if (policy.changes !== 1) { + throw new Error("ACCOUNT_RATE_LIMIT_RESET_POLICY_MISSING"); + } + }); + remove.immediate(); } async createProject(label: string, requestedRoot: string, makeDefault = false): Promise { @@ -7881,6 +8371,197 @@ export class StateStore { return terminalize.immediate(); } + requireAccountRateLimitResetPolicy( + profileId: ProfileId, + ): AccountRateLimitResetPolicyRecord { + const parsedProfileId = profileIdSchema.parse(profileId); + const row = this.#database.query( + "SELECT * FROM account_rate_limit_reset_policies WHERE profile_id=?", + ).get(parsedProfileId); + if (row === null) throw new Error("ACCOUNT_RATE_LIMIT_RESET_POLICY_MISSING"); + try { + return mapAccountRateLimitResetPolicy(row); + } catch (error: unknown) { + throw new Error("ACCOUNT_RATE_LIMIT_RESET_POLICY_INVALID", { cause: error }); + } + } + + authorizeAccountRateLimitResetPolicy(input: { + profileId: ProfileId; + processGeneration: number; + accountFingerprint: string; + weeklyWindowDurationMinutes: number | null; + weeklyWindowResetsAt: number | null; + }): AccountRateLimitResetPolicyDecision { + const profileId = profileIdSchema.parse(input.profileId); + const processGeneration = z.number().int().positive().max(Number.MAX_SAFE_INTEGER) + .parse(input.processGeneration); + const accountFingerprint = sha256Schema.parse(input.accountFingerprint); + const weeklyWindowDurationMinutes = input.weeklyWindowDurationMinutes === null + ? null + : z.number().int().positive().max(Number.MAX_SAFE_INTEGER) + .parse(input.weeklyWindowDurationMinutes); + const weeklyWindowResetsAt = input.weeklyWindowResetsAt === null + ? null + : unixMillisecondsSchema.parse(input.weeklyWindowResetsAt); + const authorize = this.#database.transaction((): AccountRateLimitResetPolicyDecision => { + const authority = z.object({ + process_generation: z.number().int().positive().max(Number.MAX_SAFE_INTEGER), + state: z.literal("signed_in"), + provider_email: z.string().email(), + }).strict().parse(this.#database.query( + "SELECT process_generation,state,provider_email FROM profiles WHERE id=?", + ).get(profileId)); + if ( + authority.process_generation !== processGeneration + || canonicalAccountFingerprint(authority.provider_email) !== accountFingerprint + ) throw new Error("ACCOUNT_RATE_LIMIT_RESET_POLICY_AUTHORITY_CHANGED"); + + let policy = this.requireAccountRateLimitResetPolicy(profileId); + const transition = (input: { + state: AccountRateLimitResetPolicyRecord["state"]; + accountFingerprint: string | null; + weeklyWindowResetsAt: number | null; + }): AccountRateLimitResetPolicyRecord => { + const changed = this.#database.query( + `UPDATE account_rate_limit_reset_policies + SET state=?,account_fingerprint=?,weekly_window_resets_at=?, + revision=revision+1,updated_at=MAX(updated_at,?) + WHERE profile_id=? AND revision=?`, + ).run( + input.state, + input.accountFingerprint, + input.weeklyWindowResetsAt, + unixMillisecondsSchema.parse(this.#now()), + profileId, + policy.revision, + ); + if (changed.changes !== 1) { + throw new Error("ACCOUNT_RATE_LIMIT_RESET_POLICY_CONFLICT"); + } + return this.requireAccountRateLimitResetPolicy(profileId); + }; + + const now = unixMillisecondsSchema.parse(this.#now()); + const weeklyWindowMaximum = Math.min( + Number.MAX_SAFE_INTEGER, + now + CODEX_WEEKLY_RATE_LIMIT_WINDOW_MINUTES * 60_000, + ); + const hasFreshExactWeeklyWindow = weeklyWindowDurationMinutes + === CODEX_WEEKLY_RATE_LIMIT_WINDOW_MINUTES + && weeklyWindowResetsAt !== null + && weeklyWindowResetsAt > now + && weeklyWindowResetsAt <= weeklyWindowMaximum; + if ( + policy.accountFingerprint !== null + && policy.accountFingerprint !== accountFingerprint + ) { + this.#closeRecoverableAccountRateLimitResetIdentityAttempts({ + profileId, + accountFingerprint: policy.accountFingerprint, + selection: "matching", + now, + }); + policy = transition({ + state: "reconciliation_required", + accountFingerprint: null, + weeklyWindowResetsAt: null, + }); + return { decision: "block", reason: "account_identity_changed", policy }; + } + + if (!hasFreshExactWeeklyWindow) { + return { + decision: "block", + reason: "weekly_window_unavailable", + policy, + }; + } + + switch (policy.state) { + case "active_unbound": { + policy = transition({ + state: "active_bound", + accountFingerprint, + weeklyWindowResetsAt, + }); + return { decision: "allow", reason: "active", policy }; + } + case "reconciliation_required": { + policy = transition({ + state: "window_suppressed", + accountFingerprint, + weeklyWindowResetsAt, + }); + this.#closeRecoverableAccountRateLimitResetIdentityAttempts({ + profileId, + accountFingerprint, + selection: "different", + now, + }); + return { + decision: "suppress", + reason: "reconciliation_window", + policy, + }; + } + case "window_suppressed": { + if (policy.weeklyWindowResetsAt === null) { + throw new Error("ACCOUNT_RATE_LIMIT_RESET_POLICY_INVALID"); + } + if (weeklyWindowResetsAt < policy.weeklyWindowResetsAt) { + return { + decision: "block", + reason: "weekly_window_nonmonotonic", + policy, + }; + } + if (weeklyWindowResetsAt === policy.weeklyWindowResetsAt) { + return { + decision: "suppress", + reason: "reconciliation_window", + policy, + }; + } + if (now < policy.weeklyWindowResetsAt) { + return { + decision: "block", + reason: "weekly_window_nonmonotonic", + policy, + }; + } + policy = transition({ + state: "active_bound", + accountFingerprint, + weeklyWindowResetsAt, + }); + return { decision: "allow", reason: "active", policy }; + } + case "active_bound": { + if (policy.weeklyWindowResetsAt === null) { + throw new Error("ACCOUNT_RATE_LIMIT_RESET_POLICY_INVALID"); + } + if (weeklyWindowResetsAt < policy.weeklyWindowResetsAt) { + return { + decision: "block", + reason: "weekly_window_nonmonotonic", + policy, + }; + } + if (weeklyWindowResetsAt > policy.weeklyWindowResetsAt) { + policy = transition({ + state: "active_bound", + accountFingerprint, + weeklyWindowResetsAt, + }); + } + return { decision: "allow", reason: "active", policy }; + } + } + }); + return authorize.immediate(); + } + prepareAccountRateLimitReset(input: { profileId: ProfileId; processGeneration: number; @@ -7909,6 +8590,12 @@ export class StateStore { ) { throw new Error("ACCOUNT_RATE_LIMIT_RESET_AUTHORITY_CHANGED"); } + const policy = this.requireAccountRateLimitResetPolicy(profileId); + if ( + policy.state !== "active_bound" + || policy.accountFingerprint !== accountFingerprint + || policy.weeklyWindowResetsAt !== weeklyWindowResetsAt + ) throw new Error("ACCOUNT_RATE_LIMIT_RESET_POLICY_NOT_ACTIVE"); const recoverable = this.#database.query( `SELECT * FROM account_rate_limit_reset_attempts WHERE profile_id=? AND account_fingerprint=? @@ -8040,6 +8727,16 @@ export class StateStore { if (!["prepared", "ambiguous", "retryable"].includes(row.state)) { throw new Error("ACCOUNT_RATE_LIMIT_RESET_REBIND_STATE_INVALID"); } + const policy = this.requireAccountRateLimitResetPolicy(row.profileId); + const policyWindowAuthorizesAttempt = policy.weeklyWindowResetsAt !== null + && (row.state === "ambiguous" + ? policy.weeklyWindowResetsAt >= row.weeklyWindowResetsAt + : policy.weeklyWindowResetsAt === row.weeklyWindowResetsAt); + if ( + policy.state !== "active_bound" + || policy.accountFingerprint !== row.accountFingerprint + || !policyWindowAuthorizesAttempt + ) throw new Error("ACCOUNT_RATE_LIMIT_RESET_POLICY_NOT_ACTIVE"); const authority = z.object({ process_generation: z.number().int().positive().max(Number.MAX_SAFE_INTEGER), provider_email: z.string().email(), @@ -8102,6 +8799,9 @@ export class StateStore { if (!["prepared", "ambiguous", "retryable"].includes(row.state)) { throw new Error("ACCOUNT_RATE_LIMIT_RESET_CLOSE_STATE_INVALID"); } + if (row.state === "ambiguous" && resolution !== "account_identity_changed") { + throw new Error("ACCOUNT_RATE_LIMIT_RESET_CLOSE_RESOLUTION_INVALID"); + } const changed = this.#database.query( `UPDATE account_rate_limit_reset_attempts SET state='closed',local_resolution=?,updated_at=MAX(updated_at,?) @@ -8135,10 +8835,39 @@ export class StateStore { const row = mapAccountRateLimitResetAttempt(this.#database.query( `SELECT r.* FROM account_rate_limit_reset_attempts r JOIN profiles p ON p.id=r.profile_id + JOIN account_rate_limit_reset_policies policy ON policy.profile_id=r.profile_id WHERE r.idempotency_key=? AND p.state='signed_in' AND p.process_generation=r.current_process_generation AND lower(trim(p.provider_email)) IS NOT NULL`, ).get(key)); + if (row.state === "effect_started") { + throw new Error("ACCOUNT_RATE_LIMIT_RESET_EFFECT_ALREADY_STARTED"); + } + if (!["prepared", "ambiguous", "retryable"].includes(row.state)) { + throw new Error("ACCOUNT_RATE_LIMIT_RESET_BEGIN_STATE_INVALID"); + } + const policy = this.requireAccountRateLimitResetPolicy(row.profileId); + const policyWindowAuthorizesAttempt = policy.weeklyWindowResetsAt !== null + && (row.state === "ambiguous" + ? policy.weeklyWindowResetsAt >= row.weeklyWindowResetsAt + : policy.weeklyWindowResetsAt === row.weeklyWindowResetsAt); + if ( + policy.state !== "active_bound" + || policy.accountFingerprint !== row.accountFingerprint + || !policyWindowAuthorizesAttempt + ) throw new Error("ACCOUNT_RATE_LIMIT_RESET_POLICY_NOT_ACTIVE"); + const now = unixMillisecondsSchema.parse(this.#now()); + const authorizedWindowResetsAt = row.state === "ambiguous" + ? policy.weeklyWindowResetsAt + : row.weeklyWindowResetsAt; + const weeklyWindowMaximum = Math.min( + Number.MAX_SAFE_INTEGER, + now + CODEX_WEEKLY_RATE_LIMIT_WINDOW_MINUTES * 60_000, + ); + if ( + authorizedWindowResetsAt <= now + || authorizedWindowResetsAt > weeklyWindowMaximum + ) throw new Error("ACCOUNT_RATE_LIMIT_RESET_WINDOW_NOT_FRESH"); const identity = z.object({ provider_email: z.string().email() }).strict().parse( this.#database.query( `SELECT p.provider_email FROM account_rate_limit_reset_attempts r @@ -8148,18 +8877,11 @@ export class StateStore { if (canonicalAccountFingerprint(identity.provider_email) !== row.accountFingerprint) { throw new Error("ACCOUNT_RATE_LIMIT_RESET_AUTHORITY_CHANGED"); } - if (row.state === "settled") return row; - if (row.state === "effect_started") { - throw new Error("ACCOUNT_RATE_LIMIT_RESET_EFFECT_ALREADY_STARTED"); - } - if (!["prepared", "ambiguous", "retryable"].includes(row.state)) { - throw new Error("ACCOUNT_RATE_LIMIT_RESET_BEGIN_STATE_INVALID"); - } const changed = this.#database.query( `UPDATE account_rate_limit_reset_attempts SET state='effect_started',updated_at=MAX(updated_at,?) WHERE idempotency_key=? AND state=?`, - ).run(unixMillisecondsSchema.parse(this.#now()), key, row.state); + ).run(now, key, row.state); if (changed.changes !== 1) { throw new Error("ACCOUNT_RATE_LIMIT_RESET_AUTHORITY_CHANGED"); } @@ -8208,12 +8930,43 @@ export class StateStore { ).get(key)); } - recoverAccountRateLimitResetAttempts(): readonly string[] { + recoverAccountRateLimitResetAttempts(input: { + profileId: ProfileId; + processGeneration: number; + accountFingerprint: string; + weeklyWindowResetsAt: number; + }): readonly string[] { + const profileId = profileIdSchema.parse(input.profileId); + const processGeneration = z.number().int().positive().max(Number.MAX_SAFE_INTEGER) + .parse(input.processGeneration); + const accountFingerprint = sha256Schema.parse(input.accountFingerprint); + const weeklyWindowResetsAt = unixMillisecondsSchema + .parse(input.weeklyWindowResetsAt); const recover = this.#database.transaction(() => { + const authority = z.object({ + process_generation: z.number().int().positive().max(Number.MAX_SAFE_INTEGER), + provider_email: z.string().email(), + state: z.literal("signed_in"), + }).strict().parse(this.#database.query( + "SELECT process_generation,provider_email,state FROM profiles WHERE id=?", + ).get(profileId)); + if ( + authority.process_generation !== processGeneration + || canonicalAccountFingerprint(authority.provider_email) !== accountFingerprint + ) throw new Error("ACCOUNT_RATE_LIMIT_RESET_RECOVERY_AUTHORITY_CHANGED"); + const policy = this.requireAccountRateLimitResetPolicy(profileId); + if ( + policy.state !== "active_bound" + || policy.accountFingerprint !== accountFingerprint + || policy.weeklyWindowResetsAt !== weeklyWindowResetsAt + ) throw new Error("ACCOUNT_RATE_LIMIT_RESET_POLICY_NOT_ACTIVE"); const keys = this.#database.query( `SELECT idempotency_key FROM account_rate_limit_reset_attempts - WHERE state='effect_started' ORDER BY attempt_sequence`, - ).all().map((row) => z.object({ idempotency_key: z.string().uuid() }) + WHERE profile_id=? AND account_fingerprint=? + AND weekly_window_resets_at<=? AND state='effect_started' + ORDER BY attempt_sequence`, + ).all(profileId, accountFingerprint, weeklyWindowResetsAt) + .map((row) => z.object({ idempotency_key: z.string().uuid() }) .strict().parse(row).idempotency_key); for (const key of keys) { const changed = this.#database.query(