flybrain/docs/design/session-framework/ipc-v1.md
acamilo 05e93a95ef
Some checks failed
ci / node 22 (test + typecheck) (push) Has been cancelled
ci / rust stable (cargo test --workspace --release) (push) Has been cancelled
ci / infra/tests/lint.sh (push) Has been cancelled
ci / playwright apps/stage (allowed to fail) (push) Has been cancelled
docs(design): session-framework specs (bus-v1, ipc-v1, step-v1, workers-v1, publishing-v1, state-media-v1) and the MaleCNS modular-session plans
2026-09-22 11:01:55 +00:00

207 lines
11 KiB
Markdown

# 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](bus-v1.md)**. It no longer defines a
separate socket protocol, direct worker connections, or coordinator-owned buffer service.
The [architecture index](README.md) 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](step-v1.md).
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
```ts
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:
```text
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:
```ts
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`):
```ts
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):
```ts
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.
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:
1. Stop further world-step dispatch.
2. 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.
3. Resolve only a matching terminal result. Never repeat an Advance with a new domain ID.
4. 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.