flybrain/packages/session-types
dev 6655a1b1c6 session: coherent all-participant checkpoint and recovery
STATE-01 over the FLYSESS1 envelope CONTRACT-01 specified.

fly-session gains a `state` module: the durable store with its generations, its
rotation and the commit order of checkpoint-envelope-v1 section 5, where the store
manifest rename is the durable commit point; a compatibility block whose comparison
names the identity that differs rather than one opaque digest; and a bounded writer
that owns its payload handles until the bytes are committed or the job fails.

The writer's queue slot is taken before the first State.Capture, so a saturated
writer refuses a capture rather than queueing it without bound, and the refusal is a
BUSY the stepping session survives. Capture and durability are two events: a capture
completes when an immutable capture exists, and only the store manifest rename moves
the durable mark. A lost save reply is an outcome, and the resolution asks the store
about the same checkpoint instead of saving again.

Both worker roles implement State.Capture, State.StageRestore and
State.ActivateRestore, with once-only restore tokens bound to checkpoint, scope,
payload and incarnation. A restore selects a complete compatible generation, imports
every payload as a fresh artifact, stages the group, validates the coordinator's own
ledgers, and only then activates; a failure anywhere leaves the fence closed and
records every participant that staged as one that must be replaced. The fence lifts
at Failed -> Restoring(k) -> Paused(k) and nowhere else.

The task and the action executor gain the capture/validate_restore/install_restore
interfaces workers-v1 section 4 lists, and the ledger can re-derive the event
identities it issued under another epoch, which is what lets a resumed run's
behaviour trace be compared with an uninterrupted one.

media: check_required_audio now takes the observation's provenance instead of
exempting boundary 0. A chunk is the audio of an interval, and the observation
ActivateRestore installs covers none.

checkpoint-envelope-v1 section 3 gains a dated amendment adding `environment` to the
manifest, the holder of the world's own payload, which the table named for every
other participant; `helperState`, which that table already listed, joins the
required-field set in Rust and TypeScript. The fixture was regenerated by the
existing example; the schema set and contractDigest are unchanged.

state-media-v1 section 5 gains a dated amendment for three readings this slice
enforces: the State RPCs' compatibilityDigest is the participant's, not the
manifest's composition-level block; a restored observation carries no audio chunk;
and a participant that staged into an abandoned install must be replaced.
2026-09-22 17:43:43 +00:00
..
src session: coherent all-participant checkpoint and recovery 2026-09-22 17:43:43 +00:00
tests feat(session-types): the TypeScript half of the contracts, over the same fixtures 2026-09-22 11:54:13 +00:00
package.json feat(session-types): the TypeScript half of the contracts, over the same fixtures 2026-09-22 11:54:13 +00:00
README.md feat(session-types): the TypeScript half of the contracts, over the same fixtures 2026-09-22 11:54:13 +00:00
tsconfig.json feat(session-types): the TypeScript half of the contracts, over the same fixtures 2026-09-22 11:54:13 +00:00

@flybrain/session-types

The session framework contracts in TypeScript: types, validation, canonical JSON (RFC 8785) and canonical digests.

The other half of services/flysim/crates/fly-session-types. Same rules, same canonical bytes, same digests, and the same fixture corpus: this package loads the crate's fixtures/ directory rather than keeping a copy, so a case written once holds both languages to it. Nothing here opens a socket; it reads, validates and hashes.

This is the internal session path (docs/design/session-framework/). The public feed and control contracts are unchanged and still live in @flybrain/feed.

Modules

Module Contents
canonical canonicalize, digestOf, parseStrict, requireEnvelopeFit, rejectBusIdentities
scalar Id, U64, Digest, Scope, RationalNs with checked arithmetic, and the four identities as branded types
reader Reader, which reads one object field by field and then refuses any field it did not read
common readScope, readSchemaRef, readTypedValue, operationKeyDigest, bodyDigest
media View and audio descriptors and refs, and the State.* payloads
workers The closed enums and every Agent/Environment/Worker method payload
rpc SessionRpcRequest, the success and failure replies, ErrorCode, MutationCertainty
publishing SessionDescriptor, CommittedSnapshot
trace The step-v1 section 8 record and the behaviour-only comparator
seed seed-derivation-v1
checkpoint The FLYSESS1 envelope layout
fixtures Loading the shared corpus

Reading a payload

Every reader takes unknown, validates, and hands back a value whose fields are exactly the ones it read. A payload with an unknown or misspelled field fails instead of silently defaulting, and a round trip through a reader is the test that no field is dropped.

import { canonicalize, digestOf, readScope, readPrepareParams, bodyDigest } from '@flybrain/session-types';

const scope = readScope(payload.scope);
const params = readPrepareParams(payload.params);
const digest = bodyDigest('Agent.Prepare', scope, params); // the ipc-v1 section 5 comparison

Rules that need another value in hand are separate functions, because a payload cannot check them alone: validatePortControlAgainst, validateBatch, validateSensoryInputAgainst, validateObservationAgainst, validateStepResultAgainst, validateSnapshotAgainst, validateTelemetryRoles, validateRemainder, validateCommitAgainstScope.

Canonical JSON

Three rules make the two implementations agree byte for byte:

  • object keys sort by UTF-16 code unit, which is what comparing JavaScript strings does;
  • numbers print with String(number), the ECMAScript algorithm RFC 8785 requires;
  • a number is canonicalizable when it is finite and, if integral, no larger in magnitude than Number.MAX_SAFE_INTEGER. Larger integers are refused rather than rounded: every counter and clock in these contracts is a U64 decimal string. The rule is on the value, not on how it was written, because JSON.parse cannot tell 1e21 from the same digits written out.

parseStrict is a small recursive-descent parser rather than a wrapper around JSON.parse, which keeps the last of two duplicate keys instead of failing.

Digests use node:crypto. This package is contract tooling for services and tests, not browser code; the presentation layer consumes the public feed package instead.

Tests

npm test --workspace @flybrain/session-types
npm run typecheck --workspace @flybrain/session-types

Nine files, all fixture-driven. The one that says the most about the two implementations is in tests/checkpoint.test.ts: a FLYSESS1 envelope written here is byte-identical to the one the Rust crate wrote into the fixture.