Open
Conversation
Forks examples/multi-agent-chat into examples/deterministic-peer-loop and implements the §7.2 cycle from the deterministic peer-loop specification: Taras gate (timebox in optional mode), control read, stop-before-pause, speaker override with same-cycle clear, default alternation, single peer step per cycle, independent TurnEntry + PeerRecord persistence, and requested-effect disposition with the policy module. Wires Opus (Claude) and GPT (Codex) peers through @tisyn/claude-code and @tisyn/codex in capability-restricted posture, adds the operator control panel (paused / stopRequested / nextSpeakerOverride) with owner/observer WebSocket hydration, and lands the scripted Core conformance harness covering 10 of 14 DPL-* categories (22 tests).
BrowserSessionManager previously tracked a single socket, so non-owner browsers received a one-time hydrate and no further pushes. Split the socket state into an owner reference plus an observer set, and fan out loadChat, showMessage, setReadOnly, and control updates to every connected client. Elicit prompts remain owner-only.
The host emits showMessage frames as { type, speaker, content }, matching
HostShowMessageSchema. The React client was reading msg.entry, so every
peer turn produced an undefined transcript row. Read speaker and content
directly from the frame.
The UI sends nextSpeakerOverride: null when the operator picks "auto", but the patch schema derived from Type.Partial(LoopControl) only accepted undefined. Define BrowserControlPatchSchema explicitly so null is valid, and normalize it to "field absent" when merging into the stored control record. LoopControlSchema itself stays strict.
Remove the promise-backed EffectsProcessor agent and route each requested effect through the planned per-effect surface: Policy.decide selects a disposition and, for executed entries, EffectHandler.invoke runs the handler. Per-effect EffectRequestRecord rows now land as distinct appendEffectRequest calls. The authored workflow iterates via a stateful EffectsQueue helper (seed / shift) so the drain runs in a single non-nested while loop, and the dispatch steps live in exported sub-workflows because the compiler rejects nested while and try inside a loop body. EffectHandler returns an InvokeOutcome union instead of throwing so the workflow captures handler errors without a try/catch. Conformance tests switch from an effectsScript batch to policyScript plus dispatchScript entries, exercising all four dispositions end to end through the new agents.
hydrateObserver only sent setReadOnly when a terminal reason was already stored, so observers attaching mid-loop saw "connected" status and a writable-looking control panel even though their messages are dropped. Send the "Session owned by another browser" banner on every observer attach, and keep the existing fallback that prefers a stored terminal reason (done / stopped) when the workflow has already ended.
The harness imported Val from both @tisyn/ir and src/schemas.js, which tsc rejects as a duplicate identifier on a clean build. Drop the @tisyn/ir import — every use in this file is the local schemas.js Val type.
Observers entered read-only on attach but became writable again as soon as the server broadcast showMessage or loadChat, because useChat stored the read-only reason in the same `status` slot that live activity updates overwrote. Give read-only its own state cell (`readOnlyReason`) so once set it persists for the session, and derive the control panel `disabled` prop and banner text from it instead of `status.level`.
Lint was failing on the PR branch:
- unicorn/no-useless-fallback-in-spread on `...(initial ?? {})`.
- Four curly violations on single-statement if/for bodies.
Also apply oxfmt to the example so `pnpm run format:check` is clean.
Behavior unchanged; this is purely lint and formatter conformance.
PR #120 removed the `{ input: ... }` wrapper for single-parameter ambient agent calls. The conformance harness was still destructuring `{ input }` from every payload and re-wrapping args in the operations log, which broke every captured call shape under the new contract. Destructure the payload directly and record it verbatim so the harness matches what the runtime now dispatches.
6afe602 to
b8299fc
Compare
The ambient declaration `takeTurn(input: { input: PeerTurnInput })`
was an artifact of the pre-PR-#120 wrapper style. Under the direct
payload contract it lowered to `operation<{ input: PeerTurnInput },
PeerTurnResult>` — an awkward single-key wrapper with no remaining
purpose. Drop the wrapper so the payload is `PeerTurnInput` directly.
Updates the declaration, the OperationSpec in peers/agents.ts, the
workflow call sites, the Opus/GPT bindings, and the harness
OperationCall type in lock-step.
The workflow constructed every peer TurnEntry with `usage: result.usage` unconditionally. When a peer returned no usage, this produced a present-but-undefined `usage` key that `TurnEntrySchema` rejected (`Expected object`), crashing store validation on the first turn. Select between two TurnEntry shapes with a truthy-check ternary so the key is absent when usage is missing and included verbatim when present. TurnEntrySchema is unchanged. Adds PER-07 covering the omission path (including the published `showMessage` frame) alongside the existing PER-06 pass-through test.
Replaces the file-backed DB agent and parallel JSON store with a stateless Projection reducer agent. Application-level state (transcript, LoopControl, peer records, effect-request records, readOnlyReason) lives in workflow locals and is rebuilt on restart via runtime replay of journaled Projection-op return values. The App surface collapses per-message fan-out (showMessage, loadChat, readControl, setReadOnly) into a single per-iteration hydrate snapshot and a blocking-pull nextControlPatch for browser-origin control updates. The workflow drains queued control patches inline via timebox(0, App.nextControlPatch) immediately after the Taras gate so every stop/pause/override check observes all patches the browser has sent during the iteration. A new peerLoop return value (FinalSnapshot) and a host-side post-execute publish step in main.ts guarantee browser hydration on terminal replay-only restarts when execute() returns without any live dispatch. Extracts runDescriptorLocally from the tsn run CLI path so the example's main.ts can keep the WebSocket server alive past the workflow's return. Rebaselines the conformance harness and 27 tests against the new OperationCall surface, adds a 9-test projection-reducer unit suite, and adds DPL-JNL-01/02 replay tests asserting journal-driven reconstruction and post-frontier hydrate dispatch. Updates the specification (§4.1 App, §4.2 Projection, §6.10 LOOP-PERSIST-1, §7.1/§7.2 initial state and cycle body, §11 rejected alternatives) and the test plan (DPL-CTRL/OVR/DONE/PER/INIT/RPL rewrites, new DPL-JNL category, fixture and OperationCall schema updates).
CI fails with ERR_PNPM_OUTDATED_LOCKFILE because the previous commit moved @tisyn/cli from devDependencies to dependencies in the example's package.json (main.ts now imports runDescriptorLocally at runtime) without regenerating the lockfile.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 2 of the Tisyn Deterministic Peer Loop: a single reference-MVP implementation landed as a private example package (
@tisyn/example-deterministic-peer-loop). Forksexamples/multi-agent-chatrather than modifying it.timeboxin optional mode), in-cycleApp.readControl(), stop-before-pause, speaker override consumed + cleared viaDB.writeControlsame cycle, default alternation, at-most-one peer step per cycle, independentTurnEntry+PeerRecordpersistence,App.showMessagefor every persisted message, andrequestedEffectsdisposition via the policy module.@tisyn/claude-codeand@tisyn/codexin capability-restricted posture, with a freshCodeAgentsession per turn. Peer wrappers parse strict-JSONPeerTurnResultthrough the TypeBoxPeerTurnResultSchema— no raw prose leaks into control flow.controlSnapshoton reconnect,updateControlis owner-only.test/conformance/) covering 10 of 14 DPL-* categories with 22 tests. Harness capturesOperationCallat the agent-dispatch boundary — never asserts on internalRecursiveState.src/schemas.ts) is the source of truth for persisted records, the store document, browser protocol messages, andPeerTurnResult. TS types are single-sourced viaStatic<typeof Schema>..changeset/config.json(privatePackages.version = false), so no changeset.Scope
tisyn.config.tsinput/includekeys inherited from the fork), live-adapter smoke tests (env-gated), and additional Core coverage for GATE / RPL / TYPE / CAP categories toward the 64-test target.Test plan
pnpm -C examples/deterministic-peer-loop build:workflow— generates workflow IRpnpm -C examples/deterministic-peer-loop build:node— node bundle compilespnpm -C examples/deterministic-peer-loop build:browser— browser bundle builds (417 kB / 123 kB gzip)pnpm -C examples/deterministic-peer-loop test— 26/26 tests pass (22 conformance + 4 unit)pnpm -w test— no regression (2093+ tests across 22 packages/examples pass;examples/multi-agent-chatunchanged)pnpm -C examples/deterministic-peer-loop test:browser— pending in follow-up (config needsrootsmigration)