A kind-word decision records the inputs it considered, and a reply whose inputs moved is not accepted - #932
Merged
Conversation
…ile the model answers is asked about again Signed-off-by: dada-yan <BinjunYann@gmail.com>
Signed-off-by: dada-yan <BinjunYann@gmail.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Wayland Yang <wayland0916@gmail.com>
Contributor
|
Merged current dev and set CURRENT_SCHEMA_VERSION to 80 as a maintainer edit: #938 landed first with 0091, so with 0092 the count is 80. Nothing else touched. |
WaylandYang
approved these changes
Sep 26, 2026
WaylandYang
left a comment
Contributor
There was a problem hiding this comment.
The kind-word half of 0053 done the way the phrase half was: one REPEATABLE READ snapshot before the model call, a basis of candidate ids, updated_at and ancestor closure, acceptance that locks the candidate classes FOR SHARE in the same order a delete takes and recomputes the basis, and the bounded re-ask for words the batch could not settle. The reproduction from #795 fails on dev and passes here; the lock-order and deleted-candidate tests are the ones that would have caught the two easy mistakes. Thank you; merging.
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.
Closes #795. The phrase half was #878 (ADR 0053); this is the kind-word half, scoped to
type_alignment.rsandtype_bindingsas discussed on the issue.What was wrong
type_bindings::stalecompared timestamps: a bound kind word went stale when its class was updated afterdecided_at, a negative one when any class was.decided_atis written withnow()after the model answers, so a class edit committed while the model was answering is older than the decision: both votes read the old definition and the result is kept as current. A parent edge does not moveupdated_atat all.What this changes
align_typesreads the classes, their versions and parent edges, the kind-word signatures and the existing decisions in oneREPEATABLE READ, READ ONLYtransaction, and commits it before the first model call.type_bindings.basis(migration 0092) stores, for an agent decision, a fingerprint of the candidate classes the model was shown: each one's id,updated_atand ancestor closure (ClassSnapshot::basis). Candidates are retrieved once per run for every live kind word, and a word is decided again when its stored basis differs from the current one. Agent rows without a basis are decided again once; a person's rows carry none and are never re-evaluated.decide_and_apply_if_currentlocks the candidate class rowsFOR SHAREbefore writing the binding row, which is the order a class delete takes before it cascades to the binding. It then recomputes the basis from the rows as they are now and discards the reply if it moved; the run queues another. Updates and deletes of the candidates wait for that transaction. A parent edge or a new class committed in the same window is not blocked, and leaves the decision detectably stale for the next run.devit requeues with no counter: a variant of the new test, run for three rounds against unmodifieddevwith a reply that never parses, asked the model six times and queued an immediatealign_typesjob withoutreaskafter every round, so an endpoint that always fails keeps the job going indefinitely.decide_and_apply_humanenqueues itsalign_phrasesjob in the same transaction as the decision and its projection (jobs::enqueue_unless_queued_tx), instead of after the commit. The route's response is unchanged.A failed retrieval still falls back to the whole class list, as before and as the phrase shortlist does; it is now logged instead of swallowed. Words decided during the failure carry that list in their basis and are asked once more when retrieval recovers. Not falling back would leave a small base untyped for as long as its embedding endpoint is broken.
ADR 0053 gets a dated revision, and
docs/design/ontology.mdno longer says kind-word bindings useupdated_at.Migration number: 0091 is held for #901, so this takes 0092 and sets the CLI's
CURRENT_SCHEMA_VERSIONto 79. I'll renumber if you'd rather it had another.Cost
Retrieval now runs for every live kind word on every run, not only for the words being decided: one embedding request per 64 words and one nearest-class query per word. On a base the size of the typed-graph bench (about 400 kind words) that is 7 embedding requests and 400 queries per run, and extraction queues a run after every document (documents that finish while one is already queued share it). The phrase shortlist likewise embeds its wide signatures (those with more structural candidates than it shows) on every run. If this is too much, a cheap pre-check can skip retrieval when the class layer has not changed since the stored decisions; I left it out to keep this PR to the design agreed on the issue.
Existing agent decisions have no basis and are decided again once after the migration, as #878 did for phrases.
Tests
an_edit_during_the_model_request_is_asked_againis the reproduction from the issue, written in the existing kind-word test fixture: the first request is held, the class definition is edited, and both votes carryOLD definition. Ondevit fails:Here the reply is not accepted, the next run asks with
NEW definition, and a third run asks nothing.Also new:
updated_atunchanged;Moved,WrittenandKeptPerson, and for the human decision committing with its job;Moved);Negative controls for the lock, run on a scratch copy and not committed: taking the class lock after the binding write makes the lock-order test fail with
deadlock detected; dropping the lock makes the deleted-candidate test accept the reply (Writteninstead ofMoved).The existing kind-word tests pass unchanged, with one added assertion.
Run on Linux against
pgvector/pgvector:pg16with Rust 1.98.1:Workspace: 1,144 passed, 0 failed, 5 ignored. The five are the existing opt-in tests that need network access or a dedicated idle database. That includes
utopia-server(427 passed),utopia-store(71 unit, 265 store) and the CLI'sschema_version_policy_compares_against_currentwith the constant at 79.All model calls in these tests go to a scripted local endpoint; no real model was called.
Not in this PR
updated_at, parent edges are in the closure, and the vector refresh changes neither, so no semantic change escapes the basis. The guard would only turn "stale at the next run" into "rejected now" for an edit landing between the acceptance check and its commit.phrase_alignment.rs, it has the same shape at the end of a run: a stale signature whose batch failed still counts as changed and requeues immediately. It is not changed or tested here.