Repository navigation
[Fix] Agent tool loops silently exhaust inference credits - #3658
Conversation
|
Both prior findings are addressed; no new code issues found in the latest update. See session Reviewed OAuth environment stripping, isolated auth-store startup/cleanup, the synthetic ChatGPT/Copilot transport regression, judgment-router fixtures, and the limits documentation. Current-head CI: lint, type checks, knip, docs, upgrade compatibility, and worker build passed; Test, app build, and JavaScript/TypeScript analysis are still running. The real-binary subscription transport tests are opt-in and were not run during this review.
Reviewed 04e3ac6 |
|
Fixed both findings in 04e3ac6c:
Validation: full SDK suite 2,670 tests; full cloud-agents suite 2,646 tests serially; 259 focused helper/decision checks, including real OpenCode ChatGPT/Copilot transport regressions. Both transport regressions fail with protection disabled and pass restored, using synthetic auth and a local denying gateway. Lint, types, knip, judgment and push hooks pass. The local parallel suite's separate global-compute fixture race is documented in the PR. Current-head CI is settled: Test passes, Roomote code review passes with no new findings, both original threads are resolved, and all 29 reported checks are completed without a failing/pending result. The platform promoted the PR after the clean review; metadata refresh preserved that state. No merge performed. No human-input blocker remains. |
Related issue
Fixes #3585
Why this PR exists
Active review/judge children could repeat the same empty search indefinitely while silence watchdogs treated every iteration as progress. Post-response usage accounting could report the bill but could not stop the next provider request. This adds execution bounds and a durable admission boundary rather than relying on another model response to notice the loop.
What changed
Bound repeated tool activity across subagent types. The worker and control-plane OpenCode prompt runner share a pass guard. Five consecutive unique completed calls with the same tool name, recursively key-sorted arguments, and identical result/error stop execution; empty results count. Duplicate/out-of-order updates for the same invocation ID do not increase counts. Changed arguments or results reset the streak. The exact read-only task-update polling action gets 20 identical completions; shell/search tools do not receive a polling exemption. Safety diagnostics retain hashes/counts, not raw arguments/results.
Make pass limits independent of progress and siblings. Live child passes have defaults of 100 unique assistant-message steps, 200 terminal tool invocations, and 20 minutes. Pending task spawns are time-bounded before their child ID arrives. Activity and starting another child do not extend the older pass's deadline. The main task also has repetition/tool-count protection; control-plane prompts cannot disable the 20-minute maximum with
timeoutMs: null. Late duplicate terminal events do not re-arm settled passes.Reserve task inference allowance before dispatch. Four additive task columns persist the admitted request count, request limit (default 1,000), and stop latch/reason. PostgreSQL row locking serializes concurrent admissions across providers, children, retries and resumed runs. Every allowed gateway POST reserves before upstream fetch, including Jevgrep; GET metadata reads do not consume allowance. Failed attempts are not refunded, and database admission failures do not dispatch upstream. Missing usage/price data cannot bypass the counter. Task-associated control-plane OpenCode helpers use dedicated gateway-backed leases so their internal steps/retries share the same counter; task-associated typed decisions reserve before direct dispatch too.
Gateway-bound helper environments strip direct subscription OAuth at the final environment boundary. Managed startup overrides the caller's data home with a private, empty auth store and cleans it up on process close or startup failure. Shared auth files used by ordinary sessions remain untouched. This prevents built-in ChatGPT/Copilot OAuth loaders from replacing the SDK transport and bypassing the gateway URL or stop latch.
Stop visibly without inference. A breach stops the entire task, clears local retry/fallback queues, attempts bounded cancellation of root/recursive children, releases the worker's parent wait with a terminal event, and refuses late local continuation. The durable stop transaction writes task and owning-session history notices with the reason, available pass counters/elapsed time, and admitted-request count. Dedicated notice events survive task history reload without colliding with normal assistant events. Parent settlement/error relays do not wake inference to explain the stop or offer a replacement pass. New runs, restarts, resumes and provider fallback cannot reset the task latch/counter.
Public task/cost documentation, an operator/developer guarantee-and-limits note (
packages/db/agent-safety.md), the generated migration, and a patch changeset are included.Impact and exact limits
How it was tested
Initial targeted validation: 535 checks passed across 14 test files:
pnpm lint:fast,pnpm check-types:fast(27 packages),pnpm knip, the real pre-push hooks,mise exec -- pnpm --filter @roomote/docs check, andpnpm exec changeset statuspass. Generated migration applied successfully to the local development database; the test schema was applied to the real test database.Review follow-up validation (
04e3ac6c): all 2,670 SDK tests pass, including the three formerly failing judgment-router cases. The fixtures now model the persisted task lookup, assert task-ID forwarding, and verify missing bindings never dispatch judgments; the production binding guard was retained. All 2,646 cloud-agents tests pass serially (three opt-in tests skipped). The 259 focused helper/decision checks also pass, including two explicitly enabled real OpenCode transport checks for ChatGPT and Copilot.The transport checks use synthetic inline/persisted OAuth and a local denying gateway, without paid provider inference. They assert the real OpenCode request uses the task token and receives the non-retryable safety denial. Both fail with the protection temporarily disabled and pass with it restored. Environment/store tests cover ambient and explicit OAuth stripping, preservation of ordinary-session auth, and private-store cleanup on success/startup failure. Browser proof is not applicable to this backend-only follow-up; screenshots cannot establish the provider transport.
The local parallel cloud suite encountered a cross-file database fixture race:
enqueue-task.test.tstemporarily excludes all compute providers whilefast-agent-pinned-launch-automation-thread.test.tsresolves a default provider. The isolated failing file and the full serial suite pass; both involved fixtures and compute-selection production code are unchanged by this PR. No unrelated compute behavior was changed.The initial optional repository judgment hook reported incomplete coverage at its three-second/file-evidence budgets; the follow-up commit's hook passes (11 judgments). Broad lint/types/knip and real push hooks pass. Full-repository test completion is left to CI; no paid-provider soak or hard monetary enforcement is claimed.
Checklist
[Fix],[Feat],[Improve],[Refactor],[Docs], or[Chore]followed by a user-facing descriptionpnpm lintandpnpm check-typespass locallypnpm changesetThe fix is cohesive but cross-layer, so it is not a small diff; most added lines are the generated Drizzle snapshot. Static validation used the repository-recommended
:fastequivalents rather than the differently named full commands above.