Skip to content

feat(core): choose a transport per node, so a node without a share can work - #78

Merged
jdfalk merged 2 commits into
mainfrom
feat/transport-mode
Aug 12, 2026
Merged

feat(core): choose a transport per node, so a node without a share can work#78
jdfalk merged 2 commits into
mainfrom
feat/transport-mode

Conversation

@jdfalk

@jdfalk jdfalk commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

The design always called for two ways an agent reaches media. Only one was ever built. This adds the concept the second one needs — the byte-moving RPCs and the server-side install follow in the next two PRs.

Mode How the agent gets the media
Mount (default, unchanged) Reads and writes the library through a share; the server translates canonical paths to that node's local paths
Stream (new) The server sends the source bytes, the agent works on a local copy, the result comes back for the server to install

A streaming node needs no share, no credentials, and no knowledge of the server's storage layout.

The behavioural change is one line

Requirement::MountCovers no longer binds a streaming agent. Such a node advertises no mounts at all, so under the old rule it would satisfy nothing and be handed no work forever while reporting healthy.

The requirement still rides on the job — the same job must stay dispatchable to a mount-mode agent, and the server needs the canonical path either way: to translate it, or to read the bytes from.

Why this is not theoretical

On 2026-08-10, all three of windows-rtx2070's SMB mounts to the server reported Unavailable. Windows drive mappings are per-logon-session, and the session an agent runs in is not the interactive one holding them. Under mount-only transport that node is undispatchable no matter how well its encoder works — and NVENC there is confirmed working (RTX 2070 SUPER, driver 610.47, hevc_nvenc verified).

Backward compatibility

#[serde(default)] on the domain type, TM_MOUNT = 0 on the wire. An agent that has never heard of transports keeps exactly the behaviour it had. A new field that silently switched an existing agent to streaming would have been the worst possible way to ship this — there's a test asserting an older document still deserialises as Mount.

The conversion boundary

Both directions carry the field, with a note at each. That boundary is where the muxers field was dropped, and the failure shape was identical: an agent registering healthy and matching nothing.

The round-trip test now uses a streaming fixture, so dropping the field on the way out turns it red. Verified by doing exactly that:

# with `transport` hardcoded to TmMount outbound
a_capability_round_trips_through_the_wire ... FAILED

Also fixed

agent survey told a streaming node it was broken — with no mounts it printed "can be given no work", which sends an operator hunting a share problem on a node deliberately not using one. It now prints the transport and says the right thing for each.

Verification

  • cargo test --workspace524 passing, 0 failed (was 517)
  • cargo fmt -- --check — exit 0 · cargo clippy --all-targets --all-features -- -D warnings — exit 0
  • Four new targeted tests: streaming matches an unseeable path; mount mode still fails the same job (the paired positive); streaming does not excuse a missing encoder; an older document defaults to Mount

🤖 Generated with Claude Code

https://claude.ai/code/session_01U9keBRnGXBP2DiQmrcqm33

jdfalk and others added 2 commits August 10, 2026 19:19
…n work

The design always called for two ways an agent reaches media and only one was
ever built. This adds the concept the second needs.

Mount is what every agent did: read and write the library through a share, with
the server translating canonical paths to that node's local paths. Stream is the
other half -- the server sends the source bytes, the agent works on a local copy,
and the result comes back for the server to install, so the node needs no share,
no credentials and no knowledge of the server's storage layout.

The behavioural change is one line and it is the whole point: MountCovers no
longer binds a streaming agent. Such a node advertises no mounts at all, so under
the old rule it satisfied nothing and would have been handed no work forever
while reporting healthy. The requirement still rides on the job, because the same
job must stay dispatchable to a mount-mode agent and the server needs the
canonical path either way -- to translate, or to read the bytes from.

Concretely: on 2026-08-10 all three of windows-rtx2070's SMB mounts reported
Unavailable, because Windows drive mappings are per-logon-session and the session
an agent runs in is not the interactive one holding them. Under mount-only
transport that node is undispatchable however well its encoder works, and NVENC
there is confirmed working.

Older agents are unaffected: #[serde(default)] on the domain type and TM_MOUNT=0
on the wire, so an agent that never heard of transports keeps its behaviour. A
field that silently switched an existing agent to streaming would be the worst
way to ship this.

Both directions of the conversion boundary carry it, with a note at each. That
boundary is where the muxers field was dropped, and the failure was identical in
shape: an agent registering healthy and matching nothing. The round-trip test now
uses a streaming fixture, so dropping the field on the way out turns it red --
verified by doing exactly that.

Also fixes `agent survey` telling a streaming node it is broken: with no mounts
it printed "can be given no work", which sends an operator hunting a share
problem on a node deliberately not using one.

Streaming's byte-moving RPCs and the server-side install are the next two
changes; this is what they attach to. Workspace suite 517 -> 524.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U9keBRnGXBP2DiQmrcqm33
…eaming contract

The rule was already stated and already checked: transcodarr-agent must never
depend on transcodarr-store, because it "has to stay copyable to the Windows node
without dragging SQLite along", verified with cargo tree.

The crate honoured it. Every shipped artifact defeated it. There was one binary,
transcodarr, and it links transcodarr-server -> store -> SQLite, so the thing an
operator would actually copy to a worker node contained the whole orchestrator
and a C library it will never open. The check passed because it asked about the
dependency graph while the property that mattered was about the binary. That is
the fifth time in this project an invariant has held exactly where it was cheap
to measure and failed where it was needed.

transcodarr-agent is now its own binary: 0 sqlite crates against the CLI's 3,
19M against 38M. transcodarr keeps its agent verbs unchanged.

Also lands the streaming wire contract -- FetchSource, PushOutput, and FileChunk
with an explicit offset, an explicit last, and a content_sig on the final chunk.
Offset is carried rather than implied so a restarted stream is detectable instead
of appending into a corrupt file of exactly the right length.

Streaming does not work yet, and the server methods say so: they return
unimplemented with a reason rather than an empty stream or an
accepted-but-ignored push. Either of those would look like success and produce a
job reporting done having moved no bytes, which is this repository's most
frequent failure shape.

Fixes .gitignore silently swallowing the new binary. `bin/` matched
crates/transcodarr-agent/src/bin/, where Cargo keeps source, so the file was
invisible to git status and would not have been committed -- a fresh clone would
have built a workspace with no agent in it while every local check passed. This
is the second instance of exactly this defect; the first was 0001_initial.sql,
kept out of every commit by a global *.sql rule, which made main compile on one
machine and nowhere else. The fix is the same negation the repository already
carries for migrations, two lines below it.

KNOWN BAD, do not merge yet: `cargo test --workspace` hangs, locally and in CI.
It reproduces on this machine past 600s where the suite used to take 45s, so the
hung Test Rust on #78 was a real defect and not a stuck runner. Suspect the new
unimplemented stubs or the TransportMode change interacting with a test that
waits on a stream it now never receives. Find it before merging.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U9keBRnGXBP2DiQmrcqm33
@jdfalk
jdfalk merged commit 12d717d into main Aug 12, 2026
11 of 13 checks passed
@jdfalk
jdfalk deleted the feat/transport-mode branch August 12, 2026 02:19
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