Skip to content

rfc: graph access and cross-process writes - #1017

Merged
ragnorc merged 21 commits into
ModernRelay:mainfrom
ragnorc:rfc/local-sessions-and-client-access
Oct 21, 2026
Merged

ragnorc merged 21 commits into
ModernRelay:mainfrom
ragnorc:rfc/local-sessions-and-client-access

Conversation

@ragnorc

@ragnorc ragnorc commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

This draft covers graph access and cross-process writes while retaining one
ordinary cluster-backed server model on localhost and remote hosts.

The second serving model is discontinued. #1075, #1077 and #1083 are closed
without merging, and #980 is closed as not planned. This RFC no longer proposes
standalone/local serving APIs, generated local policy and credentials, server
records, election/reuse, automatic Studio or MCP hosting, or retirement of
omnigraph-server as part of that design.

Independent scope remains:

Accepted 2026-10-20 (maintainer decision): the remaining storage, identity,
HTTP, MCP and outcome changes. Appendix A's items stay separate decisions. No
serving implementation from the cancelled stack reached main.

Validation: documentation and RFC registry checks, spelling, and git diff --check. This is a documentation-only scope correction; no runtime behavior or
test qualification is claimed.

Draft RFC for issue ModernRelay#980: `omnigraph serve` local sessions with session
records, a thin `omnigraph studio` launcher, `omnigraph mcp` for agents,
and `omnigraph credential` for static tokens. Wider-blast-radius changes
are listed in Appendix A with their filed issues.

@aaltshuler aaltshuler 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.

Reviewed the RFC against current main and RFC 0059. The inline comments identify seven design gaps in session election, destination/actor binding, Studio transitions, and MCP behavior. These are specification findings; this PR contains no runtime implementation.

Comment thread docs/rfcs/2026-10-16-local-sessions-and-client-access.md Outdated
Comment thread docs/rfcs/2026-10-16-local-sessions-and-client-access.md Outdated
Comment thread docs/rfcs/2026-10-16-local-sessions-and-client-access.md Outdated
Comment thread docs/rfcs/2026-10-16-local-sessions-and-client-access.md Outdated
Comment thread docs/rfcs/2026-10-16-local-sessions-and-client-access.md Outdated
Comment thread docs/rfcs/2026-10-16-local-sessions-and-client-access.md Outdated
Comment thread docs/rfcs/2026-10-16-local-sessions-and-client-access.md Outdated
Rename the session to a local server and --session to --via-server.
Move server records from /tmp, which macOS ages out after three days,
to per-user directories resolved from the account, with the machine
they belong to. Never write a token to a terminal, list live local
servers in targets --json, give each agent context one write path,
add platform coverage, and gather every refusal code in one table.
@ragnorc ragnorc changed the title docs(rfc): propose local graph sessions and client access docs(rfc): propose local graph servers and client access Oct 18, 2026
@ragnorc ragnorc changed the title docs(rfc): propose local graph servers and client access rfc: local graph servers and client access Oct 18, 2026
…-process writes

Rewrite the RFC from first principles: name the target, never the path;
correctness in the engine across processes; one set of operations on three
surfaces; one serving concept with explicit audiences, folding the
omnigraph-server binary into omnigraph serve; --as as a declaration or an
assertion; one outcome model and exit table; and introspection through
target list and access. Bring cross-process writer safety and the access
route into scope, with the engine design on the __manifest version CAS,
and land the interfaces before the engine phase. Rename the file to match
the new title.
@ragnorc ragnorc changed the title rfc: local graph servers and client access rfc: graph access, serving and cross-process writes Oct 18, 2026
Comment thread docs/rfcs/2026-10-16-graph-access-serving-and-cross-process-writes.md Outdated
Comment thread docs/rfcs/2026-10-16-graph-access-serving-and-cross-process-writes.md Outdated
Comment thread docs/rfcs/2026-10-16-graph-access-serving-and-cross-process-writes.md Outdated
…ep set

The review of ef44175 recommended merging, with four optional
corrections: read-back has three results; a caller settles a control
fact from the fact, since the head does not move; cleanup candidates
still pass protected_manifest_branches and retained_merge_bases; a crash
loses the attempt id, so only delete and drop converge after one. A
not-ready branch's listing fields come from its registration.
@ragnorc

ragnorc commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

All four optional corrections are in eca33ef:

  1. Crash. A crash loses the attempt id with its process. Delete and drop converge without it. A create doesn't: a new create of the name refuses branch_exists, and repair completes the branch or its delete removes it.
  2. Read-back has three results: landed, lost (readable with anything else, or absent and never sent) and pending (unreadable). A caller settles a control fact from the registration, tombstone or drop record itself, since the head doesn't move, and a retry is safe. The sample attempt is now ULID-shaped. Evidence adds an unreadable-version case, plus a delete and a drop that find another attempt's fact.
  3. Keep set. Cleanup candidates still pass protected_manifest_branches and retained_merge_bases, and a not-ready fork point stays. The registration set picks candidates; it doesn't replace that closure.
  4. feat(server,cli): report each branch's head, start and times in the branch listing #1012. A not-ready branch's registration fills the name, parent and fork commit, and head and created_at stay absent until its ref exists.

The #1093 log sentence now names the publication id (013877f).

ragnorc commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

Hold the merge. One blocking hole is open. The four optional corrections from the review of ef441758 are in the text. Two of them are only partly closed. The create and repair crash path is consistent.

Why this shape is right

A person names a target. The ordinary cluster server is the only HTTP server. Writer safety stays on the __manifest version compare-and-swap. Lance writes that version with put-if-not-exists, so one writer wins the version. A control fact does not move the head, so the fact carries an attempt id. That id is what a lost acknowledgement reads.

Earlier findings, at eca33ef1

  1. A crash drops the attempt id. Resolved. The recovery paragraph says the id dies with the process. A later delete or drop completes the follow-ups. A new create of the name refuses with branch_exists. repair completes the branch, or delete removes it. That matches the create paragraph. The rule uses the durable fact and the next operation.

  2. Read-back has three results, and the caller reads the fact. Partly. The text now has landed, lost, and pending. The sample attempt is a ULID (01JAD…). The catalog evidence adds the unreadable version, and a delete and a drop that find another attempt's fact. The caller reads the registration, the tombstone, or the drop record. Two gaps remain. See the blocking finding and optional finding 1.

  3. The cleanup keep set. Resolved. Candidates still pass protected_manifest_branches and retained_merge_bases. A not-ready registration's fork point stays. The registration set picks candidates. On main, protected_manifest_branches in collector.rs holds the live manifest trees, the trees of live tags, and the trees retained_merge_bases imports. optimize.rs closes that set over dependencies before it deletes.

  4. feat(server,cli): report each branch's head, start and times in the branch listing #1012 fields for a not-ready branch. Partly. The registration fills the name, the parent, and the fork commit. head and created_at stay absent until the ref exists. See optional finding 2.

Blocking

  1. An unknown publication must use the version-key fence. This RFC says a later write to a branch whose publication is pending answers service_unavailable, and that a control fact stays pending under its attempt id the way rfc: storage outcomes per operation and restartable ownership #1093 keys a row publication by graph commit id. rfc: storage outcomes per operation and restartable ownership #1093 at 9f225da7 removes that record. An unknown manifest request blocks nothing. The version key fences it: the branch's next publication builds on the late commit, or takes the version first. Later writes proceed. rfc: storage outcomes per operation and restartable ownership #1093 records why the registry went: a rerun that had captured its authority before the registry resolved failed to converge. The log sentence in 013877fd does name the publication id, as the reply says. The id is a log field and the later disposition.check value.

    Use that same fence for a control fact. Keep the attempt id on the fact, in the log, and in disposition.check. Remove the one-branch service_unavailable row. A read of the registration, tombstone, or drop record is final only after the branch moves past the attempted version.

    Line notes, same file docs/rfcs/2026-10-16-graph-access-serving-and-cross-process-writes.md:

    • The sentence "a later write to a branch whose publication is pending answers service_unavailable", and the table row "A later write to a branch whose publication is pending". Remove both. Keep outcome_unknown.
    • The bullet "Pending publications" ("pending under its attempt id, as a row publication is under its graph commit id"). A control fact uses the version-key fence. The attempt id stays on the fact, in the log, and in disposition.check.

Optional

  1. A missing version stays pending. fix(catalog): a publication that never sent its manifest is unpublished #1092 at faeef19e returns a definite failure from the commit's request record before it reads the version back. A failed checkout, including an absent version, stays in doubt: a request that may have landed can still land. After an acknowledgement, a readable version that holds another commit also stays in doubt. The new sentence puts "absent, and the request never reached the store" in the lost result, and it calls every other readable fact a loss. Follow the fix(catalog): a publication that never sent its manifest is unpublished #1092 split. Never-sent is the request record. Absence at checkout stays pending. Another fact is a loss only when this attempt was not acknowledged. Line: the read-back sentence that starts "It lost when the version is readable".

  2. updated_at has no source on a not-ready branch. feat(server,cli): report each branch's head, start and times in the branch listing #1012 at 87bc0a81 builds BranchDetail.updated_at from created_at when the branch has written no commit, and from head.created_at otherwise. Both sources are absent until the ref exists. head and updated_at are required fields today. Let both stay absent. The registration fills name, from, and from_commit_id. Line: "Its registration fills the name, the parent and the fork commit".

  3. Name the access targets with the RFC 0059 kinds. Those kinds are direct graph, direct root, direct catalogue, server, and managed account or cluster. The Access section answers for direct targets and server targets. The new evidence sentence answers for root, graph-store, standalone, and server, and it refuses catalogue. graph-store and standalone are not kinds in either text. cli_data is the right owner. cli_data::access_reports_what_the_caller_may_do_at_a_target is not on main. Line: the evidence bullet that starts "access answers for root, graph-store".

Tradeoff and liability

The attempt id stays the right witness. It adds no second row format. The version key is already the one publication door. A pending record would be a second authority, and #1093's crash enumeration showed that authority makes a rerun fail. Dropping the record here removes that failure mode. The cost is that one read of a missing fact is not a final failure. Five more control facts use the same version key and the same attempt id.

The client half is unchanged by this delta. One server, one target resolver, and one failure object still remove liability. That stays safe while serve stays removed and the one-writer boundary stays until the engine phase ships.

Validation

  • Reviewed head eca33ef17b768fa737169962ea8c8ce595015d9f. Main is b692610c4aa31066cb6a9fd34830f56977288943. The pull request is open. No other review covers this head. The delta from ef441758 is two commits, db2a0c81 and eca33ef1: 41 insertions and 15 deletions, in the RFC file only.
  • Inspection of main: publisher.rs read-back and the VersionNotFound arm; collector.rs protected_manifest_branches and retained_merge_bases; optimize.rs dependency close. Today's repair reports foreign drift and writes no ref. This RFC assigns it the completion duty.
  • Command, on main b692610c: cargo test -p omnigraph-catalog --lib tests::publish_without_lineage_moves_the_table_rows_under_the_head_it_read -- --exact. Result: ok (1 passed; 206 filtered out). The production lineage.is_none() guard is compiled out of this test build. The pass shows the head-preserving shape. The production refusal is the source guard.
  • Open heads read: rfc: storage outcomes per operation and restartable ownership #1093 9f225da7 (013877fd names the publication id in the log; 77056789 replaces the pending registry with the version-key fence), fix(catalog): a publication that never sent its manifest is unpublished #1092 faeef19e (a failed checkout stays in doubt), feat(server,cli): report each branch's head, start and times in the branch listing #1012 87bc0a81.
  • Lance transaction page, fetched 2026-10-20: put-if-not-exists, one writer wins a manifest create, and a conflict may rebase. OmniGraph's zero-retry overwrite does not rebase. The delta leaves that claim as written.
  • This pull request is documentation. It has no bug fix. No GQT regression applies. A manifest head beside a row change is not a query result. Workspace suite: not run.
  • No performance numbers were checked. The cost paragraph is a plan.

Recommendation

Hold the merge until the pending-publication paragraph uses #1093's version-key fence. accepted can stay once that paragraph does. The three optional sentences can land in the same edit.

Storage outcomes per operation dropped its pending-publication registry:
an unknown publication is recorded nowhere and blocks nothing, and its
version key fences it. A control fact follows the same fence, and the
one-branch service_unavailable row goes. Read-back follows ModernRelay#1092's
split, a not-ready branch's absent listing fields are named, and the
access evidence uses RFC 0059's target kinds.
@ragnorc

ragnorc commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

Addressed at 78eb27f.

Blocking: the version-key fence. A control fact now follows #1093's §5. An ambiguous request is recorded nowhere and blocks nothing; the chain's next publication either loads it or takes its version first. A read of the registration, tombstone or drop record is final only after the chain moves past the attempted version. The attempt id stays on the fact, in the log and in disposition.check. The one-branch service_unavailable sentence, its table row and the 'pending under its attempt id' bullet are gone, and outcome_unknown stays.

Optional

  1. Read-back follows fix(catalog): a publication that never sent its manifest is unpublished #1092's split. Never-sent (the request record) is lost. Its own attempt id is landed. Another fact is lost unless the store acknowledged this attempt. An absent version at checkout or a failed read stays unknown until the version key settles it. The evidence case matches.
  2. A not-ready branch's registration fills name, from and from_commit_id. head, created_at and updated_at stay absent until its ref exists, so feat(server,cli): report each branch's head, start and times in the branch listing #1012 makes them optional.
  3. The access evidence now uses RFC 0059's kinds: direct graph, direct root, server, direct catalogue and managed. It names the test in feat(cli): say who you are at a target and what you may do #1104, where it lives.

@ragnorc

ragnorc commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Non-blocking alignment note from the actor views RFC (#951, now at 5f14b1b). It adopts this RFC's model as revised today.

  • Access route. rfc: granular permissioning #951 lists GET /graphs/{graph_id}/access and omnigraph access among its surfaces and fills the reserved view field. The field holds facts about the caller's own view only, none of which change with hidden rows: restricted or bypass, the view digest and access version, bounded counts of readable, writable and owned securables, and a delegated credential's caveats. Explaining another actor's view is a separate bypass-only operation.

  • Error registry. Its refusals join the one ErrorCode registry. Each is a refusal met before the write started, so effects: none, action: fix:

    Code Status
    view_restricted 403
    view_write_denied 403
    identity_binding_denied 403
    placement_required 400
    delete_blocked 409
    view_limit_exceeded 422

    For a restricted actor the server strips foreign facts (other rows' ids, counts, versions) from the details, never the code or the disposition.

  • disposition.check (later phase). Under a restricted view, commit listings and commit show are refused in rfc: granular permissioning #951's first scope, while branch heads stay visible. A restricted writer can therefore settle an unknown write only through the check: it should answer for the caller's own publication id and return the caller's own commit receipt, without the commit's other effects. Until that phase ships, head comparison plus if-graph-commit replay works unchanged.

  • Serving. A graph with actor-view declarations activates only under a version-2 cluster policy, which defines internal and bypass. The server refuses it without authentication and with static tokens but no policy, so a self-hosted static-token deployment needs that policy for such graphs. Direct access stays storage-holder access. Where the engine enforces the graph's policy for the declared actor, it applies that actor's view as a convenience, not as confidentiality.

@ragnorc

ragnorc commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Aligned with the actor views note at 298515b:

  • Access route. The reserved view field now names rfc: granular permissioning #951 as its owner, holding facts about the caller's own view only.
  • disposition.check. Under a view that hides the commit listing, the check answers for the caller's own publication id and returns only the caller's own receipt.

The six refusal codes and the serving rule belong to #951 and need no text here. The registry already takes them as effects: none, action: fix refusals. Decision log updated.

@ragnorc ragnorc mentioned this pull request Oct 21, 2026
5 of 7 tasks

ragnorc commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Hold the merge. The pending-publication record is gone, and that is the right cut. One settlement rule still waits one version too long, and the linear fence is stated for every write.

Reviewed head 298515b42d6be5c8fe4c19de15ef6af93d6366af. Main is 50283ceff03b8e3a1902d30757fd0808ea277b7d. The delta from eca33ef1 is 78eb27f1 and 298515b4.

Why, what, how, impact

An unknown manifest request must not grow a second record. The version key already decides a linear overwrite. This delta drops the one-branch service_unavailable row and adopts that fence. The attempt id stays on the control fact, in the log, and in disposition.check.

The new sentences say a read is final only after the chain moves past the attempted version. A read is final when that version is readable. The same sentences give every unsettled write the linear fence. A detached proposal and an unattached snapshot do not use that fence.

The extra wait stalls a control-fact create. Its next step writes a ref. It does not publish another manifest version. A detached write settled from the linear head can report the wrong result.

Earlier findings

  1. Unknown publication must use the version-key fence. Partly. The pending record and the one-branch service_unavailable row are gone. outcome_unknown stays. The finality sentence does not match rfc: storage outcomes per operation and restartable ownership #1093 §5. See the blocking finding.
  2. Read-back follows fix(catalog): a publication that never sent its manifest is unpublished #1092. Partly. A never-sent request is a loss. A version that holds this attempt id landed. An absent version stays unknown. The acknowledged case has no result, and a refused request is missing. See the optional finding.
  3. Absent fields on a not-ready branch. Resolved in this text. The registration fills name, from, and from_commit_id. head, created_at, and updated_at stay absent until the ref exists. feat(server,cli): report each branch's head, start and times in the branch listing #1012 at 16b832f5 still requires head and updated_at (BranchDetailOutput). That change stays on feat(server,cli): report each branch's head, start and times in the branch listing #1012, before the engine phase.
  4. Access target kinds. Resolved. The evidence uses direct graph, direct root, server, direct catalogue, and managed. cli_data::access_reports_what_the_caller_may_do_at_a_target is on feat(cli): say who you are at a target and what you may do #1104 at 44365430. The test covers those kinds, an absent store (not_found), and a damaged store (internal).

The actor-views sentences match #951 at 5f14b1ba. The view field holds facts about the caller's own view. Under a view that hides commit listings, the check returns the caller's own receipt. The six refusal codes stay in #951. Each has effects: none and action: fix.

Blocking

1. A readable attempted version is already final

Use this rule for a linear overwrite:

  • The version holds this attempt: the write landed.
  • The version holds another fact, and this attempt was not acknowledged: the write lost.
  • The version is absent: the write stays unknown until some commit holds that version.

#1093 §5 at c8ba085 says the next publication loads the late commit, or it creates that version first. #1096 at 9c18c8a says a missing write is not a failure until another commit follows the base. publisher.rs already returns success when the reloaded head is this attempt.

This text says the read is final only after the chain moves past the attempted version. That wait is one version too long. The read-back bullet says a version that holds this attempt id landed. The unknown-outcomes bullet says that read is not final yet. An implementer cannot follow both. A create's follow-up writes the ref, so the chain can remain on the registration.

Keep the detached case on #1093 §5. A proposal and an unattached snapshot publish with overwrite_detached. The next linear publication does not load that version and does not take its id. The rule 6 sentence gives every unsettled write the linear fence. Point that sentence at §5.

Change these sentences: "later writes to its branch proceed"; "The outcome is final once the branch moves past the attempted version"; the unknown-outcomes bullet; the catalog evidence bullet "stays unknown until the chain moves past it".

Optional

  1. Name both definite losses, and give the acknowledged case a result. commit.rs marks the attempt unpublished when ManifestRequest::may_have_landed is false. That is NotSent and Refused. A refused request was sent. The store refused it before it could apply. It is a loss, with the never-sent request. When the store acknowledged this attempt and the version holds another fact, the attempt stays unknown. publisher.rs returns in doubt on after_ack. The "unless" clause removes the loss and states no result.

Tradeoff and liability

The version key removes the pending record. That record made a rerun fail, which is why #1093 dropped it. One attempt id remains the witness for every control fact. The common write stays on the reload path.

The extra wait adds a stall to every control fact whose follow-up is not a publication. Five more facts of this kind would copy that stall. A detached write judged from the linear head adds a false result. The readable-version rule removes both. It adds no new row and no new record.

The client half is unchanged. One server, one target resolver, and one failure object still remove liability. That stays safe while serve stays removed and the one-writer boundary stays until the engine phase ships.

Validation

Recommendation

Request changes. Hold the merge until the finality sentences match #1093 §5. accepted can stay once they do. The optional sentence can land in the same edit.

Inline

docs/rfcs/2026-10-16-graph-access-serving-and-cross-process-writes.md:373

Blocking. This sentence gives every unsettled write the linear fence. #1093 §5 limits that fence to a linear overwrite. A proposal and an unattached snapshot publish with overwrite_detached. The next linear publication does not load that version and does not take its id. Point this sentence at §5, and leave the detached case there.

docs/rfcs/2026-10-16-graph-access-serving-and-cross-process-writes.md:767

Blocking. A read of the attempted version is final when that version is readable. It landed when the version holds this attempt id. It lost when the version holds another fact and this attempt was not acknowledged. It stays unknown while the version is absent, until some commit holds that version.

#1093 §5 says the next publication loads the late commit or creates that version. It does not wait for a later version. The read-back bullet above already says a version that holds this attempt id landed. This sentence says that read is not final yet. An implementer cannot follow both.

A create's follow-up writes the ref. It does not publish the next manifest version, so the chain can remain on the registration. Use the readable-version rule here, in "The outcome is final once the branch moves past the attempted version", and in the catalog evidence bullet "stays unknown until the chain moves past it".

docs/rfcs/2026-10-16-graph-access-serving-and-cross-process-writes.md:756

Optional. commit.rs marks the attempt unpublished when ManifestRequest::may_have_landed is false. That is NotSent and Refused. A refused request was sent. The store refused it before it could apply. Name it beside the never-sent request.

When the store acknowledged this attempt and the version holds another fact, the attempt stays unknown. publisher.rs returns in doubt on after_ack. The "unless" clause removes the loss and states no result.

@ragnorc

ragnorc commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Addressed at 33ba294.

Blocking: a readable attempted version is final. The read-back bullet, the unknown-outcomes bullet, disposition.check and the catalog evidence bullet now follow #1093 §5:

  • the version holds this attempt: landed;
  • it holds another fact and the store never acknowledged this attempt: lost;
  • it is absent: unknown until some commit holds it.

Nothing waits for a later version, so a create whose follow-up writes only the ref settles at the registration.

Blocking: the linear fence. Rule 6 now points at §5. A linear publication's version key fences it. A detached proposal or unattached snapshot settles as §5 says, not by the next linear version. The unknown-outcomes bullet states that every control fact is a linear publication on its chain.

Optional. A refused request is a loss beside a never-sent one: may_have_landed is false for both. An acknowledged attempt whose version holds another fact stays unknown, as the publisher's after_ack in-doubt result has it.

The decision log records the change.

ragnorc commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Merge this RFC. The settlement holes from the review of 298515b4 are closed. I found no new blocking hole. One optional sentence remains. It does not change the decision.

Why, what, how, impact

An unknown manifest write becomes definite when its attempted version is readable. The version holds this attempt: the write landed. The version holds another fact, and the store did not acknowledge this attempt: the write lost. The version is absent: the write stays unknown until some commit holds that version.

A linear publication uses that version key. The next publication loads the late commit, or it creates the version first. A detached proposal and an unattached snapshot settle as §5 of #1093 says. The next linear version does not fence them.

A create whose follow-up writes only the ref settles at the registration. The chain can stay on that version.

Earlier findings

  1. A readable attempted version is already final. Resolved. The read-back bullet, the unknown-outcomes bullet, disposition.check, and the catalog evidence use this rule. Nothing waits for a later version.
  2. The linear fence covered every write. Resolved. Rule 6 points at §5. The unknown-outcomes bullet limits the fence to a linear control fact.
  3. Name both definite losses, and give the acknowledged case a result. Resolved. A never-sent request and a refused request are losses (may_have_landed is false for both). An acknowledged attempt whose version holds another fact stays unknown on the publisher read-back.

Optional

  1. The later check cannot see the acknowledgement. disposition.check carries the branch, the publication id, and the head before the write. rfc: storage outcomes per operation and restartable ownership #1093 records an unresolved request nowhere. Once the attempted version holds another commit, §5 says that write is not committed. State that loss in the check paragraph. Keep the in-doubt result on the publisher read-back, where after_ack is known.

Tradeoff and liability

The readable-version rule removes the extra wait. A control fact whose follow-up is a ref, a deregistration, or a tag release can settle on its own publication. The version key stays the one fence for a linear overwrite. This delta adds no row and no pending record.

The in-doubt result after an acknowledgement stays on the attempt that holds the request record. A later check that required that bit would leave a taken version unknown. Five more control facts use the same attempt id and the same version key.

The client half is unchanged. One server, one target resolver, and one failure object still remove liability. That stays safe while serve stays removed and the one-writer boundary stays until the engine phase ships.

Validation

  • Pull request open. Head 33ba2940bd81cc4749e739bc03a8b44bef89d763. No other review covers this head. The delta from 298515b4 is one commit: 41 insertions and 20 deletions, in the RFC file only.
  • Main is 50283ceff03b8e3a1902d30757fd0808ea277b7d.
  • Inspection of main: publisher.rs read-back. after_ack with another commit stays in doubt. A failed checkout_version, including an absent version, stays in doubt. Unpublished returns before read-back. commit.rs maps a probe with may_have_landed false to Unpublished. That is NotSent and Refused. overwrite_detached sets with_detached(true).
  • Lance 13.0.0 source: ConditionalPutCommitHandler writes the manifest with PutMode::Create. commit_transaction targets read_version + 1. A zero-retry overwrite does not rebase. commit_detached_transaction picks a random id with the high bit set (DETACHED_VERSION_MASK). The next linear version does not take that id.
  • Commands, on main 50283cef:
    • cargo test -p omnigraph-catalog --lib --locked tests::transaction_file_failure_before_the_manifest_is_unpublished_issue_1067 -- --exact → ok (1 passed; 212 filtered out; 0.22s).
    • cargo test -p omnigraph-catalog --lib --locked tests::manifest_create_without_a_definite_answer_stays_in_doubt_issue_1067 -- --exact → ok (1 passed; 212 filtered out; 3.33s).
  • Lance transaction page, fetched 2026-10-21: put-if-not-exists, one writer wins a manifest create, and a conflict may rebase. The page does not describe detached version ids. The source above does.
  • Open heads read: rfc: storage outcomes per operation and restartable ownership #1093 12d71633 (§5 still limits the fence to a linear overwrite. A read is not final until the branch moves past the write's base). rfc: granular permissioning #951 5f14b1ba (the view field and the check still match). fix(server): scope an unknown storage outcome to its key and branch #1096 7125d324 (a publication in doubt is fenced by its version key). feat(server,cli): report each branch's head, start and times in the branch listing #1012 16b832f5. feat(cli): say who you are at a target and what you may do #1104 3bcf8139 (the named access test is still present). fix(catalog): a publication that never sent its manifest is unpublished #1092 is merged.
  • This pull request is documentation. It has no bug fix. No GQT regression applies. A manifest version beside a control fact is not a query result. Workspace suite: not run.
  • No performance numbers were checked. The cost paragraph is a plan.

Recommendation

Merge. accepted can stay. The optional sentence can land in the same edit or after merge. It does not block.

Inline

docs/rfcs/2026-10-16-graph-access-serving-and-cross-process-writes.md:393

Optional. This check is a later read. Its fields are the branch, the publication id, and the head before the write. #1093 records an unresolved request nowhere.

Once this version holds another commit, §5 says the write is not committed. A create-if-absent cannot land after the version is held. State that loss here.

Keep the in-doubt result on the read-back bullet in the engine section. An acknowledged attempt whose version holds another fact stays unknown. publisher.rs returns in doubt only when after_ack is set, and only that attempt has the flag.

The catalog bullet already names both results. This paragraph names the loss only when the store did not acknowledge the attempt, and a later check cannot know that.

@ragnorc
ragnorc added this pull request to the merge queue Oct 21, 2026
Merged via the queue into ModernRelay:main with commit 17ad3d8 Oct 21, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants