Skip to content

fix(ask): fence Ask settlement on the claim generation - #979

Open
seonghobae wants to merge 23 commits into
codex/ask-timeout-attribution-20260907from
feat/ask-ownership-fenced-liveness-20260907
Open

fix(ask): fence Ask settlement on the claim generation#979
seonghobae wants to merge 23 commits into
codex/ask-timeout-attribution-20260907from
feat/ask-ownership-fenced-liveness-20260907

Conversation

@seonghobae

@seonghobae seonghobae commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Ask jobs could lose an already completed answer when a claim renewal committed just before completion: cancelling the renewal discarded its new generation, so compare-and-set settlement updated no row. A simultaneous renewal failure could also be ignored. Drain in-flight renewal before admitting success/failure, reject unconfirmed ownership, and preserve native cancellation of both tasks.

This PR also replaces the elapsed compute deadline with claim-generation fencing, retains nullable explicit transport timeout, and prevents reclaimed owners from settling. It is stacked on #974 (def15fc691d4442c0d82103c1642147b1528d7be); merge that parent through protection before retargeting this child to main. Do not merge into the feature base.

Validation for exact source correction 06be667c01f930fefb41a5107bf21fceb81ba7e7 (current head 6662ea5df6f8f026b54a083677d3e0ee98d2d6d2, including the test-fixture repair):

  • Two new race regressions failed before the fix; 42 focused queue/lifecycle/configuration/docstring tests pass afterward, including cancellation during completion drain.
  • Real PostgreSQL execution passed both stale-owner settlement rejection and committed-renewal completion/persistence regressions. The exact production migrations passed idempotent replay; the synthetic database was removed. The new production-path regression exposed missing migrations 0212/0218 in the shared schema fixture; both are now applied and replayed. The normal two-second local connection admission skipped tests; the separately reported real execution used an authenticated 20-second connection preflight, the production migrations/functions, and synthetic visibility/model computation. This proves the database behavior, not authenticated HTTP/UI acceptance.
  • Content-safe synthetic k6 setup authenticated, then Ask submission timed out at the explicit 20-second observation limit. Zero workload iterations ran; concurrent workload acceptance, saturation attribution, deployment and desktop/mobile acceptance remain unverified.
  • No schema/API/release-number or UI changes in the correction. ADR 0371 remains Proposed; test(#830): validate exact #829 descendant convergence #980 owns ADR 0370, and feat: establish versioned UI translation ledger foundation #929 owns translation ADR 0362/migrations 0246–0247. Remote canonical ContextualWisdomLab/disksage spelling is corrected in the PRD register.

Fresh hosted checks are required on the final head; predecessor Tests GREEN does not transfer. Independent approval is absent. The inherited three-heartbeat reclaim ratio still lacks a validated failure-detector/capacity contract and remains an explicit acceptance gap. No model policy, local mathematical implementation, timeout heuristic or warning suppression is added by this correction. The gap baseline separates current implementation, authority, non-identifying runtime observations and historical snapshots.

Orphan recovery can flip a long-running job back to queued while
the original worker still holds the answer. Settlement now
compare-and-sets the claim's updated_at so the previous owner
cannot overwrite a reclaimed row.
@coderabbitai

coderabbitai Bot commented Sep 7, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 46a7d1dd-48c5-4a27-8bed-71328307611d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Codex added 2 commits September 7, 2026 21:45
Compare-and-set settlement now inspects PostgreSQL UPDATE 0 so a
reclaimed Ask job is left with the new owner instead of being
misread as a completed write.
Age-based orphan recovery could reclaim a job whose owner was still
computing. Renew updated_at on the recovery interval, abort when the
generation is lost, and keep the 600 s deadline until that heartbeat
is independently approved.

@seonghobae seonghobae left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 heartbeat wrapper가 compute_global_ask_answer()를 별도 asyncio.create_task()로 분리했는데, 바깥 process_global_ask_job() task가 shutdown/user cancellation으로 취소될 때 finally에서 beater만 취소하고 worker task는 취소·await하지 않습니다. 기존 direct-await 경계에서는 native cancellation이 계산 작업으로 전파됐지만 이 head에서는 child operation이 detached 상태로 계속 실행될 수 있습니다. 이는 #975의 native-cancellation/liveness 경계와도 충돌합니다. 현재 timeout_source="shutdown" 테스트는 child coroutine이 스스로 CancelledError를 던지는 경우만 다뤄서 이 regression을 잡지 못합니다. 외부에서 wrapper/process task를 cancel했을 때 operation이 실제로 cancel되고 detached task가 남지 않는 realistic RED를 먼저 추가한 뒤, wrapper finally에서 미완료 worker를 cancel+gather하는 최소 causal fix가 필요합니다. 동시에 PR 본문의 “does not ... add liveness heartbeats”는 current diff와 불일치하므로 live authority에 맞게 고쳐야 합니다.

@seonghobae
seonghobae marked this pull request as draft September 7, 2026 14:10
Owner-task cancellation left compute_global_ask_answer running as a
detached child of the claim-heartbeat wrapper. Cancel and await that
worker in finally, and remove the one-shot repair workflow whose RED
now lives in the product test suite.

@seonghobae seonghobae left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fresh exact-head repair review on 3dbba6b15733c6e517d70461446eb1049eba57bf: the queued one-shot run 34131105502 did not execute its RED or fix. It was admitted only after the branch had already advanced from workflow authority 3f0b63d... to this concurrent product commit, so its Verify authority before repair step correctly failed and all repair/test steps were skipped. I adopted the intervening delta instead of treating that failure as a product regression. The current source now cancels and gathers both the inner Ask worker and heartbeat in finally, which is the causal cleanup required by review 5132764499, and the temporary workflow is gone; however this commit has no exact-head backend GREEN because repository Tests are skipped while Draft. Keep the cancellation regression test and run current-head PostgreSQL-backed validation before any completion or merge claim.

@seonghobae
seonghobae marked this pull request as ready for review September 7, 2026 14:55
Omitted ORCHESTRATOR_ANSWER_TIMEOUT_SECONDS now means no LineageWeave
elapsed socket limit, matching post-chat's null transport default.
An explicit finite value still has to stay below the 600 s job
deadline, which remains until the heartbeat is independently approved.

@seonghobae seonghobae left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid current-head runtime finding: the new 30 s claim heartbeat/CAS fencing now distinguishes a live owner from an orphan, but process_global_ask_job() still wraps the entire model-backed computation in asyncio.timeout(JOB_DEADLINE_SECONDS) with JOB_DEADLINE_SECONDS = 600. That terminates a healthy reasoning/stream/tool-call operation solely because elapsed wall time reached 600 s, despite the live claim continuing to renew. This conflicts with the current execution contract: model timeout defaults null; user cancellation, provider completion/failure, admin policy and owner-liveness loss must remain distinct; elapsed time alone must not terminate a healthy model-backed operation. The orphan-recovery threshold is also still derived from that hard deadline even though heartbeat freshness is now the authoritative liveness signal.

Repair boundary: first add a realistic test in which the operation remains live across the former 600 s boundary while the claim heartbeat continues, and require no failed settlement/cancellation from elapsed time alone. Then remove the LineageWeave hard execution timeout from the active operation path and make orphan reclaim depend on stale heartbeat/claim generation rather than a guaranteed deadline. Keep explicit deployment/admin timeout policy separate if one is introduced; do not copy provider timeout policy or invent a default. Preserve native cancellation, CAS settlement, bounded failure detail and recovery after an actually dead worker.

@seonghobae seonghobae left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current-head ADR/operability finding: this branch now owns a material runtime decision—claim-generation CAS settlement, periodic liveness renewal, stale-owner reclaim semantics, native cancellation, and removal of elapsed-time-only execution termination—but its changed-file set contains no ADR. AGENTS.md makes ADRs normative and requires a decision record before architectural/runtime policy changes; issue #975 also requires the operability/recovery decision record with alternatives, rejected timer-only recovery, rollback, and exact evidence. Keep the existing elapsed-deadline RED as the behavior gate, then in the same causal repair allocate an unclaimed Proposed ADR for the ownership-fenced liveness invariant and make the operability/gap docs code-current. Do not mark the ADR Accepted before protected-main acceptance. No new provider/model policy belongs in that ADR.

@seonghobae seonghobae left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acceptance-gap finding on the same exact head: #975 explicitly requires the ownership/recovery invariant against real PostgreSQL claim/settlement transactions and says not to rely on synthetic sleep-only assertions. The changed test surface here still uses in-memory/mock pool connections; tests/test_global_ask_elapsed_deadline.py is useful RED for elapsed cancellation, but it cannot prove the SQL CAS/heartbeat/reclaim invariant. Add a PostgreSQL-backed regression using the repository's real 0165_global_ask_job.sql schema (the normal Tests job already provisions PostgreSQL): Worker A must hold/renew one running claim generation, recovery must not reclaim while that heartbeat is current; when liveness actually becomes stale/reclaimed, the prior owner must fail CAS settlement and the queued successor remains authoritative. Keep Valkey as a wake-up stub if needed—the database row is already the documented source of truth. This test belongs in the same causal repair before #975 can be treated as verified.

Codex added 2 commits September 8, 2026 01:47
The 600 s asyncio timeout still cancelled a renewing Ask owner.
Compute now runs under claim-generation liveness only. ADR 0370
records that Proposed policy. Orphan recovery uses three missed
heartbeats. Provider TimeoutError stays unavailable, not a worker
deadline. PostgreSQL-backed race evidence remains a follow-up.
Issue #975 requires the reclaim invariant against actual claim and
settlement transactions, not only in-memory fakes. After 0001 and
0165 replay, the previous owner's compare-and-set is UPDATE 0 and
the new owner settles.

@seonghobae seonghobae left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current-head repair finding on fbe68078a325d760e82105b7175492176d29453d: the elapsed compute deadline has been removed from process_global_ask_job(), and the new real-PostgreSQL CAS regression is a valid intervening delta to adopt. However backend/app/config.py still carries GLOBAL_ASK_JOB_DEADLINE_SECONDS = 600 and rejects an explicit finite ORCHESTRATOR_ANSWER_TIMEOUT_SECONDS >= 600 solely because it must be below that removed worker deadline. global_ask_queue.py also keeps the dead deadline import/alias, and the orphan-recovery docstring still says a per-job deadline guarantees settlement. This preserves deleted timeout policy in configuration and documentation and contradicts this PR's stated separation of nullable transport timeout from heartbeat/claim liveness. Realistic RED: an explicit finite positive transport timeout such as 900 s must be accepted after the worker deadline is removed, while omitted/blank remains null and zero/negative/non-finite values remain rejected. Minimal repair: validate only finite-positive explicit transport timeout, remove the dead worker-deadline constant/import/alias and stale recovery wording, and keep heartbeat-grace orphan recovery/native cancellation/CAS settlement unchanged. Do not introduce provider policy or a new default.

@seonghobae
seonghobae marked this pull request as draft September 7, 2026 18:35
@seonghobae
seonghobae marked this pull request as ready for review September 7, 2026 18:38
Collection on aecb873 imported GLOBAL_ASK_JOB_DEADLINE_SECONDS after
the constant was removed. load_settings now accepts an explicit 900 s
transport timeout and rejects non-positive values. ADR 0370 Proposed
no longer bounds that timeout below 600 s.

Copy link
Copy Markdown
Contributor Author

RCA on exact head aecb873a Tests run 34152472692: collection aborted with ImportError: cannot import name 'GLOBAL_ASK_JOB_DEADLINE_SECONDS' from 'backend.app.config'. Frontend on that run stayed GREEN.

Causal leftover: backend/tests/test_config.py still imported the removed worker-deadline constant and required an explicit timeout below 600 s. Repair on 29d226529 drops that import, accepts an explicit 900 s transport timeout through load_settings(), rejects zero/negative/non-finite values, and updates ADR 0370 Proposed so an explicit timeout is no longer bounded by the removed deadline.

Local evidence (not a CI transfer): 36 targeted tests passed; 266 tests collected without ImportError; live PostgreSQL test_postgres_stale_ask_owner_cannot_settle_after_reclaim passed. Keep #979 unmerged until repository Tests are terminal GREEN on this exact head, parent #974 remains valid, hosted security checks are acceptable, and independent APPROVE is present. No self-approval.

Tests run 34153018910 failed the public-docstring gate because the
nested Ask heartbeat coroutine was named beat. Rename it _beat so the
helper is not a public production definition.

Copy link
Copy Markdown
Contributor Author

RCA on exact head 29d226529 Tests run 34153018910: collection succeeded, then tests/test_public_docstrings.py failed because the nested Ask heartbeat coroutine at backend/app/global_ask_queue.py:141 was named beat. Frontend on that run stayed GREEN.

Repair on 262d496a9 renames that nested helper to _beat so it is not a public production definition. Local: 38 tests green including the docstring gate, elapsed-deadline, cancellation, transport timeout, queue, and settings contracts. Keep #979 unmerged until repository Tests are terminal GREEN on this exact head, parent #974 remains valid, hosted security checks are acceptable, and independent APPROVE is present. No self-approval.

Copy link
Copy Markdown
Contributor Author

Exact-head 262d496a9 repository Tests run 34156704752 is terminal GREEN (Full test suite 22m50s, Frontend GREEN). Merge state is CLEAN.

Prior review findings on this stack are closed on this head: native cancellation of the inner worker, claim-generation CAS settlement, live heartbeat vs elapsed-time cancellation, real PostgreSQL reclaim (test_postgres_stale_ask_owner_cannot_settle_after_reclaim), nullable transport timeout including explicit 900 s, and the public-docstring gate (_beat).

Parent #974 (def15fc69) Tests/Frontend are GREEN and remain BLOCKED on independent APPROVE plus org review gates (Noema, OpenCode, Strix, CodeQL-compat). This PR has no independent APPROVE. CodeRabbit/Devin skips are not APPROVE. No self-approval and no merge.

Next unproven liveness edge, not changed on this GREEN head: if the heartbeat task ends while compute is still running without lost being set, the owner would continue without renewals. Keep that as a follow-up RED rather than invalidating this exact-head GREEN.

A heartbeat exception left compute running without renewals until the
test timed out. Treat a finished heartbeat with a live worker as a lost
claim so the owner is cancelled instead of settling.

Copy link
Copy Markdown
Contributor Author

Follow-up on GREEN 262d496a9: a heartbeat exception left compute running without renewals. RED test_dead_heartbeat_aborts_live_ask_operation timed out (TimeoutError) waiting for settlement. Repair 875e9364a treats a finished heartbeat with a live worker as a lost claim and cancels the owner. Local: 21 tests green (cancellation, elapsed-deadline, docstring, queue). Repository Tests on this new head are not polled. Keep unmerged until Tests are terminal GREEN here, parent #974 remains valid, and independent APPROVE is present. No self-approval.

Leftover-map validation #980 already allocated ADR 0370 for comparison
axis-singular. Keep that number on the leftover stack and record Ask
claim-generation liveness as Proposed ADR 0371.

Copy link
Copy Markdown
Contributor Author

Exact-head 875e9364a Tests run 34164205806 was terminal GREEN (22m51s) + Frontend GREEN. No independent APPROVE; no merge.

Repair finding vs leftover validation #980: that lane already allocated docs/adr/0370-leftover-map-compare-axis-singular.md. This stack had independently used ADR 0370 for Ask claim liveness. Commit fef48b14f reallocates Ask claim-generation liveness to Proposed ADR 0371 so the leftover number stays on #980/#830. Repository Tests on fef48b14f are not polled. Keep unmerged until Tests GREEN on this new head, parent #974 remains valid, and independent APPROVE is present.

Copy link
Copy Markdown
Contributor Author

Exact-head fef48b14f Tests run 34167781764 is terminal GREEN (22m52s) + Frontend GREEN. Merge CLEAN. No independent APPROVE; no merge. Parent #974 remains Tests GREEN and BLOCKED on REVIEW_REQUIRED.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

OpenCode reviewed the current-head product diff. Coverage is a separate gate.

Changed files

  • backend/app/config.py — API and service runtime
  • backend/app/global_ask_queue.py — API and service runtime
  • backend/app/main.py — API and service runtime
  • backend/tests/test_config.py — regression suite
  • docs/adr/0371-ask-claim-generation-liveness.md — operator or user guidance
  • docs/product-technical-gap-baseline.md — operator or user guidance
  • tests/test_global_ask_claim_cancellation.py — regression suite
  • tests/test_global_ask_elapsed_deadline.py — regression suite
  • tests/test_global_ask_queue.py — regression suite
  • tests/test_global_ask_transport_timeout.py — regression suite
  • tests/test_schema.py — regression suite

Changed behavior

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Backend: config.py (3 files)"]
  S1 --> I1["API and service runtime"]
  I1 --> R1["Review risk: Backend: config.py (3 files)"]
  R1 --> V1["backend tests"]
  Evidence --> S2["Test: test_config.py"]
  S2 --> I2["regression suite"]
  I2 --> R2["Review risk: Test: test_config.py"]
  R2 --> V2["targeted test run"]
  Evidence --> S3["Docs: 0371-ask-claim-generation-liveness.md (2 files)"]
  S3 --> I3["operator or user guidance"]
  I3 --> R3["Review risk: Docs: 0371-ask-claim-generation-liveness.md (2 files)"]
  R3 --> V3["docs review"]
  Evidence --> S4["Test: test_global_ask_claim_cancellation.py (5 files)"]
  S4 --> I4["regression suite"]
  I4 --> R4["Review risk: Test: test_global_ask_claim_cancellation.py (5 files)"]
  R4 --> V4["targeted test run"]
Loading

Findings

No source-backed product finding is synthesized from the coverage gate. A coverage miss belongs in the status comment.

  • Head SHA: fef48b14f302dd40e1fa83096810cc880a5c4c66
  • Workflow run: 34173501051
  • Workflow attempt: 1
  • Coverage gate: failure

Review outcome

Coverage is a gate, not the review. This body reviews the changed product files.

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Backend: config.py (3 files)"]
  S1 --> I1["API and service runtime"]
  I1 --> R1["Review risk: Backend: config.py (3 files)"]
  R1 --> V1["backend tests"]
  Evidence --> S2["Test: test_config.py"]
  S2 --> I2["regression suite"]
  I2 --> R2["Review risk: Test: test_config.py"]
  R2 --> V2["targeted test run"]
  Evidence --> S3["Docs: 0371-ask-claim-generation-liveness.md (2 files)"]
  S3 --> I3["operator or user guidance"]
  I3 --> R3["Review risk: Docs: 0371-ask-claim-generation-liveness.md (2 files)"]
  R3 --> V3["docs review"]
  Evidence --> S4["Test: test_global_ask_claim_cancellation.py (5 files)"]
  S4 --> I4["regression suite"]
  I4 --> R4["Review risk: Test: test_global_ask_claim_cancellation.py (5 files)"]
  R4 --> V4["targeted test run"]
Loading

@opencode-agent

opencode-agent Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

OpenCode Review Overview

Coverage evidence did not pass, so approval is blocked. The formal pull-request review is the source-backed diff review, not this status comment.

Copy link
Copy Markdown
Contributor Author

Still exact-head fef48b14f Tests GREEN (run 34167781764). No independent APPROVE; no merge. Parent #974 remains Tests GREEN and BLOCKED on REVIEW_REQUIRED.

@seonghobae seonghobae added bug Something isn't working priority: medium labels Sep 8, 2026 — with ChatGPT Codex Connector

Copy link
Copy Markdown
Contributor Author

Head advanced concurrently to 6662ea5df. Tests run 34182446558 is terminal GREEN (23m48s) + Frontend GREEN. Still no independent APPROVE; no merge.

@seonghobae seonghobae left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent current-head review of 6662ea5df6f8f026b54a083677d3e0ee98d2d6d2 (not APPROVE; same-author reviews do not satisfy the protected-branch independent-approval rule).

Prior product findings on this stack are addressed in the current source:

  • owner cancellation cancels and gathers the inner compute task and heartbeat (tests/test_global_ask_claim_cancellation.py)
  • live compute is not terminated by elapsed wall time; JOB_DEADLINE_SECONDS / GLOBAL_ASK_JOB_DEADLINE_SECONDS are gone
  • omitted/blank Ask socket timeout stays null; explicit 900 s is accepted (backend/tests/test_config.py)
  • settlement is compare-and-set on claim updated_at; stale-owner PostgreSQL proof is in tests/test_schema.py
  • inflight renewal is drained before settlement; a failed renewal rejects the simultaneous answer
  • Proposed ADR 0371 is present and not marked Accepted

Hosted evidence on this exact head: Frontend and Full test suite GREEN (34182446558). This is stacked on #974 (codex/ask-timeout-attribution-20260907 @ def15fc69). Do not merge to main from this child. #974 Tests/Frontend are GREEN; remaining #974 failures are noema-review, opencode-review, strix (6h), and CodeQL compatibility analysis — not the product suite. Parent-first protected merge, then retarget.

Unresolved (not treated as current-head product defects in this delta): the inherited three-heartbeat orphan ratio is still uncalibrated, as ADR 0371 already states. No self-approval, force-push, or bypass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working priority: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant