Two fixes from the independent review of f043cd7.
RationalNs::checked_add cross-multiplied into u128 and then added without a check. Each
product fits, but their sum does not: two reduced fractions near the U64 maximum add to about
2^129, which panics in debug and wraps in release, after which the reduction returns a
confidently wrong rational. Every multiplication and the addition now go through a checked
path, as ipc-v1 section 2 requires, with a fixture row and a test that reaches the overflow
rather than the "does not fit U64" reduction limit.
The schema-set test checked fourteen hardcoded type names, so a new payload type could have
shipped outside contractDigest. It now iterates the readers own list, all fifty of them.
Also amends ipc-v1 section 5, dated: Worker.Acknowledge carries req-<U64> request ids, not
arbitrary Ids, because the serial watermark rule in that same section cannot reject reuse
after acknowledgment unless the acknowledged id carries its serial.
11 KiB
Session RPC contracts over Flybus
Status: draft 2, 2026-09-18. The filename is retained for existing links. This document now defines domain contracts carried by Flybus v1. It no longer defines a separate socket protocol, direct worker connections, or coordinator-owned buffer service. The architecture index states scope and precedence. Public feed/control v1 stay unchanged; this is the new internal session path.
1. One transport, domain-specific meaning
Every session/worker RPC is a Flybus call to a named, incarnation-pinned service. Pub/sub, application supervision and artifact bookkeeping use the same bus. The router moves messages; the receiver validates its method payload and the session step machine.
The bus owns framing, connection identity, route registration, bounded delivery and artifact ownership. This document owns Scope, model-related scalar types, worker capability negotiation, domain operation deduplication and errors. Domain request identity is independent of the bus callId: a safe retry has a new transport callId but the original domain requestId/body.
2. Common domain types
type Id = string; // ^[a-z0-9][a-z0-9._-]{0,63}$
type U64 = string; // "0" or [1-9][0-9]*; <= 18446744073709551615
type Digest = string; // 64 lowercase hexadecimal digits (SHA-256)
interface Scope { sessionId: Id; epoch: Id; step: U64 }
interface RationalNs { numerator: U64; denominator: U64 }
interface SchemaRef { id: Id; version: number; digest: Digest }
interface TypedValue { schema: SchemaRef; value: object }
interface SessionRpcRequest { requestId: Id; scope: Scope | null; params: object }
All fields are required unless marked ?. Schema version is integer 1..65535. Fractions
are reduced, denominators positive, durations positive; zero is encoded 0/1. Arithmetic is
checked. JSON numbers representing rates/rewards/controls are finite. Counters/clocks use
decimal strings. Task/profile schemas bound collections and numeric ranges before mutation.
First session composition limit: 4 agents, 4 ports and 64 rate roles per agent; these are
session/model limits, not limits on the number of application personas or generic bus clients.
Each TypedValue has a canonical JSON size limit of 32 KiB, while the complete envelope must still fit Flybus's 64-KiB maximum. Large typed state goes in a listed Artifact attachment under an explicit schema, not an oversized inline object. Changing the old draft's 1-MiB worker envelope to Flybus must not silently truncate a payload.
3. Request/reply mapping
Illustrative client call:
bus.call(
target = {service: "agent.fly-a", expectedIncarnation: pinnedRegistration},
method = "Agent.Prepare",
payload = {requestId: "req-41", scope: {sessionId, epoch, step: "41"}, params},
attachments = ownedArtifactHandles
)
Flybus's eventual rpc.result outcome is one of:
interface SessionRpcSuccess {
type: "result"; requestId: Id; workerId: Id; incarnationId: Id;
scope: Scope | null; result: object;
}
interface SessionRpcFailure {
type: "error"; requestId: Id; workerId: Id; incarnationId: Id;
scope: Scope | null;
error: { code: ErrorCode; message: string; mutation: "none" | "applied" | "unknown" };
}
Replies echo the original scope. The receiver identity and bus service incarnation must match the negotiated worker. Bus route/admission failure is not a SessionRpcFailure produced by the handler. A bus admission acknowledgment is not an Agent.Prepare/Environment.Advance completion. Only a matching terminal domain reply resolves a simulation phase.
ArtifactRefs inside request/result payloads must be declared in bus attachments and backed by live owned handles. Domain canonical-body digests include the references but exclude changing bus callIds, deliveryIds and owner tokens. A cached result owns Artifact handles independently of the first delivery; it is not a JSON object holding unowned pointers.
4. Worker negotiation and status
After bus connection/registration, call Worker.Hello (scope:null):
interface HelloParams {
sessionId: Id; expectedWorkerId: Id;
role: "agent" | "environment" | "coordinator";
supportedMajors: number[];
}
interface HelloResult {
selectedMajor: 1; selectedMinor: 0;
workerId: Id; incarnationId: Id; role: "agent" | "environment" | "coordinator";
buildDigest: Digest; contractDigest: Digest;
capabilities: Id[];
limits: { maxAgents: number; maxPorts: number };
}
The bus supplies caller identity; do not accept a forged caller in params. Bind a worker's session authority to the expected coordinator identity/incarnation during negotiation and initialization. Wrong worker/role, no common major or missing required capability refuses the composition. Required capabilities are agent-step-v1 and world-step-v1 for their roles; checkpoint-v1 and pixel-observation-v1 are conditional. Artifact transport capability is negotiated once by Flybus, not as another worker memory API.
Worker.Status has params {} and the caller's last known scope (null before initialization):
interface StatusResult {
state: "uninitialized" | "ready" | "preparing" | "prepared" | "advancing"
| "committing" | "capturing" | "staged-restore" | "restoring"
| "failed" | "stopping";
currentScope: Scope | null;
activeRequestId: Id | null; lastCompletedRequestId: Id | null;
lastBatchId: Id | null; progressCounter: U64;
}
ProgressCounter advances on computational/phase progress, not on status queries. Dispatch Status through the same service without waiting for a long numerical operation. One mutation executes at a time; at most one may be pending, and normal stepping pipelines neither. The router's larger RPC capacity is not permission to overlap worker mutations. Never hold a simulation lock while waiting for network I/O, artifact resolution or release bookkeeping.
5. Domain idempotency and retention
requestId is req- plus a canonical U64 serial, increasing for newly issued operations
per caller/worker pair. Retries reuse it unchanged even though bus callId changes. Flybus
preserves first-dispatch order per caller/service; worker handlers maintain that request
admission order while allowing read-only status alongside compute.
The operation key for step mutations is (sessionId, epoch, step, method, workerId).
There is at most one Prepare, Commit or Advance for that key.
- Same key/request/body returns its cached reply, with fresh bus delivery ownership over retained artifacts. It never repeats ticks, stimulation, controller execution or reward.
- Changed ID/body for an existing key is CONFLICT. Canonical comparison uses RFC 8785 over method, scope and validated params. A rejected duplicate does not undo the earlier result.
- Check retained request identity before phase checks or artifact dereferencing. A duplicate may arrive after the original input delivery was consumed; it needs only the cached result.
- Keep current/immediately previous step result records. Eviction never enables reexecution: highest-issued request serial and step watermarks reject expired retries/old steps.
- Original expired serial → RESULT_EXPIRED; fresh serial naming an old step → STALE_STEP.
An exact duplicate arriving while execution is active receives the terminal domain error IN_PROGRESS for that bus call. The original bus call still completes normally. Retry or query Status later; no second mutation is started. This avoids multiple terminal replies to one bus call and does not mistake IN_PROGRESS for the original operation's failure.
Lifecycle/capture replies are retained until Worker.Acknowledge:
params:{requestIds:Id[]} (1..16), result {acknowledged:Id[]}. It drops domain cache handles,
not another consumer's bus delivery. Already released/unknown IDs are ignored. Serial
watermarks reject reuse after acknowledgment without an unbounded tombstone list.
Amendment, 2026-09-22 (CONTRACT-01): those ids are domain request ids in the req-<U64>
serial form, not arbitrary Ids. The serial watermark rule in the sentence above cannot reject
reuse after acknowledgment unless the acknowledged id carries its serial, so a bus callId or a
bare Id is refused there.
Bound unacknowledged lifecycle replies at 16, then BUSY before application. Status and Acknowledge use a cache of their last 16 replies; current/previous step records have their separate finite retention. Caches containing big artifacts consume bus owner/byte budgets; configure capture limits consistently. Never evict a promised replay artifact but keep a successful pointer-only reply. An intentionally expired result returns RESULT_EXPIRED.
6. Timeout and failure handling
Timeouts are measured on the caller's monotonic clock. Prototype defaults: probe after two seconds without reply, fail after ten seconds without progress; long boot/capture have separate budgets. These are failure-detection values, not a gameplay latency goal.
After an uncertain call:
- Stop further world-step dispatch.
- If the same bus/service/worker incarnation still exists, query Status or issue a fresh bus call with the original domain requestId/body and retained input attachments.
- Resolve only a matching terminal result. Never repeat an Advance with a new domain ID.
- If routes/ownership were lost, incarnation changed or retained result expired, fail the epoch and restore/reset the group.
Endpoint crash or bus restart is not covered by in-memory deduplication. Router/store restart invalidates all transient artifacts and routes. Worker disconnect also invalidates its bus owners and registration; v1 does not silently reattach that worker to an active epoch. Recover coherently even if an OS process survived with some numerical state in memory.
7. Domain errors
| Code | Meaning |
|---|---|
| INVALID_ARGUMENT | Invalid schema/range, before mutation |
| UNSUPPORTED | Missing method/capability |
| IDENTITY_MISMATCH | Wrong session/profile/port/build/asset identity |
| STALE_EPOCH / STALE_STEP / FUTURE_STEP | Timeline/order mismatch |
| INVALID_PHASE | Wrong worker phase |
| CONFLICT | Existing logical operation with changed ID/body |
| IN_PROGRESS | Original operation still executing; duplicate bus call did not start work |
| BUSY | Domain capacity unavailable before admission |
| BUFFER_INVALID | Missing/unowned/mismatched artifact or invalid media shape |
| RESULT_EXPIRED | Safe replay is no longer available; never recompute to replace it |
| INCOMPATIBLE_STATE | Restore validation failed before activation |
| BACKEND_FAILURE / INTERNAL | Runtime fault, with explicit mutation certainty |
Messages are <=512 code points and exclude raw game memory/credentials. Errors after partial mutation use unknown unless completion is established. No error authorizes skipping a fly, pressing fallback controls, or continuing a partially committed match.
Worker.Shutdown, params {reason:Id}, returns {stopping:true} if responsive and terminates
the worker after replying. It does not imply saved state. Only configured supervisors may
invoke it; workers have no authority to shut down the coordinator. Shutdown/release notifications
travel on the same bus; there is no reverse lease socket or Buffer.Release/Reclaim RPC.