Skip to content

Refactor session into runtime, log, fold, reconcile, and wire - #48

Open
andreisavu wants to merge 21 commits into
mainfrom
claude/implement-attached-plan-mse2st
Open

Refactor session into runtime, log, fold, reconcile, and wire#48
andreisavu wants to merge 21 commits into
mainfrom
claude/implement-attached-plan-mse2st

Conversation

@andreisavu

@andreisavu andreisavu commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

This refactor splits the monolithic session.ts into focused modules, laid out in layers the toolchain holds, that separate concerns and enable room resumption over a persisted log.

Summary

The room now owns no state in memory. Every fact about the room — the roster, the exchange, the people, the leases, the wakes pending — is a pure fold over the log. A room that resumes over the log continues where the last run stopped. The refactor also moves module-level globals into a Runtime value that a host owns, so two hosts in one process do not share a model call, a session repository, or a register of running rooms.

Layout

packages/ambion/src is laid out in layers, and an import points down only. Biome refuses every other import, one noRestrictedImports override per layer (docs/toolchain.md §1):

  • types, wire, define, render — the vocabulary
  • host/ — the runtime value, a clock, an opener, a SQLite storage
  • log/ — one serial queue over a Pi session
  • room/ — every fact and decision, pure over the log
  • tools/ — the workspace and its backends
  • seat/ — one activation, the hands it holds, the actor, the in-process transport
  • session.ts — the room, which composes them all

Key changes

  • host/runtime.ts (new): Holds what a host owns: a clock, a model call, a Pi session opener, and a register of running rooms. startSession, readSession, and defineWorkspace take a Runtime parameter that defaults to defaultRuntime. A runtime holds a transport only when its host gave it one; the room defaults to every seat as an actor in its own process.

  • host/sqlite.ts (new): Pi's SessionStorage over any SQLite a host reaches through two calls, run and all. A process wraps node:sqlite; the Cloudflare package wraps ctx.storage.sql. Every scenario, the restart suite and the widened chaos sweep run on memory, JSONL and SQLite.

  • log/log.ts (new): The log commits one entry at a time on a serial queue. A repeated key lands once. A commit the record moved past is refused. Nothing observes a message before its write is confirmed. An append that fails puts the log in doubt, and the log reads the storage at once. A checkpoint replaces every row before it, and the log drops those rows from memory.

  • room/fold.ts (new): Every fact about the room is a pure function over the log entries: the roster, the exchange, the people, the leases, the wakes pending, and the summaries owed. checkpointOf writes the rows a later fold still reads, behind a floor below which every wake was answered.

  • room/reconcile.ts (new): decide is pure. It reads the folded state and the clock and returns the rows to write, the wakes to send, the attempts to abandon at the cap, and when to look again. The room applies a decision, and a second decision over the result writes nothing.

  • wire.ts (new): What crosses between a seat and its room, and what the log holds beside a message. Every shape is plain JSON. A seat reaches the room through three calls: view, commit, lease. The room reaches a seat through two: wake, and cut for an activation whose lease the room ended.

  • room/lease.ts (new): Activations named by what caused them. An activation's id is derived from the log, so a wake is safe to send twice, a retried commit lands once, and a request from an activation whose lease ended is refused. Every lease row carries heard. A wake is answered by a lease of the seat that heard it and ran to its end, or that spoke. A lease that expired or failed without speaking answers nothing: the room wakes the seat again after the backoff. No lease runs past runtime.wake.deadline from its claim. At the cap the room writes the attempt it does not make as a lease abandoned, and the host hears an abandoned event.

  • room/view.ts (new): What an activation is given, read off the fold and rendered.

  • seat/seat.ts and seat/hands.ts (refactored): SeatActor takes a wake, claims the lease, reads the room's view, builds the Pi Agent over it with the hands the view names, runs it, renews the lease while it runs, and releases the lease when it stops. It runs one activation at a time, queues every wake that lands meanwhile, cuts an activation the room ended and moves on even when the run ignores the abort. hands.ts holds say, summarise and seat.

  • session.ts (refactored): Composes the log, fold, reconcile, and wire. It holds no state in memory. An evicted room closes its log and reaches no listener again.

  • room/assistant.ts (refactored): Down to what the assistant is: the refusal of one with hands, and the threshold a summary is written above. Who is owed is a fold over the log. A draft the host revoked stands down.

  • room/presence.ts, seat/activation.ts (refactored), record.ts (removed).

  • cloudflare/ (new): A room as Cloudflare Durable Objects. One object holds the room, one object holds each seat. The log lives in the room object's SQLite storage through the core's storage. The alarm is the room's clock. RPC is the wire, with cut over it.

  • Tests: restart.test.ts, property.test.ts, scenarios.ts, invariants.ts, log.test.ts, reconcile.test.ts, lease.test.ts, wire.test.ts, runtime.test.ts, matrix.test.ts, seat.test.ts, checkpoint.test.ts, and live/resume.test.ts prove the room resumes over its log on every storage, the record keeps its shape under a random walk, an activation is cut at its deadline, the cap is written, and the fold over a checkpointed log equals the fold over every row.

  • Chaos tier (chaos.test.ts): One scenario runs once to count the appends its log takes, then once per append, crashing the room at that append before the entry lands and again after it landed with the confirmation lost. The same scenario runs in a child process on JSONL storage and is killed mid-activation. The random walk fails writes and crashes the room up to three times. pnpm chaos widens all three to JSONL and SQLite.

  • Review fixes (ef252c1): A review of the whole branch found three faults and three edges, fixed with regression tests: a revoked draft that kept the summary owed for ever, a seat actor that could run two activations at once, a refused start that kept the name, a delivery directed at the assistant, and a commit from a dead lease answered missed before stale.

  • Demo and report: demos/2026-09-09-the-room-comes-back.html is the report of a run on a real model that drops the runtime mid-exchange and resumes it over the same log.

https://claude.ai/code/session_017i49PfakG27M56FjqvyG1e

A host owns a Runtime: the clock, the session opener, the model call, the
rooms that run and the workspace names that are taken. startSession,
readSession and defineWorkspace take one and default to defaultRuntime.
Every Date call in src routes through the runtime's clock, and the view a
seat reads carries the time it was built at.

The harness gains a fake clock, the two storages (memory and JSONL over a
temporary directory), the two workspace backends, a faulty opener, the
shared invariants, and four scenarios that matrix.test.ts runs on both
storages. runtime.test.ts proves two runtimes share nothing and that a
room on JSONL reads back through a second runtime.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017i49PfakG27M56FjqvyG1e
The record becomes a log. RoomLog.commit runs the key check, the
readThrough check, the append and the cache update inside one link of a
promise chain, and what the room does with a fresh message runs there
too. A message exists when its write is confirmed: a write that fails
leaves nothing on the record, nothing on the stream, and wakes nobody.
A repeated key hands back the message the first commit landed.

A seat's say and the assistant's two tools take Pi's tool call id as the
key; a delivery takes the key the host passes, or a fresh one. The
composition is checked against the replayed record, so a name the record
knows as a person is refused at the first call, and a visit is checked
after the replay.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017i49PfakG27M56FjqvyG1e
…cile

The room is now one operation and one step. A commit builds its message
where the write happens, with the seats it wakes written on the message,
and sends once the write is confirmed. Reconcile folds the log, decides,
writes what it decided and sends; it runs after every commit, every lease
change, every alarm and every wake, and running it twice writes nothing.

Every fact about the room is a fold over the log: the roster from the
composition row and the seatings after it, the people from arrivals and
departures, the open exchange from the last close row, the leases, the
wakes still pending and the summaries still owed. Attendance, Exchanges
and the assistant's in-memory scheduler are gone.

An activation's id is derived from the log, and it holds a lease with an
expiry. A seat reaches its room through three JSON calls (view, commit,
lease) and the room reaches a seat through two (wake, steer), so a seat
and a room can live in two processes. A lost wake is sent again after the
resend window, a lost release expires on the room's alarm, a lost steer is
read off the record at the next pass, and a request under a lease that
ended is refused as stale. A failed draft retries after a backoff, up to
three attempts, on the room's own alarm.

resumeSession brings a name back over its log and continues a mid-exchange
room; runtime.evict drops one from memory and writes nothing. stopSession
revokes the leases in flight and writes left for everyone present; the
next run writes its own composition row. readSession folds the roster.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017i49PfakG27M56FjqvyG1e
packages/cloudflare runs a room as Durable Objects: one object per room,
one per seat, the log in the room object's SQLite, the alarm as the
room's clock, and RPC as the wire. SqliteSessionStorage implements what
Pi's Session reaches for appendCustomEntry, appendMessage and findEntries
and refuses the rest. RoomObject resumes the room its storage names and
exposes the host's verbs and the seat's three calls. SeatObject stores a
wake, runs the activation on its alarm, and can be put on hold.

The package is private and nothing deploys it. Its three tests run inside
workerd through @cloudflare/vitest-pool-workers, as part of turbo test.
The pool's own workerd binary arrives as a platform package, so no
install script is allowed. SeatActor gains a public run(id) for a host
that runs a seat inside one request.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017i49PfakG27M56FjqvyG1e
property.test.ts walks the room under a seeded random sequence of visits,
departures, deliveries with repeated keys, host seatings, time moving,
transport faults and one crash with a resume, and holds the record to the
invariants at the end. Two hundred seeds found two defects, both fixed
here: a wake sent to a seat the host unseated stayed pending for ever,
and the room's own memory of sent wakes did not read the roster.

resume.test.ts in the live tier resumes a room mid-exchange on a real
model. view.ts holds what an activation is given, off the fold. The
toolchain contract records the test inputs, the live table gains the
resume test, and the planning files close items 1, 3, 18 and 19 and add
what this change deferred.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017i49PfakG27M56FjqvyG1e
…try a silent activation again

A live run showed the gap. The routing wrote only the idle seats on the
message, and a seat at work heard the same message through a steer that
existed nowhere on the log. A crash lost the steer, the lease expired,
and the message never reached the seat in the resumed room.

Now `wakes` names every seat the message reaches: the idle seats its
reach wakes, and every seat at work. The seat side decides what reaching
it means: a fresh activation, or a steer into the one that runs. Every
lease row carries `heard`, the seq the activation has taken, and a wake
is answered by any lease of the seat that heard it and ran to a release,
a refusal or a revocation, or that spoke. A lease that expired or failed
without speaking answers nothing: the wake stays pending, the failure
counts as one attempt, and the room wakes the seat again after the
backoff, up to the cap. The summaries used this policy already; the
wakes use the same one, and `runtime.retry` holds it for both.

The `steer` call leaves the wire: a wake carries the line a running
activation is steered with. `abort()` also writes off the wakes still
pending, so nothing a seat was sent runs after the cut. `evict()` closes
the log, so a write the dead run still had in flight fails the way a
process that died fails. The assistant hears nothing by being at work: a
composing activation decides on the question as asked, and a drafting
one learns what landed from the refusal of its draft.

The live resume test crashes the room on the first say, whichever seat
makes it, and the seats are told to answer even when a colleague did.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017i49PfakG27M56FjqvyG1e
The evidence that the log is the truth is a crash at every write. One
scenario runs once to count the appends its log takes, then once per
append, crashing the room at that append: before the entry lands, and
again after it landed and before the room heard. A world resumes the name
in a fresh runtime, puts back the people who were present, and retries
the host action that failed under the same key. Every run must come to
the same record. The same scenario runs in a child process on a JSONL
storage and is killed mid-activation, and the random walk now fails a
write before or after it lands and crashes the room up to three times.
`pnpm chaos` widens all three.

The sweep found three faults, and each one has a fix and a test:

- A write that lands while its confirmation is lost stayed invisible
  until the next write. The log now reads the storage at once, on the
  queue behind the failed write, tells the room what it found, and the
  room emits and routes for it as for a write it confirmed. A read of
  the record waits for the queue.
- A reconcile pass whose write the storage refused dropped the alarm, so
  a lease that had run out was never ended. The pass now ends there and
  the room looks again after the resend window.
- A visit whose arrival write failed left the room holding a handle for
  the person, so the next visit wrote no arrival and the person spoke
  without ever arriving. The room forgets the visit when the arrival
  fails.

The invariants count what a resumed room inherited: the leases live at
its resume end in this run, and an open exchange closes in it, with no
start and no open of their own.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017i49PfakG27M56FjqvyG1e
…omes back

The demo drops the runtime that holds the room as the first answer to
Sam's question lands, and resumes the name in a second runtime over the
same log. The run's JSON carries the crash point and the room's own log,
and the report reads the leases the dead run held, when they expired, and
the wakes the resumed run sent again off those rows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017i49PfakG27M56FjqvyG1e
On a storage on disk, a release the actor sends after its script ends
lands after real I/O. The fake clock fired the renewal at half the expiry
and the expiry itself back to back, with only event-loop turns between,
so on JSONL the lease expired before its release landed, and the room
then waited on retry backoffs the test never moved past. The clock now
yields a few milliseconds, several times, after every alarm it fires.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017i49PfakG27M56FjqvyG1e

Copy link
Copy Markdown
Contributor Author

Live tests on anthropic/claude-sonnet-5 is red on ed04f13, and the cause is the key, not the diff. Every model call in the job returned 400 invalid_request_error: Your credit balance is too low to access the Anthropic API. Each live activation errored on that, the rooms retried after the backoff, and the tests hit their deadlines or their error caps. The same error hit every file in the tier, including ones this branch did not change.

No fix exists in the repository for this: the key CI uses needs credits. A re-run before that would fail identically, so I am not spending it. Once the key has credits, a re-run of the job on this head is the check. The scripted tier is green on this head: lint and types, CLI smoke, and the tests on Node 22 and Node 24.


Generated by Claude Code

A dead process emits nothing. The seat side of a run that was dropped
still ran to its next room call, heard stale, aborted, and reported the
abort through the dead session's listeners into the host's stream. A
demo run counted those aborts beside the leases that expired. Eviction
now clears the listeners with the alarm and the log.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017i49PfakG27M56FjqvyG1e
A refused model call is an error, and the room now wakes the seat again
after the backoff, up to the cap. The test runs with no wait between
attempts, and holds the room to three errors, three activations that
left no mark, and an exchange that closed at the cap.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017i49PfakG27M56FjqvyG1e
One live run on a real model: the runtime that holds the room is dropped
as the first answer to Sam's question lands, and a second runtime resumes
the name over the same log. The report shows the six leases the dead run
held, what each had heard, when they expired on the resumed room's alarm,
the five wakes it sent again, and the one message the exchange closed
into, with the crash inside the range it covers. The demos README carries
the row and what the run showed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017i49PfakG27M56FjqvyG1e
…a name a refused start took

The review of the branch found three faults and three edges, and this
commit fixes them.

A draft lease that ended revoked was neither an attempt nor a judgment,
so the summary stayed owed under an id the revoked row had taken. The
room sent the same draft wake for ever, and `quiet()` never resolved
after an abort mid-draft or a stop and resume mid-draft. A revoked draft
now stands down, the way a revoked wake is written off.

The seat actor cleared `current` before it awaited the release, so a
wake that landed during the release started a second activation beside
the one still releasing, and the queue held one id. The actor now holds
`current` through the release, queues every wake in order once each,
and `run()` resolves once the queue is drained. The Cloudflare seat
object awaits that, so its alarm no longer clears the storage under a
queued activation.

A start whose composition the record refuses now frees the name. A
delivery directed at a seat that wakes for nothing said is refused. A
commit from a lease that ended is answered stale before it enters the
queue. `routing` returns a set and builds the at-work seats once. The
dead `joinLater` loop is gone.

The backlog gains item 33: opening a name that does not exist creates
it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017i49PfakG27M56FjqvyG1e
…ation runs

The README named a `steer` call the wire no longer has. A wake into a
running activation is handed to the actor, which steers the message in.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017i49PfakG27M56FjqvyG1e
… wire, and move the SQLite storage into the core

Four changes to the room's durability, and one move that lets a host
over any SQLite run it.

The core owns its SQLite storage: `sqliteSessions(sql)` is Pi's
`SessionStorage` over two calls, `run` and `all`, that a host wraps its
driver in. The Cloudflare package wraps `ctx.storage.sql`; the test
matrix wraps `node:sqlite`, so every scenario, the restart suite and the
widened chaos sweep now run on memory, JSONL and SQLite.

No lease runs past `runtime.wake.deadline` from its claim: the room caps
every claim and renewal there, so an activation that runs on expires on
the room's alarm and counts as an attempt. The seat actor cuts the
activation when its lease reaches the deadline, and a run that ignores
the abort is left behind: the actor moves on, and every call the run
still makes is answered stale. A lost renewal leaves the lease to expire
where it stands, and the actor cuts the activation there.

The room reaches a seat through two calls: `wake`, and `cut` for an
activation whose lease the room ended. The room talks to ports alone;
the seat object takes `cut` over RPC.

At the cap the room gives up in writing: the fold reports every wake
still pending and every draft still owed with its attempts, `decide`
ends the attempt the room does not make as a lease `abandoned`, the row
answers the wake or the close, and the host hears an `abandoned` event
that names it.

A checkpoint bounds what a fold costs. Every `runtime.checkpoint.rows`
rows the room writes an `ambion/checkpoint`: the composition, the closes
and the leases a later fold still reads, behind a floor below which every
wake was answered. The fold reads it in place of every row before it,
and the log drops those rows from memory. The rows stay on the storage,
and a checkpoint the room cannot read is ignored. The restart suite runs
over a log checkpointed every three rows; a test proves the fold over
the compacted log equals the fold over every row on all three storages.

Backlog 26 closes; 28 keeps the host verb.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017i49PfakG27M56FjqvyG1e
`packages/ambion/src` is laid out in layers, and an import points down
only: the vocabulary (`types`, `wire`, `define`, `render`), then `host/`
(the runtime value and the SQLite storage), `log/` (the queue), `room/`
(every fact and decision, pure over the log), `tools/` (the workspace),
`seat/` (one activation, the hands it holds, the actor, the in-process
transport), and `session.ts`, which composes them all. Biome refuses every
other import, one `noRestrictedImports` override per layer; the core
imports no platform module, and every other package reaches the core
through `@ambionframework/ambion`.

Three moves made the direction hold. The host contracts a seat needs,
`Clock`, `SessionOpener` and `ModelResolver`, are vocabulary, and the
seat context carries a clock and a catalog instead of a runtime. The
in-process transport is the seat side's, and a runtime holds a transport
only when its host gave it one: the room defaults the rest. The hands a
seat holds, `say`, `summarise` and `seat`, are one module beside the
actor, so `room/assistant.ts` is down to what the assistant is, and the
room's fold imports no tool. `persistTurns` belongs to the activation,
the exchange shapes to the vocabulary, and `BUILTIN_TOOL_NAMES` to
`types.ts`, which closes backlog item 5.

`docs/toolchain.md` §1 names the layers and what each may import.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017i49PfakG27M56FjqvyG1e
A read in doubt started at the replay's end, whatever the log had taken
since, so every failed write cost a read of every entry appended in the
process's life, and the ids kept to tell a found entry from a cached one
grew with them. The cursor now moves to the last entry every read saw,
the read asks Pi for the entries past it, oldest first, and the ids are
cleared once a read is over: a second doubt costs the entries since the
first.

The SQLite storage reads a cursor against the order, the way Pi's own
storages do.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017i49PfakG27M56FjqvyG1e
SPLIT_PLAN.md names fifteen PRs that each leave main green and reach
the same tree: the scope of each, the files to take from the final
tree, what to trim, and which branch commits cherry-pick cleanly.
The layout lands first, so every later PR puts a file in its place
once. The file is deleted with the last PR of the stack.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017i49PfakG27M56FjqvyG1e
andreisavu pushed a commit that referenced this pull request Sep 9, 2026
The record becomes a log. log/log.ts replaces record.ts. RoomLog.commit
runs the key check, the readThrough check, the append and the cache
update inside one link of a promise chain, and what the room does with a
fresh message runs there too. A message exists when its write is
confirmed: a write that fails leaves nothing on the record, nothing on
the stream, and wakes nobody. A repeated key hands back the message the
first commit landed. The entries on the storage do not change: messages
only.

A seat's say and the assistant's two tools take Pi's tool call id as
the key; a delivery takes the key the host passes, or a fresh one. The
composition is checked against the replayed record, so a name the record
knows as a person is refused at the first call, and a visit is checked
after the replay.

The test storage regains the faulty opener, which the log's fourth test
needs. The layer table gains the log/ row.

This is PR 2 of the stack SPLIT_PLAN.md cuts from #48. It carries
0f44d6c from that branch, with the path change to log/log.ts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJjjR3S6iwqEbDXsRDJgiq
andreisavu added a commit that referenced this pull request Sep 9, 2026
The record becomes a log. log/log.ts replaces record.ts. RoomLog.commit
runs the key check, the readThrough check, the append and the cache
update inside one link of a promise chain, and what the room does with a
fresh message runs there too. A message exists when its write is
confirmed: a write that fails leaves nothing on the record, nothing on
the stream, and wakes nobody. A repeated key hands back the message the
first commit landed. The entries on the storage do not change: messages
only.

A seat's say and the assistant's two tools take Pi's tool call id as
the key; a delivery takes the key the host passes, or a fresh one. The
composition is checked against the replayed record, so a name the record
knows as a person is refused at the first call, and a visit is checked
after the replay.

PR 2 of the stack SPLIT_PLAN.md cuts from #48.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJjjR3S6iwqEbDXsRDJgiq
Main now holds the runtime value, the layered layout, the harness and the
serial log queue, cut from this branch as the first two PRs of the split
plan. Every conflict resolves to this branch: main's version of each file
is an earlier state of it. Two references are corrected on the way: the
Biome comment points at toolchain.md §1, and backlog items 1, 4 and 7
name `host/runtime.ts`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017i49PfakG27M56FjqvyG1e
andreisavu added a commit that referenced this pull request Sep 10, 2026
The seat side becomes a client of the room. wire.ts names what crosses:
the seat calls view, commit and lease; the room calls wake, which
carries the line a running activation is steered with when a message
caused it. Every shape is plain JSON, and survives a round trip through
JSON.stringify unchanged.

seat/activation.ts is one activation over a view. seat/hands.ts holds
say, summarise and seat. seat/seat.ts holds the routing rule, SeatActor
and inProcessTransport. The actor takes a wake, claims the lease, reads
the view, builds the Pi Agent with the hands the view names, runs it,
renews the lease while it runs, and releases it when it stops. A steer
that lands before the model has been asked is held until it is, so a
steer follows the request it lands during and never joins it. A steer
still queued when a run ends says the record moved.

room/lease.ts derives an activation's id from the record. room/assistant.ts
keeps what the assistant is, and what the room holds of it while it runs.
session.ts answers the three calls from a lease table it keeps in memory.
A seat is live from the wake that was sent until that lease ends. A
seating changes the roster where the message lands, once per key.

The runtime gains Transport and the optional transport option. The
scenarios run over a transport that serializes every request and
response. seat.test.ts drives the actor by hand over a room the test plays.

PR 3 of the stack SPLIT_PLAN.md cuts from #48.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJjjR3S6iwqEbDXsRDJgiq
Main now holds the seat side behind three JSON calls, cut from this
branch as the third PR of the split plan. Every conflict resolves to this
branch: main's version of each file is an earlier state of it. Backlog
item 13 now names `routing` in `session.ts` and `handsFor` in
`seat/hands.ts`, where they live.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017i49PfakG27M56FjqvyG1e
The worker sets the resend window to 50 ms. A runner whose seat alarm
claims the lease later than that is woken again, so the first assertion
holds for at least one wake. The lease rows and the one message the
seat said still prove that one activation ran.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017i49PfakG27M56FjqvyG1e
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.

2 participants