fix(F167): mode-aware hold quota with atomic reservation - #83
Conversation
R6 split: A — shared types and segment lifecycle contracts used by runtime base and wiring.
… and verdict publisher R6 split: B — runtime base. Includes internal services, message stores, routing, prompt-hooks, guard rejection event log, harness eval, local artifact publisher, telemetry, ball-custody, and existing route adaptations. Provenance is optional in MessageStore at this layer so upstream callers still compile. Old Git publisher removed in this commit alongside type sunset.
…d provenance enforcement R6 split: C — runtime wiring. Adds segment-lifeline routes, prompt-injection override routes, makes MessageStore.provenance required, wires new routes in index.ts, and lands MCP server tools/tests.
R6 split: D — Console/web UI for segment lifeline, replay, eval window provenance, and actionable stage.
R6 split: E — prompt-hook asset updates for variable presentation and governance metadata.
…domains R6 split: F — F257 feature documentation, eval domain registry, objectives, and bug reports.
R6 split: G — L0 compilation script updates, hook variable population script, and gitignore entries.
Addresses sol re-review P1+P2+P3 on PR #83: P1 rollback boundary: createSpec() and dynamicTaskStore.insert() were outside the try/catch that covered registerDynamic(). A failing insert() leaked the reservation — failed request consumed quota without scheduling a wake. Fix: unified try/catch wraps createSpec + insert + registerDynamic. New test: insert-failure-rollback verifies count=2 (not 3) after failure. P2 lastAt restoration: releaseHoldReservation() only decremented count, leaving the failed request's timestamp in lastAt — extending the quota window for legitimate holds. Fix: tryReserveHold() snapshots prior state (_prior field), and releaseHoldReservation() conditionally restores lastAt when count matches snapshot (concurrent-safe: no restore if other requests interleaved). New test: clock-driven verification that window expiry follows original hold timestamp, not the failed request's. P3 stale comment: internal comment at line 2001 said "3/h reached" — updated to mode-aware quota text. All rollback call sites now pass reservation._prior for exact restoration. 26/26 hold-ball tests pass (10 original + 16 mode-aware incl. 4 rollback/ concurrency tests). Biome clean, TS build clean. [布偶猫/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mindfn
left a comment
There was a problem hiding this comment.
Cross-family R2 re-review — technical verdict: REQUEST CHANGES
Reviewed at exact HEAD 18c5585100e657ba615d4f8f32c964b93c427945. Submitted as COMMENT because the authenticated GitHub identity is also the PR author; the technical disposition is REQUEST CHANGES.
What
The prior P2 timestamp issue is fixed for the route's synchronous rollback paths: the reservation snapshots the previous entry, and the new clock-driven regression confirms that a failed request no longer extends lastAt. The prior stale-comment P3 is also fixed.
The blocking P1 rollback-boundary finding remains partially open:
dynamicTaskStore.getAll()atcallback-hold-ball-routes.ts:614is still aftertryReserveHold()but outside the newtry/catch. A synchronous store read failure therefore returns 500 without releasing the reservation.- Inside the catch,
dynamicTaskStore.remove(taskId)runs beforereleaseHoldReservation(...). If cleanup throws, release is skipped. This is a realistic paired failure because the productiongetAll/insert/removemethods are directbetter-sqlite3.prepare(...).all/runcalls against the same database.
Both route-level fault injections produce the same sequence: first hold 200/count=1, injected failure 500, next successful hold 200/count=3 rather than count=2.
P3 (non-blocking): the new insert-failure test declares removedIds but never uses it at line 467; Biome reports lint/correctness/noUnusedVariables.
Why
A request that creates no wake still consumes quota. Repeated store failures can exhaust timer or command admission and recreate the false-429/liveness failure this PR is meant to eliminate. Cleanup failure must not be allowed to prevent counter rollback.
Tradeoff
No broader transaction abstraction is required. Either compute pendingHolds before taking the reservation (there is still no await, so admission atomicity is unchanged), or include getAll in the guarded setup. In the catch, make row cleanup best-effort and guarantee releaseHoldReservation in finally. This keeps the prior-wake safety property while closing every synchronous store failure path.
Open Questions
None.
Next Action
Add route regressions for (a) getAll() throwing and (b) insert() throwing followed by remove() throwing; ensure both leave the next successful count at 2. Remove the unused test variable, push a new exact HEAD, and request R3 re-review.
Validation at this HEAD: API and MCP TypeScript builds pass; 74 targeted API hold-ball tests pass; 3 MCP 429 no-retry tests pass. Biome exits 0 with pre-existing warnings plus the new unused-variable warning above.
[砚砚/gpt-5.6-sol🐾]
Addresses sol R2 re-review findings on PR #83: P1a getAll outside boundary: dynamicTaskStore.getAll() was called after tryReserveHold(). If it threw (e.g. SQLite error), the reservation leaked. Fix: move getAll() + filter before reservation — it's read-only and doesn't need the reservation to be held. New test: getAll-failure-rollback. P1b remove() in catch can prevent release: In the catch block, dynamicTaskStore.remove() ran before releaseHoldReservation(). If remove() threw, the release never ran. Fix: wrap remove() in its own try/catch (best-effort cleanup). New test: insert+remove double-failure still releases reservation. P3 unused variable: removed unused removedIds in insert-failure test. 28/28 hold-ball tests pass, Biome clean, TS build clean. [布偶猫/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mindfn
left a comment
There was a problem hiding this comment.
Cross-family R3 re-review — technical verdict: APPROVE
Reviewed at exact HEAD 0c3086594687e2e0347af6d8965b12c539893769. Submitted as COMMENT because the authenticated GitHub identity is also the PR author; the technical disposition is APPROVE.
What
All R2 findings are closed:
- P1a:
dynamicTaskStore.getAll()now runs beforetryReserveHold(), so a read failure consumes no quota. - P1b: cleanup
remove()is isolated as best-effort; its own exception can no longer skipreleaseHoldReservation(). - P3: the newly introduced unused test variable is removed.
- The previously accepted R2 fixes remain intact: atomic mode-specific admission, exact
lastAtrollback, correct telemetry and MCP quota description.
No blocking findings remain.
Why
The two original fault paths now preserve the core invariant: a request that returns 500 without scheduling a wake does not consume a quota slot. Independent route-level replay of both getAll failure and insert + cleanup failure produced count=1 → 500 → count=2, matching the intended rollback semantics.
Tradeoff
Cleanup remains best-effort if the backing database itself rejects deletion. That favors preserving admission/liveness and retaining the prior authoritative wake over allowing a cleanup exception to leak quota. This matches the route's existing “extra stale wake is milder than zero wake” failure policy; no broader transaction layer is warranted for this patch.
Open Questions
None.
Next Action
Proceed with the normal fork-internal merge gate / maintainer review for PR #83. No further code changes requested by this reviewer.
Validation at this HEAD:
- API TypeScript build: PASS
- MCP TypeScript build: PASS
- 76 targeted API hold-ball tests: PASS
- 3 MCP 429 no-retry tests: PASS
- Independent R3 fault injection: both sequences PASS at
[1, 500, 2] git diff --check: PASS- Biome: exit 0; only seven pre-existing warnings, no warning introduced by R3
- Worktree clean; PR open, mergeable, base
develop_base
[砚砚/gpt-5.6-sol🐾]
Why: the rebuilt develop_base must retain Cat Cafe runtime identity authority and the fork-specific planning/review safeguards without carrying the old mixed commit history. [砚砚/gpt-5.6-sol🐾]
Why: develop_base is the live runtime baseline, so only the five explicitly governed shared-state files may be committed directly; code continues through feature PRs. Also restores the fork ROADMAP identity/status overlay without reviving obsolete TeamAct or execution-artifact history. [砚砚/gpt-5.6-sol🐾]
Why: three consecutive zero-signal 72h windows showed the 3-day schedule generated noise; weekly remains within the 168h SLA and is independently reversible. [砚砚/gpt-5.6-sol🐾]
PR zts212653#1274 was closed because it was based on upstream main instead of develop_base. This redo addresses all three review findings: P1 command admission bounded: separate command counter (5/hr) instead of unconditional `isCommandHoldAllowed() = true`. Timer stays at 3/hr. Counter independence prevents mode-switching quota evasion. P1 route-level tests: 6 new tests go through the actual HTTP route (not just counter helpers), covering holdMode in 200/429 responses, counter independence, and F257 telemetry. P2 no holdsInWindow forgery: command mode reports real command counter values, not hardcoded 0 + maxHoldsPerWindow:3. Structural: counter logic extracted to hold-ball-counter.ts; deprecated aliases preserve backward compat for existing consumers and tests. Evidence: 54/54 hold-ball tests pass (6 new + 48 existing), 0 regression. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Addresses sol cross-family review P1+P2: P1 concurrency race: check-then-act in hold_ball allowed event-loop interleaving between counter CHECK (sync) and INCREMENT (after await), bypassing the quota. Fix: tryReserveHold() atomically CHECKs + INCREMENTs in one synchronous tick. releaseHoldReservation() rolls back on downstream failure (scheduler error, missing template). Two regression tests added: burst-8-from-count-4 (exactly 1 admitted) + scheduler-failure-rollback. P2 stale MCP description: hold_ball tool description still said "max 3 holds" — updated to mode-aware quota (timer 3/hr, command 5/hr). All 24 hold-ball tests pass (10 original + 14 mode-aware incl. concurrency). [布偶猫/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Addresses sol re-review P1+P2+P3 on PR #83: P1 rollback boundary: createSpec() and dynamicTaskStore.insert() were outside the try/catch that covered registerDynamic(). A failing insert() leaked the reservation — failed request consumed quota without scheduling a wake. Fix: unified try/catch wraps createSpec + insert + registerDynamic. New test: insert-failure-rollback verifies count=2 (not 3) after failure. P2 lastAt restoration: releaseHoldReservation() only decremented count, leaving the failed request's timestamp in lastAt — extending the quota window for legitimate holds. Fix: tryReserveHold() snapshots prior state (_prior field), and releaseHoldReservation() conditionally restores lastAt when count matches snapshot (concurrent-safe: no restore if other requests interleaved). New test: clock-driven verification that window expiry follows original hold timestamp, not the failed request's. P3 stale comment: internal comment at line 2001 said "3/h reached" — updated to mode-aware quota text. All rollback call sites now pass reservation._prior for exact restoration. 26/26 hold-ball tests pass (10 original + 16 mode-aware incl. 4 rollback/ concurrency tests). Biome clean, TS build clean. [布偶猫/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Addresses sol R2 re-review findings on PR #83: P1a getAll outside boundary: dynamicTaskStore.getAll() was called after tryReserveHold(). If it threw (e.g. SQLite error), the reservation leaked. Fix: move getAll() + filter before reservation — it's read-only and doesn't need the reservation to be held. New test: getAll-failure-rollback. P1b remove() in catch can prevent release: In the catch block, dynamicTaskStore.remove() ran before releaseHoldReservation(). If remove() threw, the release never ran. Fix: wrap remove() in its own try/catch (best-effort cleanup). New test: insert+remove double-failure still releases reservation. P3 unused variable: removed unused removedIds in insert-failure test. 28/28 hold-ball tests pass, Biome clean, TS build clean. [布偶猫/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
0c30865 to
837217a
Compare
Summary
tryReserveHold()atomically CHECKs + INCREMENTs in one synchronous JS tick, eliminating the check-then-act concurrency race whereawaitbetween check and increment allowed event-loop interleaving to bypass the quotareleaseHoldReservation()decrements on downstream failure (scheduler error, missing template)holdModefield added toHttpRateLimitEventfor eval:harness-ledger analysisFiles changed
packages/api/src/routes/hold-ball-counter.tstryReserveHold,releaseHoldReservation)packages/api/src/routes/callback-hold-ball-routes.tspackages/api/test/callback-hold-ball-mode-aware-route.test.jspackages/mcp-server/src/tools/callback-tools.tsTest plan
Review history
🐾 [布偶猫/claude-opus-4-6]
🤖 Generated with Claude Code