Skip to content

fix(cli)!: print the load route's body from load --json - #1113

Merged
ragnorc merged 4 commits into
ModernRelay:mainfrom
ragnorc:cli-load-json-route-body
Oct 21, 2026
Merged

ragnorc merged 4 commits into
ModernRelay:mainfrom
ragnorc:cli-load-json-route-body

Conversation

@ragnorc

@ragnorc ragnorc commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

What

omnigraph load --json now prints the load route's response body,
GraphBatchLoadOutput (the body of POST /graphs/{graph_id}/load/ndjson), on
direct, served and managed targets. The CLI-only LoadOutput is removed.

field before after
uri always present (CLI-only) removed
actor_id absent present; null when the write has no actor
base_branch omitted when --from was not given always present; null without --from

The other fields (branch, branch_created, mode, nodes, edges,
total_entities, embedding_generation, commit) keep their values. Their
order in the JSON follows the route type now, so mode comes after
branch_created. Human output is unchanged: the address it prints comes from
the client, not the body.

Each field was checked against GraphBatchLoadOutput in
crates/omnigraph-api-types/src/lib.rs:

  • Served arm: the CLI returns the body it deserialized from the server.
  • Embedded arm: the CLI builds the body with
    graph_batch_load_receipt_output, the same constructor
    server_load_ndjson uses. Its actor_id is the actor the CLI attributes
    the write to (--as, else operator.actor), the same value as
    commit.actor_id. On the served arm it is the actor the server resolved
    from the credential.

This changes machine output, hence the ! and a breaking release note.

Why

Backing RFC: #1017, "Graph access and cross-process writes", section One set
of operations
: a command's --json output is the route's response body. Its
gap table lists "load --json prints a CLI-only LoadOutput, which adds
uri, drops actor_id and omits a null base_branch → Print the route's
body."

Tests

  • parity_matrix::parity_load: both arms print actor_id: act-parity and an
    explicit base_branch: null, and each arm's output round-trips through
    GraphBatchLoadOutput unchanged, so it has no field the route lacks. The
    existing scrubbed embedded/remote comparison still holds.
  • cli_data::load_json_outputs_summary_for_main_branch: the direct output has
    no uri, has base_branch: null and actor_id: null (equal to
    commit.actor_id), and round-trips through the route type.
  • output::tests::load_human_preserves_branch_and_embedding_diagnostics now
    builds the route type and passes the address separately.

No existing test asserted the load output's uri. Parity had scrubbed it as
volatile.

Verification

Run locally with RUST_MIN_STACK=16777216, after cargo build -p omnigraph-server:

  • cargo test --locked -p omnigraph-cli --lib --test integration -- output:: transfer:: client:: cli_data::load parity_matrix::parity_load cli_cluster::managed_data system_local::local_cli_ingest system_local::local_cli_load system_local::local_cli_end_to_end system_local::local_cli_failed_load system_remote::remote_ingest system_remote::remote_load system_remote::remote_unix_socket system_remote::remote_policy_enforces_branch_first --test-threads=6:
    lib 33 passed, 3 ignored (nightly: deadline tests); integration 21 passed.
  • cargo test --locked -p omnigraph-cli --test integration -- parity_matrix:: cli_data:: --test-threads=6:
    127 passed.
  • cargo test --locked -p omnigraph-cli --lib -- skill::: 6 passed;
    cargo test --locked -p omnigraph-skill: 1 passed.
  • cargo clippy -p omnigraph-cli -p omnigraph-api-types -p omnigraph-server --all-targets --locked -- -D warnings -W clippy::dbg_macro: clean.
  • cargo fmt --all --check, python3 scripts/check-docs.py,
    bash scripts/check-agents-md.sh, typos on the changed files: clean.

load --json printed a CLI-only LoadOutput that added uri, dropped
actor_id and omitted a null base_branch. Both the embedded and served
arms now return the route's GraphBatchLoadOutput: the served arm as the
server sent it, the embedded arm built by the constructor the route
uses. The human output still names the graph from the client address.

ragnorc commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Recommendation

Merge. I found no blocking defect.

Why

load --json printed a CLI-only LoadOutput. That shape added uri, dropped actor_id, and omitted a null base_branch. RFC 1017 (One set of operations) says a command's --json output is the HTTP route body. The route is POST /graphs/{graph_id}/load/ndjson. Its body type is GraphBatchLoadOutput.

What

This change deletes LoadOutput and both mappers. Served and managed loads return the body the server sent. A direct load builds that body with graph_batch_load_receipt_output, the constructor server_load_ndjson already uses. Human text still names the graph. The address comes from client.uri(), which is the direct graph URI or the remote base URL. That is the string the old human line printed.

How the values stay true

The direct actor is --as, else operator.actor (resolve_actor_with_config). The load and the body both receive that value. The commit records the same string in new_lineage_intent_for_branch. For a served load, the server resolves the actor from the bearer token and writes that actor on the body. The CLI prints the body it decoded.

normalize_load_scope sets base_branch to None when --from is absent. The route type serializes that as null. The old CLI skipped the null. CliLoadMode is Copy, so both mode.into() calls are the same mode. The JSON spelling matches LoadMode::as_str.

Impact

A script that read uri from load JSON must take the address from its own --target. The stderr target line also names the destination. actor_id is new. It is null when the write has no actor. base_branch is always present. Human output keeps the same words.

Tradeoff and liability

One type now owns the machine contract. Human output takes the address from the client, so the route type stays the route body. That split is the right one.

The change removes two mappers that could drift from the route. A new field on GraphBatchLoadOutput appears on every arm with no second mapper. load --json will change when the route type changes. That coupling is the contract. The break is in changelog.d/load-json-route-body.breaking.md.

Five more changes of this kind delete the next private output types the same way. They do not add a framework.

Managed load uses the remote arm, so it prints this same body. Catalogue load refuses before any body. Open PRs 1114 and 1115 edit the CLI and do not replace this body. PR 1115 will conflict in run_load because it drops the managed-client argument. The JSON contract still holds after that edit.

Invariant

The broken rule is one public JSON contract (RFC 1017 and invariant 12, one source of truth). The class is every command that projects a private DTO over a route body. The fix sits at the CLI boundary that owns --json, and it uses the type the route already owns. Served trust stays at the server (invariant 10). The served actor comes from the credential.

Tests

A .gqt case cannot see CLI stdout. The owner is the CLI integration suite. The PR extends cli_data::load_json_outputs_summary_for_main_branch and parity_matrix::parity_load.

Before, commit 50283ceff03b8e3a1902d30757fd0808ea277b7d. I added only the new assertions to that existing test, then restored the file.

RUST_MIN_STACK=16777216 cargo test --locked -p omnigraph-cli --test integration cli_data::load_json_outputs_summary_for_main_branch -- --exact

Result: failed. The payload still had uri, no base_branch key, and no top-level actor_id.

After, commit 533f752d403f3894bc58585454bb282376b9607d. The same command passed (1 passed).

cargo test --locked -p omnigraph-cli --lib output::tests::load_human_preserves_branch_and_embedding_diagnostics -- --exact passed.

cargo test --locked -p omnigraph-cli --test integration parity_matrix::parity_load -- --test-threads=2 passed both parity_load and parity_load_embedding_diagnostics.

CI on this commit is green, including Test Workspace, Clippy, rustfmt, the GQT jobs, and the docs checks. I did not re-run that full set locally.

Limitations

I did not run the full CLI integration suite. The first parity run in this environment timed out because the harness looks for ../../target/debug/omnigraph-server and CARGO_TARGET_DIR pointed elsewhere. The rerun found the built server and passed. I make no performance claim.

Findings

Blocking: none.

Optional, on skills/omnigraph/references/commands.md line 414: this sentence names direct and served targets. Managed load --json uses the same remote arm and prints the same body. The changelog already says direct, served, and managed. Add managed here so the skill matches that contract.

# Conflicts:
#	crates/omnigraph-cli/src/client.rs
# Conflicts:
#	crates/omnigraph-cli/src/client.rs
@ragnorc
ragnorc added this pull request to the merge queue Oct 21, 2026
Merged via the queue into ModernRelay:main with commit 277478d Oct 21, 2026
32 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.

1 participant