flybrain/packages/session-types
acamilo 77c8ee4558 session: resolve uncertain calls, and measure each mode in its own process
Review fixes for SESSION-02.

An expired caller deadline was becoming a failed epoch without the
ipc-v1 section 6 resolution. That procedure existed and was correct and
had exactly one caller, a test injection, so the deadline this slice
introduced bypassed it and a merely slow participant lost its epoch.
Deadlines is now the two-stage shape section 6 describes -- a probe,
then a bounded resolve budget and attempt count -- call_owned returns a
typed CallOutcome so an expiry is distinguishable from a refusal, and
Prepare, Commit, Advance and the lifecycle calls all query the same
request id against the same incarnation before the epoch can fail. This
is also step-v1 section 7's Advance row, which was imperative about it.

The coordinator peak-RSS column was measuring the measuring process.
VmHWM never falls and every row shared one process, so the column was
cumulative and the mode ranking reversed when the rows were reordered.
Each row now runs in a measure-row child of its own. The corrected
numbers say the opposite of what the first report claimed: the
coordinator's own peak is roughly flat across the modes and lowest in
process mode, and the cost of the split is the children.

workers-v1 section 2 bounded Agent.Initialize's workerThreads by
"within launcher allocation" and named no wire for it. Dated amendment:
HelloResult.limits gains workerThreads, the worker reports what its
launcher gave it, and the launcher refuses one that disagrees. The
schema set, the shared fixtures and the TypeScript package move
together; contractDigest changes, which ipc-v1 section 4 provides for.

Also: the stale-epoch row now reaches the stale-epoch path against a
live agent process and asserts exact codes on both halves; the
router-restart row asserts the handle drop it claimed; frames are
counted from the behaviour trace instead of calculated; the README says
which suites run over which transports; bootstrap is fence-guarded; the
shutdown reason is an Id rather than a silent fallback; and
agent_mutations returns None rather than zero where the counter lives
in another process.
2026-09-22 15:41:42 +00:00
..
src session: resolve uncertain calls, and measure each mode in its own process 2026-09-22 15:41:42 +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.