Skip to content

Fleet Studio slice 3 — session multiplexer + SSE relay (interact as if local) #1440

Closed as not planned

Description

@jeonghun-jj-lee

Important

Problem — Even with a fleet-wide list, opening a session on another machine today swaps the whole window (re-attach + full reload, fleet-manager.test.ts:224) and only one machine's traffic is live at a time (all engine//amicode/* traffic routes to one upstream chosen by a single global attachment pointer, fleet-manager-tab.tsx:111 / server.ts:279-408). You cannot stream a solve on machine B while prompting a session on machine A.
Approach — A session-aware multiplexing proxy + SSE relay in the local service routes each session-scoped request — messages, prompt, tool-permission responses, the engine file API, and the SSE subscription — to that session's owner, and merges peers' event streams into the one stream the app reads. This inverts the single-pointer/full-reload model: it creates a per-session owner binding (none exists today) and retires the single attachment pointer. The app keeps a single origin.
Scope — in: the new per-session owner binding + owner-routing key (net-new plumbing — sessions carry no owner today); the multiplexing proxy for session-scoped requests; the SSE merge with new per-event session_id attribution + a per-peer reconnect cursor set; demolition of the single-pointer/full-reload attach path. out: the focused-machine sidebar (slice 4); provenance UI + Preview remote edit (slice 5).
Assumptions — Peer RPC is authorized by the slice-1 credential; the fan-out (slice 2) supplies the session→machine map; the engine file API is the same write plane the agent already uses.

Acceptance Criteria

  • Owner-routing key (new plumbing): a per-session owner binding is created and threaded to the request layer (today only a single global attachment pointer exists). A path-less engine file.write/file.read for a session carries its owner machine_id (from the Work Column's bound session); the resolver routes on that key, and a request with no key resolves local (fail-safe), never to an arbitrary attached target. Test: two remote sessions (owners A, B) each write; each write lands on its own owner; a keyless request resolves local.
  • Per-verb routing, no reload — opening a session owned by a different machine routes each of {stream messages, submit prompt, run tools, answer permission prompts} to the owner, asserted per verb; and no full-window reload / no attach-swap command is dispatched (the current attach path does a full reload — this AC guards its removal).
  • Concurrent + un-crossed (deterministic, not latency): hold machine B's SSE stub stream open/blocked; drive a prompt to a session on machine A concurrently; assert A's first event resolves while B is still pending (then release B) — i.e. no head-of-line blocking, asserted by ordering not by a timing threshold. Assert every merged SSE event carries the originating session_id matching its source (no A/B cross-attribution).
  • The app holds a single origin throughout (structural guard: no second base-URL, no origin-switch call).
  • /amicode/fleet/* and posture requests are served locally and never routed to a peer (extend the existing honesty-surface guard, fleet_client_relay.test.ts:291).

Testing Decisions

Genuinely new surface — the session→machine multiplexer routing and the merged SSE relay. The merged/global event stream emits id: undefined and carries no session_id today (out of scope in #1264) — emitting per-event session_id + a per-peer cursor set is new engine surface; build a new merge harness, do NOT lean on the per-session /api/session/{id}/event?after= seam. Concurrency test uses the held-open-stub pattern from fleet_sse_resume.test.ts (block B, drive A, assert A resolves first, release B). The owner-routing test asserts two remote writes land on distinct owners and a keyless request resolves local. Split the compound "opening a remote session…" AC into per-verb routing assertions + a "no reload / no attach-swap dispatched" guard (host: amicode-session-relay.integration.test.ts). Extend fleet_client_relay.test.ts:291 for the honesty-surface-never-proxied half. Reuse the slice-2 machine-keyed map as the resolver's input.

Key Decisions

  • Multi-target, both live via a session-aware multiplexer + SSE relay; per-session routing retires the single attachment pointer — this is in-scope demolition, not an increment on the pointer model (ADR 0031 §D1).
  • Owner-routing key for path-less engine file requests: the owner machine_id from the Work Column's bound session; keyless ⇒ local (ADR 0031 §D1a).
  • Single app origin is purchased by the multiplexer's per-session routing (ADR 0031 §D1a; ADR 0027 single-origin).

Data Contracts

  • Session→machine map + owner-routing key: session-pathed requests resolve the owner from the path id (miss → on-demand fan-out refresh); path-less engine file requests carry the owner machine_id as a mandatory routing header; absent it ⇒ resolve local. The per-session owner binding is created by this slice (no Session.Info owner field; slice 2's tag is a list overlay, not a request-layer binding).
  • SSE relay cursor: N merged upstreams need a per-peer cursor set (or a service-side re-sequenced monotonic id space) — a single scalar lastEventID cannot address N independent peer positions on reconnect.

Constraints & Invariants

  • No silent local fallback: an unreachable owner surfaces the honest degraded outcome, never a fabricated success or a local read.
  • The local honesty surface never proxies (ADR 0027 §4).
  • Single origin throughout — no native multi-origin switch.

Prior Art

The merged-projection map (slice 2); the single global attachment pointer + full-reload attach path being retired (fleet-manager-tab.tsx:111, server.ts:279-408, fleet-manager.test.ts:224); the fleet transport resolver; the engine file API and the per-session SSE resume path (fleet_sse_resume.test.ts); the merged/global stream that emits no session_id today (#1264); the Work Column bound-session surface (app CONTEXT.md — a named surface, but no session→owner binding in code yet); fleet_client_relay.test.ts:291 (honesty-surface-never-proxied).

Source

Part of #1436. ADR 0031 §D1, §D1a. Blocked by #1439 (needs the fan-out session→machine map). Heaviest UI slice — it inverts the attach model and adds new engine SSE surface.

Activity

  1. jeonghun-jj-lee commented on Sep 23, 2026

    @jeonghun-jj-lee
    ContributorAuthor

    Closed as a partial Fleet Studio foundation: #1455 landed reusable pieces, but the remaining acceptance is superseded by #1482 and #1488 under #1475. This is not a claim that the original end-to-end feature is complete.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    afkImplementable without human interaction

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions