fix(negotiations): the continuation claim and expiry stop refusing drift - #1475
Merged
Conversation
The park-time drift fence #1474 removed from the settle survived in two more places, and tonight's live verification hit the first: the answer settled, the resume enqueued, and the claim refused with admission:"invalid" because the world had moved since the park. validateMaterialBinding (claim, heartbeat, completion) now splits the same way the settle did: the coherence joins, settlement match, and the completion path's terminal-effect assertion stay hard; the park-time triple (intentFingerprint / opportunityStatus / opportunityUpdatedAt) becomes log-only behind a resumable-status gate on current reality. expireInflightQuestion keeps every binding-vs-input equality check and drops its resolveNegotiationAdmission equality gate — expiry is a cleanup act and must land even when the opportunity went terminal or the signal was edited; the admission read remains for the drift log. RESUMABLE_OPPORTUNITY_STATUSES hoists to the atomic module so both the settle (#1474) and the claim gate on the same set; the protocol type import that hoist would have tripped in lint becomes the same local structural mirror the questioner adapter already carries. Claude-Session: https://claude.ai/code/session_01N1FQ3gV1oWWXQHMGGroqcV
…piry fences The incident inverted (drift then settle then claim mints the successor), the settle-to-claim race (terminal current status still invalid), the completion path's terminal-effect assertion, the coherence pins (settlement mismatch, gone membership, dead counterparty binding), and expiry under drift, on a terminal opportunity, after an answer, and against a mismatched binding. Claude-Session: https://claude.ai/code/session_01N1FQ3gV1oWWXQHMGGroqcV
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The design law
The flow that was still broken
Tonight's live verification of #1474 (sandbox): the recipient's answer to zombie park
aefff73b-d61f-4864-bfd8-859a8758ddcfsettled exactly as designed —negotiation_answer_settled_despite_driftlogged, task canceledask_user_answered, settlementcontinuationStatus:'requested', consumptionresumed:1, recorded:0, skipped:0, resume job enqueued. Then the flow broke ONE HOP downstream:RunExistingJob: Exact negotiation continuation skipped {admission:"invalid"}. No successor turn ran; opportunity78a6b283sits atnegotiating, untouched; the answer is durable but the negotiation never resumed.The park-time drift fence #1474 removed from the settle was standing in two more places, and both still refused drift. This lane removes the last two copies:
validateMaterialBinding(shared by claim, heartbeat, and completion) — after the coherence joins it returned null when the current intent fingerprint, opportunity status, or opportunityUpdatedAt differed from the PARK-TIME values the settlement carries. Now split the same way fix(negotiations): an answer beats staleness — drift is logged, never fatal #1474 split the settle: every coherence join, the settlement match, and the completion path'sterminalOpportunityStatuseffect assertion stay hard; when no terminal status is asserted, the gate is current reality only — the current status must be in the resumable set (latent/draft/pending/negotiating/stalled, the sameRESUMABLE_OPPORTUNITY_STATUSESthe settle uses, now hoisted to the atomic module) — and within it, drift is logged (negotiation_continuation_claimed_despite_drift) and the claim proceeds. A side effect stated in the code: heartbeats re-run this validation mid-execution while the turn itself may legitimately touch the opportunity — with drift log-only, a heartbeat can no longer kill its own execution.expireInflightQuestion— theresolveNegotiationAdmissionequality triple meant a drifted park could never even EXPIRE; the 24h sweep was fenced out by the same staleness the law says must not matter (the second zombie class). Every binding-vs-input equality check (coherence — the caller must pass what the binding says) stays; the admission read remains only to feed the drift log (negotiation_expiry_settled_despite_drift), and expiry lands even when the opportunity went terminal or the signal was edited. The timeout settlement it writes is alsocontinuationStatus:'requested'and resumes through the same claim, so the fence split serves both the answer path and the timeout path.The claim keeps its existing
claimed/busy/completed/invalidvocabulary —invalidsimply stops being reachable via park-time drift. No protocol files changed.Test evidence
New DB spec
services/api/src/adapters/tests/negotiation-continuation.atomic.spec.ts(10 tests): the incident inverted (signal edited + opportunity moved → settlesettled→ claimclaimed, successor minted, consultation carries the answer), terminal current status stillinvalid(settle→claim race), completion still asserts its exact terminal effect (wrong current status throws, correct one completes), coherence pins (settlement mismatch / gone membership / dead counterparty binding →invalid), and expiry: drifted park expires and its timeout settlement claims through, terminal opportunity still expires but the resume refuses, an answered consult is not rewritten, a mismatched binding still returns null.NODE_ENV=test bun test src/adapters/tests/negotiation-continuation.atomic.spec.ts src/adapters/tests/negotiation-dm-answer.settlement.spec.ts src/adapters/tests/negotiation-question-routing.static.spec.ts src/adapters/tests/question-retirement.static.spec.ts src/queues/tests/timeout.queue.spec.ts src/adapters/tests/negotiation-stalled-retry.database.spec.ts→ 80 pass, 0 failNODE_ENV=test API_TEST_ISOLATED_TARGET=src/queues/tests/run-existing.queue.isolated.ts bun test src/lib/testing/isolated-test-import-harness.spec.ts→ 16 pass, 0 failbun run typecheckclean; eslint clean on all touched files (fixing en route the atomic module's pre-existing restricted protocol import, which the pre-commit hook now enforces — it becomes the same local structural mirror the questioner adapter already carries).Pre-existing failures (baselined per the #1462 method —
git diff origin/devover these specs and their scanned sources is empty; not chased):opportunity.service.rediscovery,opportunity.service.maintenance,tool.controller,archive-legacy-negotiations.src/adapters/testsrun: 2 failures inconversation.database.adapter.spec.ts, 1 innegotiation-scope.static.spec.ts, 4 inclicredential.adapter.spec.ts— all reproduce in isolation with zero diff vs origin/dev.Post-merge verification
The root session re-drives the real park
aefff73b-d61f-4864-bfd8-859a8758ddcfafter merge: its settlement is alreadyrequested, so redelivery reportsalready_settled→ re-enqueue → the claim must now go through and run the successor turn on opportunity78a6b283-78e3-4b53-b3f1-aed142db1966.🤖 Generated with Claude Code
https://claude.ai/code/session_01N1FQ3gV1oWWXQHMGGroqcV