Repository navigation
feat(omnigraph): negotiate the HTTP contract per server - #580
Merged
blarghmatey merged 5 commits intoNov 4, 2026
Merged
blarghmatey merged 5 commits into
blarghmatey merged 5 commits into
Conversation
witan-core pinned Omnigraph-Http-Api to 0.14, and a 0.13 server and a 0.14 client refuse each other. The omnigraph and witan stacks deploy from separate pipelines in no fixed order, so whichever deployed second left a window where witan couldn't reach its graph. PooledTransport now probes HEAD /healthz on first use and sends the server's stamp when it is in SUPPORTED_HTTP_API_CONTRACTS (0.14, 0.13). On a 400 api_contract_mismatch stamped with a supported contract it switches and resends once, writes included, because both servers refuse a wrong contract before graph resolution. That covers a data tier that changes contract under a live process. RESPONSE_CONTRACT_REQUIRED goes: both supported servers stamp every response. The served suites now run once per configured server, and CI installs the 0.13.0 server beside the pinned one so they cover both contracts. Spike: tk-spike-let-witan-core-negotiate-the-omnigraph-htt-3c94fb Task: tk-witan-core-negotiate-the-omnigraph-http-contract-e29072 Claude-Session: https://claude.ai/code/session_01FGZebHdoMkhaQ1ewuGV2Ni
…tion The probe ran under the contract lock with the full 120s request timeout, so a server that accepted connections and never answered queued every thread behind it, one probe at a time. It now runs outside the lock with a 5s timeout. A mismatch is answered without reading the request body, so the resend goes out on a fresh connection, where a failure is at connect and provably pre-send. Run against 0.13.0 and 0.14.0, a 6 MB load sent with the wrong contract fails with a broken pipe at send. The resend also carries the stamp the refusing server returned, not whatever the transport holds by then. check-omnigraph-format now asserts the binary's http-contract matches HTTP_API_CONTRACT. The new comment said it did, but nothing checked it, so a release that moved the contract without the storage format would have passed. The served group-commit tests probe a fresh transport for their raw commit count, since the cached one may be from an earlier test's server on a reused port. Claude-Session: https://claude.ai/code/session_01FGZebHdoMkhaQ1ewuGV2Ni
A load sent with the wrong contract is cut at send, before the server's stamped refusal: a 6 MB load against 0.13.0 and 0.14.0 failed with a broken pipe. So a long-lived process that cached 0.13 before the data tier moved to 0.14 would fail every large load until some small call learned the new contract from its 400. load_ndjson now probes HEAD /healthz first and adopts a changed stamp. A failed probe keeps the cached contract. Claude-Session: https://claude.ai/code/session_01FGZebHdoMkhaQ1ewuGV2Ni
1 of 4 tasks
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
The refresh can overwrite a newer contract with a stale probe result and reuse a connection to the replaced server, leaving large uploads vulnerable to broken-pipe failures.
2 open issues
What changed in this PR
This PR lets witan-core negotiate Omnigraph HTTP contracts per server, allowing the witan and data tiers to deploy in either order during the 0.13-to-0.14 cutover.
Changes:
- Adds contract discovery, a single resend after a supported mismatch, and re-probing before bulk loads.
- Runs served tests against both supported releases and checks the pinned binary’s HTTP contract.
- Updates compatibility notes and rollout documentation.
| File | Description |
|---|---|
testsupport/omnigraph_server.py |
Parameterizes served tests across configured binaries. |
packages/witan-core/witan_core/omnigraph_install.py |
Updates deployment-order comments. |
packages/witan-core/witan_core/omnigraph_http.py |
Implements per-server contract negotiation and resends. |
packages/witan-core/tests/test_omnigraph_http.py |
Tests discovery, resends, refreshes, and shared contract state. |
packages/witan-core/CHANGELOG.md |
Documents negotiation behavior. |
mcp/servers/witan/tests/test_group_commit_served.py |
Discovers the contract for commit-count requests. |
mcp/servers/witan/tests/conftest.py |
Runs served fixtures against each configured server. |
mcp/servers/witan/pyproject.toml |
Updates compatibility-floor comments only. |
mcp/servers/witan/changelog.d/20261029_125338_blarghmatey_omnigraph_0_14_pin.md |
Clarifies negotiated contract support. |
mcp/servers/witan-code/tests/test_branches_served.py |
Parameterizes branch tests across server releases. |
mcp/servers/witan-code/pyproject.toml |
Updates compatibility-floor comments only. |
mcp/servers/witan-code/changelog.d/20261029_125338_blarghmatey_omnigraph_0_14_pin.md |
Clarifies negotiated contract support. |
docs/internals/design/omnigraph-0-14-upgrade-spec.md |
Records negotiation and revised rollout ordering. |
bin/install_omnigraph_server.py |
Adds isolated installation of the previous release. |
bin/check_omnigraph_format.py |
Validates the binary’s declared HTTP contract. |
.github/workflows/witan-tests.yml |
Installs and enables the previous server for integration tests. |
Review effort: Balanced
…fresh connection refresh_contract adopted whatever its probe saw, so a probe that reached the old server could overwrite a switch another thread made meanwhile, after a request was refused by the new one. It now adopts only if the contract is unchanged since the probe started. load_ndjson now drops this thread's pooled connection before uploading. A pooled socket may lead to a server the data tier has since replaced, and a write that dies mid-upload is FATAL, while a fresh connection fails at connect, before anything was sent. Raised by Copilot on #580. Claude-Session: https://claude.ai/code/session_01FGZebHdoMkhaQ1ewuGV2Ni
The rollout step said the council probe pins 0.13 until a task lands. mitodl/ol-infrastructure#6811 is that change, so the step now names it. Claude-Session: https://claude.ai/code/session_01FGZebHdoMkhaQ1ewuGV2Ni
feoh
approved these changes
Nov 4, 2026
blarghmatey
added a commit
to mitodl/ol-infrastructure
that referenced
this pull request
Nov 4, 2026
…lthz (#6811) * feat(omnigraph): discover the council probe's HTTP contract from /healthz The council probe pinned Omnigraph-Http-Api to 0.13, so once the data tier moves to omnigraph 0.14 every run would get 400 api_contract_mismatch and fail, alerting every 15 minutes on a healthy graph and blind to a real outage. A hand-edited bump can't be timed right either: the script ships as a ConfigMap that swaps before the server pod rolls. The probe now sends HEAD /healthz first, takes the single contract the server announces if it is 0.13 or 0.14 (the contracts witan-core negotiates, from mitodl/agent-kit#580), and sends it on both queries. A missing, repeated or unsupported stamp fails the run with a message naming it. Query responses must carry the same stamp, now including a stamp-less one: the 0.11 tolerance is gone, since every stack serves 0.13 or later. The discovery call is counted in the derived activeDeadlineSeconds (now 75s). Task: tk-council-probe-discover-the-omnigraph-http-contra-9c8302 Claude-Session: https://claude.ai/code/session_01FGZebHdoMkhaQ1ewuGV2Ni * refactor(omnigraph): share the council probe's transport-failure wording discover_contract and run_probe each mapped URLError and TimeoutError to the same message, so the wording lives in one helper now, as does the "none" rendering of a missing stamp. The PROBE_QUERIES comment named the old deadline formula, which now also counts the discovery call. The mismatch message no longer promises the next run clears it: with one Recreate replica a refusal of the contract /healthz just announced can also mean something rewrote the header. Adds a test for a /healthz that answers HEAD with 405. Claude-Session: https://claude.ai/code/session_01FGZebHdoMkhaQ1ewuGV2Ni
This was referenced Nov 5, 2026
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.

What are the relevant tickets?
witan task
tk-witan-core-negotiate-the-omnigraph-http-contract-e29072, the go from spiketk-spike-let-witan-core-negotiate-the-omnigraph-htt-3c94fb. Stacked on #573: it merges intofeat/omnigraph-0.14-pin, notmain, because a 0.13-built client lacks #573's disposition handling and would replay a 409partialload on a 0.14 server.Description (What does it do?)
Today a 0.14 witan and a 0.13 data tier refuse each other (
400 api_contract_mismatch), and the omnigraph and witan stacks deploy from separate pipelines in no fixed order, so whichever deploys first breaks witan until the other catches up. This makes witan-core speak whichever of 0.14 and 0.13 the server stamps, so the 0.14 cutover needs no pause of witan's CI deploy.PooledTransportprobesHEAD /healthzon first use and sends the server's stamp when it is inSUPPORTED_HTTP_API_CONTRACTS(0.14,0.13).400 api_contract_mismatchstamped with another supported contract switches the transport and resends once, writes included. A second mismatch on the same call staysCONTRACT_VIOLATION.RESPONSE_CONTRACT_REQUIREDand the 0.11 tolerance behind it are gone.Implementation details
The evidence for resending a write is from the spike (memory
pf-omnigraph-0-13-and-0-14-servers-announce-their-h-229d01). Both servers stampOmnigraph-Http-Apion every response, the 400 mismatch and the publicHEAD /healthzincluded.require_contractruns after bearer auth and before graph resolution and body work at both tags, and 0.14's disposition lists the mismatch aseffects: none.shared_transportkeeps one transport per server URL per process. Connections stay per thread.PROBE_TIMEOUT_SECONDS), outside the lock, so a server that accepts connections and never answers doesn't queue every thread behind one probe. A failed probe, or one without exactly one supported stamp, is not cached: the request goes out with the pinned contract and classifies its own failure, so a down server still reads asUNAVAILABLE, and the next call probes again.load/ndjsonsent with the wrong contract fails at send with a broken pipe. A reused connection failing mid-flight would make a write's resend FATAL, while a fresh one fails at connect, before anything was sent.load_ndjsonre-probes before every load. Because a wrong-contract load is cut at send, a long-lived process that cached 0.13 would otherwise fail every large load after the data tier moved, until some small call learned the new contract from its 400.check-omnigraph-formatnow also asserts the binary'shttp-contractequalsHTTP_API_CONTRACT. Before this, nothing checked it, so a release that moved the contract without moving the storage format would have passed.load/ndjson, where the server refuses a wrong contract at the middleware without reading the body. A large upload could be cut mid-send and come back with no stamp to learn from._switch_contractonly adopts the stamp if the transport is still on the contract the refused request sent. If another thread already moved it, the caller resends with the transport's current contract.bin/install_omnigraph_server.py --release previous --dest …fetches the v0.13.0 server and CLI, checked against the digestsmainpinned for v0.13.0. It refuses to run without--dest, so a secondomnigraphnever lands on PATH. An import-time assert ties it toSUPPORTED_HTTP_API_CONTRACTS.0.13untiltk-council-probe-discover-the-omnigraph-http-contra-9c8302lands.How can this be tested?
Run against the 0.14.0 release binaries (a scratch
HOMEwhose~/.local/binholds them), withOMNIGRAPH_PREVIOUS_SERVER_BINpointing at a v0.13.0omnigraph-serverthat has itsomnigraphCLI beside it.just test-witan-core: 903 passed, 7 skipped.WITAN_REQUIRE_OMNIGRAPH=1, the served suites pass against both servers (each test id carriesomnigraph-0.14.0oromnigraph-0.13.0):test_group_commit_served.py6 and 6,test_migrate.py::test_store_merge_into_a_served_graph_never_shells_out1 and 1, witan-code'stest_branches_served.py3 and 3.test_pre_upgrade_candidates_exclude_the_current_binary, which fails because the scratchHOMEleaves twoomnigraphbinaries on PATH. Two witan-codetest_rebuild_refuses_*tests also fail, because the longTMPDIRwraps the phrase they assert. All three pass in the default environment.uv run prek runon the changed files,just docs-check,just check-versions,just check-omnigraph-pinsandjust check-omnigraph-format --binary <0.14.0>pass. Against a stub binary printinghttp-contract 0.15,check-omnigraph-formatexits 1 with the remedy.The new CI step that installs the 0.13.0 server is only proven by this PR's own
witan-testsrun.https://claude.ai/code/session_01FGZebHdoMkhaQ1ewuGV2Ni