Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
1128140
fix(fleet): fan-in serves every listener — one connection per listene…
Oct 12, 2026
b6c66fb
test(fleet): closing one fan-in listener closes only its own arms (#1…
Oct 12, 2026
dc0c310
test(fleet): reconcile updates every live fan-in connection on its ca…
Oct 12, 2026
31db217
fix(fleet): cap concurrent fan-in listeners — refuse beyond it with a…
Oct 12, 2026
691a8af
test(fleet): two-listener harness — one local arm each, no re-open st…
Oct 12, 2026
bebeba8
test(fleet): #1264/#1601/#1617 hold per fan-in listener; retire singl…
Oct 12, 2026
d401ea0
feat(engine): stamp every global-stream frame with an <epoch>:<seq> i…
Oct 12, 2026
a4cb720
feat(engine): resume the global stream from a cursor via a shared rep…
Oct 12, 2026
a13c802
feat(engine): answer an unservable global-stream cursor with a reset …
Oct 12, 2026
d2f51c9
feat(engine): bound the global replay buffer by bytes and age, config…
Oct 12, 2026
13e59af
feat(engine): global-stream heartbeats carry server time and the curr…
Oct 12, 2026
0589f1f
fix(engine): release a global-stream subscription when its client abo…
Oct 12, 2026
647e138
test(engine): scratch-engine check of global-stream replay and abort …
Oct 12, 2026
c6039fe
refactor(engine): one release path for the global stream; log the dis…
Oct 12, 2026
46c208c
Merge #1772: fan-in serves every listener — a second view no longer e…
Oct 12, 2026
bf594cf
test(engine): deterministic global-stream harness — wildcard bind let…
Oct 12, 2026
30289ae
Merge #1776: engine sequenced, replayable global event stream — epoch…
Oct 12, 2026
dea6cd4
feat(engine): sync copies are opt-in per global-stream subscription; …
Oct 12, 2026
f2b1b25
feat(fleet): fan-in census line every 30 s — listeners, accepted/clos…
Oct 12, 2026
7062f98
test(fleet): census close reasons are the enumerated six; the real re…
Oct 12, 2026
5f8349f
test(fleet): census transitions log one line immediately with conn + …
Oct 12, 2026
c6c0e43
feat(fleet): log each ownership refresh's per-peer outcome, duration …
Oct 12, 2026
cb427c0
test(fleet): the census adds no frame and changes no delivered byte —…
Oct 12, 2026
66306c4
test(fleet): engine-shaped stream fixtures pinned to the engine wire;…
Oct 12, 2026
06eac85
feat(engine): summary diffs carry no patch bodies on the wire; the pe…
Oct 12, 2026
eb32273
test(fleet): fan-in suites feed engine-shaped frames — id-less, seque…
Oct 12, 2026
6fdcbb7
feat(engine): throttle shell output updates to <= 1 per 200 ms with a…
Oct 12, 2026
7d40fec
test(fleet): both fan-in boot branches log owner-map refresh outcomes…
Oct 12, 2026
7252573
test(engine): fixture-turn byte report — largest live frame under 256…
Oct 12, 2026
4f12b10
style(engine): prettier-format the #1778 tests (#1778)
Oct 12, 2026
de4002b
Merge #1771: fan-in census line + engine-shaped stream fixtures (S0 o…
Oct 12, 2026
8291932
Merge #1778: engine burst-volume cuts — opt-in sync copies, patches o…
Oct 12, 2026
925900e
fix(fleet): any data frame counts as delivered — a re-opened arm from…
Oct 12, 2026
7034b06
fix(fleet): a failed peer refresh keeps that peer's owner entries — r…
Oct 12, 2026
ac38b74
fix(fleet): fleet-of-one local-arm loss severs the listener with a tr…
Oct 12, 2026
71dd6d2
fix(fleet): past the retention window a peer's owner entries expire a…
Oct 12, 2026
8bdaa1a
test(fleet): a successful refresh replaces a peer's owner entries aut…
Oct 12, 2026
da2f958
fix(fleet): fan-in and proxies error-close a lost stream — only grace…
Oct 12, 2026
c10c399
fix(fleet): the arm reader treats a non-200 upstream as an error, nev…
Oct 12, 2026
7011432
fix(fleet): an unknown-owner session request is tried locally first; …
Oct 12, 2026
e5186c6
fix(fleet): the client treats an unexpected end of the global stream …
Oct 12, 2026
6668193
test(fleet): a just-created local session routes locally, and a remot…
Oct 12, 2026
6a13937
docs(fleet): fan-in comments name the #1773 sever path (#1773)
Oct 12, 2026
1c901c5
fix(fleet): the owner-map refresh line says what the map did with a f…
Oct 12, 2026
df193f3
refactor(fleet): an outcome-less owner-map update just forgets peer r…
Oct 12, 2026
bf64938
Merge #1773: every lost stretch of events forces a resync — no silent…
Oct 12, 2026
1491f3e
Merge #1774: ownership map keeps a peer's sessions across a failed re…
Oct 12, 2026
edfa917
test(fleet): installer write-mode runs reach launchctl only through a…
Oct 13, 2026
3a45963
fix(fleet): the installer generates one SSH forward agent per attache…
Oct 13, 2026
f342847
fix(fleet): a hand-installed peer forward is replaced with a backup, …
Oct 13, 2026
ac340bc
fix(fleet): a re-opened arm resumes from its last engine id — the ups…
Oct 13, 2026
0bb0c61
fix(fleet): the healthcheck checks every per-peer forward agent the w…
Oct 13, 2026
8d67baa
fix(fleet): the installer retires the session-wide multiplex-flag age…
Oct 13, 2026
c42fe5c
docs(fleet): the installer header and fleet README describe the per-p…
Oct 13, 2026
1b94618
fix(fleet): the client resumes from the SSE id and checks contiguity …
Oct 13, 2026
ca96336
test(fleet): an upstream reset on any arm is exactly one gap for its …
Oct 13, 2026
4089797
test(fleet): fleet-of-one over real sockets — across a link loss and …
Oct 13, 2026
9c97fb9
test(fleet): a reconnecting listener's last SSE id — bare engine id o…
Oct 13, 2026
56e0020
test(fleet): two-peer E2E on two scratch engines — killing the peer l…
Oct 13, 2026
e9756d6
docs(fleet): fan-in comments name the resume path and say what an ove…
Oct 13, 2026
3a6be6a
Merge #1775: installer-managed per-peer SSH forwards with standard ke…
Oct 13, 2026
8965094
Merge #1777: lossless resume end to end — relays and client resume fr…
Oct 13, 2026
22c4ad7
perf(fleet): the relay splits SSE frames in linear time — only the ne…
Oct 13, 2026
da83442
perf(app): the global-stream reader splits SSE frames in linear time …
Oct 13, 2026
3b901f0
fix(fleet): the relay ends a frame at a CRLF blank line too, split ac…
Oct 13, 2026
bffbf18
fix(app): the global-stream reader holds a chunk's trailing CR for th…
Oct 13, 2026
ca888d9
test(fleet): both readers' framing pinned unchanged — multi-line data…
Oct 13, 2026
bb11b8f
test(app): the reader test uses the SDK's typed retry knob, not an un…
Oct 13, 2026
aef761a
Merge #1779: linear-time SSE frame splitting in the relay and app rea…
Oct 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 56 additions & 14 deletions packages/app-bundle/manifest.json

Large diffs are not rendered by default.

151 changes: 94 additions & 57 deletions packages/app-bundle/overlay/packages/app/src/context/server-sdk.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import { createRefCountMap } from "@/utils/refcount"
import { useGlobal } from "./global"
import { ServerScope } from "@/utils/server-scope"
import { attachOwnerHeaderIfArmed } from "@/components/remote-create-arm"
import { parseCursorNamespaces, resolveEventOrigin } from "./sse-origin"
import { shouldReconnectIdleStream } from "./stream-liveness"
import { createStreamEndResync, shouldReconnectIdleStream } from "./stream-liveness"
import { applyStreamCursor, createStreamCursor } from "./stream-cursor"
import { detectServerProtocol, type ServerProtocol } from "@/utils/server-protocol"
import { createCompatibleApi, type CompatibleApi } from "@/utils/server-compat"

Expand Down Expand Up @@ -357,55 +357,43 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS
}
})()

// #1264 lossless reconnect: the frontdoor buffers recent events and
// replays the gap when the stream is re-opened with ?lastEventID=<id>.
// Track the last payload id seen (the server ids every event; id-less
// events just don't advance the cursor — replay may over-deliver, which
// the reconcile-based reducers tolerate by design).
// #1264 lossless reconnect: the engine (#1776) sequences its global stream
// and replays the gap when the stream is re-opened with ?lastEventID=<id>.
// #1777: the cursor is the SSE `id:` of the last applied frame — the engine's
// `<epoch>:<seq>`, or the fan-in's composite of every origin's position —
// never the JSON payload id (the engine rejects that as malformed, so every
// reconnect used to be a reset). It also checks contiguity per origin: a
// hole, an engine reset, or an unannounced restart forces a resync, and a
// replayed frame already applied is dropped (stream-cursor.ts).
// #1264: the cursor must survive RELOADS — module state died with every
// document, so a drop right after a reload reconnected cursor-less and
// the gap's events were lost (the harness caught it: the sse-gap flow
// failed only in the post-reload context). sessionStorage is per-tab,
// cheap, and exactly the lifetime the cursor wants.
const CURSOR_KEY = "amicode.sse.lastEventID"
let lastEventID: string | undefined
let storedCursor: string | undefined
try {
lastEventID = sessionStorage.getItem(CURSOR_KEY) ?? undefined
storedCursor = sessionStorage.getItem(CURSOR_KEY) ?? undefined
} catch {
/* private mode etc. — degrade to module state */
}
const trackEventID = (payload: unknown) => {
const id = (payload as { id?: unknown } | undefined)?.id
if (typeof id === "string" && id) {
lastEventID = id
try {
sessionStorage.setItem(CURSOR_KEY, id)
} catch {
/* best-effort */
}
const cursor = createStreamCursor(storedCursor)
const persistCursor = () => {
const current = cursor.current()
if (current === undefined || current === storedCursor) return
storedCursor = current
try {
sessionStorage.setItem(CURSOR_KEY, current)
} catch {
/* best-effort */
}
}
// #1539: track the parsed composite cursor to detect event origin (local vs
// remote peer). Initialized from the persisted cursor so the first event
// after reconnect diffs correctly.
let previousParsedCursor = parseCursorNamespaces(lastEventID)
const sseFetch: FetchLike = (input, init) => {
const base = eventFetch ?? globalThis.fetch
try {
if (lastEventID) {
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url
const u = new URL(url, server.http.url)
if (u.pathname === "/event" || u.pathname === "/global/event") {
u.searchParams.set("lastEventID", lastEventID)
const modified = u.toString()
if (typeof input === "string" || input instanceof URL) return base(modified, init)
return base(new Request(modified, input), init)
}
}
} catch {
/* fall through unmodified */
}
return base(input as Parameters<typeof fetch>[0], init as Parameters<typeof fetch>[1])
// #1777: the ONE carrier is `?lastEventID=` (the SDK's own Last-Event-ID
// header is stripped so two cursors can never disagree).
const applied = applyStreamCursor(input, init, server.http.url, cursor.current())
return base(applied.input as Parameters<typeof fetch>[0], applied.init as Parameters<typeof fetch>[1])
}
const eventApi = createApiForServer({ server: server.http, fetch: sseFetch as typeof fetch })
const eventSdk = createSdkForServer({
Expand Down Expand Up @@ -475,6 +463,28 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS
// nominally connected, so the disconnect→connect edge never fires and the rail
// would otherwise never self-heal a stranded view).
const [resyncCount, setResyncCount] = createSignal(0)
// #1617 / #1777: a FULL resync of the viewed state — the forced-resync signal
// (the transcript and the entity rail refetch, #1646) plus a synthetic GLOBAL
// `amicode.sync.gap` event, on which server-sync refetches the bootstrap
// unconditionally (past the #1289 `server.connected` debounce, which keys on
// a different type). Driven by a relay's gap frame, and by the cursor's
// verdicts: an engine reset, a sequence hole, an unannounced restart.
const forceResync = (namespaces: string[]) => {
setResyncCount((n) => n + 1)
if (
enqueueServerEvent(queue, {
directory: "global",
payload: { type: SYNC_GAP_TYPE, properties: { namespaces } } as unknown as ServerEvent,
})
)
schedule()
}
// #1773: an unexpected NORMAL end of the global stream is a loss (the relays
// never end a listener cleanly except on shutdown). The loop arms this latch
// on such an end; the reconnected stream's first frame advances resyncCount,
// so the viewed transcript and the entity rail refetch once the stream is live
// again. A user stop, a page hide, or the client's own abort arm nothing.
const streamEndResync = createStreamEndResync()

// #1584 client-side liveness watchdog. The v1 event iterator neither throws
// nor completes when the socket goes silently half-open (sleep/wake, Wi-Fi
Expand Down Expand Up @@ -545,9 +555,21 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS
error,
})
}
// #1777: the SSE id of the frame about to be yielded. The SDK reader
// calls onSseEvent with each parsed frame right before it yields that
// frame's data, so this is the id of the event the loop reads next.
let frameSseId: string | undefined
const events =
kind === "v1"
? (await eventSdk.global.event({ signal: currentAttempt.signal, onSseError })).stream
? (
await eventSdk.global.event({
signal: currentAttempt.signal,
onSseError,
onSseEvent: (frame: { id?: string }) => {
frameSseId = frame.id
},
})
).stream
: eventApi.event.subscribe({ signal: currentAttempt.signal })
if (!streamGeneration.accepts(active)) return
setStreamStatus("connected")
Expand All @@ -560,8 +582,11 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS
if (!streamGeneration.accepts(active)) return
streamErrorLogged = false
lastFrameAt = Date.now()
// #1773: the first frame after an unexpected end — the stream is live
// again, so force the resync now (the refetch cannot miss the events
// between the end and this re-subscription).
if (streamEndResync.frame()) setResyncCount((n) => n + 1)
const legacy = "payload" in event
if (legacy && event.payload.type === "sync") continue
const directory = legacy ? (event.directory ?? "global") : (event.location?.directory ?? "global")
const payload = legacy ? (event.payload as Event) : adaptServerEvent(event)
// #1522 (ADR 0033 decision A): the SSE fan-in aggregator emits
Expand Down Expand Up @@ -602,33 +627,45 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS
? (payload as Record<string, unknown>)
: ((payload as { properties?: Record<string, unknown> }).properties ?? {})
const gap = parseSyncGapFrame({ type: SYNC_GAP_TYPE, ...gapData })
// #1777: the named origins resync, so their next positions are new
// baselines (not a second "hole" against the lost stretch).
cursor.gap(gap?.namespaces ?? [])
// Bump the forced-resync signal so the entity rail self-heals even
// though the socket never crossed a disconnect→connect edge.
setResyncCount((n) => n + 1)
if (enqueueServerEvent(queue, {
directory: "global",
payload: { type: SYNC_GAP_TYPE, properties: { namespaces: gap?.namespaces ?? [] } } as unknown as ServerEvent,
}))
schedule()
forceResync(gap?.namespaces ?? [])
continue
}
trackEventID(legacy ? event.payload : (event as { id?: string }))
// #1539: resolve origin from the composite cursor diff. Fleet-of-one
// (bare scalar id) returns undefined immediately — zero overhead.
const sseId: string | undefined = legacy
? (event.payload as { id?: string }).id
: (event as { id?: string }).id
const resolved = resolveEventOrigin(sseId, previousParsedCursor)
previousParsedCursor = resolved.cursor
const taggedPayload: ServerEvent = resolved.origin !== undefined
? { ...payload, origin: resolved.origin }
// #1777: the cursor and the contiguity check, on the frame's SSE id
// (v1; the v2 stream at /api/event carries no resume cursor). A hole,
// a reset or an unannounced engine restart owes a full resync; a
// replayed frame already applied is dropped, never applied twice.
const verdict = cursor.frame({
id: kind === "v1" ? frameSseId : undefined,
type: legacy ? event.payload.type : payload.type,
})
if (verdict.resync) forceResync(verdict.resync.namespaces)
if (!verdict.apply) continue
persistCursor()
// a sync copy still moves the cursor (it holds a seq), but is no event
if (legacy && event.payload.type === "sync") continue
// #1539: the origin is the composite cursor's moving namespace.
// Fleet-of-one (a bare engine id) is always local.
const taggedPayload: ServerEvent = verdict.origin !== undefined
? { ...payload, origin: verdict.origin }
: payload
if (enqueueServerEvent(queue, { directory, payload: taggedPayload })) schedule()

if (Date.now() - yielded < STREAM_YIELD_MS) continue
yielded = Date.now()
await wait(0)
}
// #1773: the iterator COMPLETED — a clean end of the global stream. If
// the client neither aborted this attempt nor stopped the loop, it is
// an unexpected end: a loss. Arm the resync for the reconnected stream.
streamEndResync.ended({
attemptAborted: currentAttempt.signal.aborted,
stopped: abort.signal.aborted || !started || !streamGeneration.accepts(active),
})
} catch (error) {
if (streamGeneration.accepts(active) && !isStreamClosed(error, currentAttempt.signal))
setStreamStatus("disconnected")
Expand Down Expand Up @@ -658,8 +695,8 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS
}

const clearGlobalStreamCursor = () => {
lastEventID = undefined
previousParsedCursor = {}
cursor.clear()
storedCursor = undefined
try {
sessionStorage.removeItem(CURSOR_KEY)
} catch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,37 @@ describe("#1617 — gap interception is wired at both client seams", () => {
expect(syncSource).toMatch(/server\.connected"\s*&&\s*Date\.now\(\)\s*-\s*lastConnectedQueueAt/)
})
})

// #1773 — an unexpected NORMAL end of the global stream is a gap too (defense in
// depth: kept even once replay exists). The relay chain now error-closes every
// loss, but a clean end the client did not ask for still means events may have
// been dropped. The decision (`createStreamEndResync`, stream-liveness.ts) is
// unit-tested there; these pins assert the loop actually wires it — falsifiable:
// deleting either call, or reporting the end without the abort/stop state, reds.
describe("#1773 — an unexpected end of the global stream advances the forced-resync signal", () => {
const sdkSource = readFileSync(join(import.meta.dir, "server-sdk.tsx"), "utf8")
const loop = sdkSource.indexOf("for await (const event of events)")
const loopCatch = sdkSource.indexOf("} catch (error) {", loop)

test("the loop's NORMAL completion (the iterator ended without throwing) reports the end", () => {
expect(loop).toBeGreaterThan(0)
const body = sdkSource.slice(loop, loopCatch)
// after the for-await body's last statement and before the catch: the normal-end path
const afterLoop = body.slice(body.lastIndexOf("await wait(0)"))
expect(afterLoop).toContain("streamEndResync.ended(")
})

test("a user stop / page hide / the client's own abort are NOT a loss: the end carries both states", () => {
const body = sdkSource.slice(loop, loopCatch)
expect(body).toMatch(/attemptAborted:\s*currentAttempt\.signal\.aborted/)
// stopped = the root abort (teardown), the started latch (stop()/pagehide), the generation fence (reset)
expect(body).toMatch(/stopped:\s*abort\.signal\.aborted\s*\|\|\s*!started\s*\|\|\s*!streamGeneration\.accepts\(active\)/)
})

test("the reconnected stream's first frame advances the forced-resync signal (the transcript + entity rail refetch)", () => {
const body = sdkSource.slice(loop, loopCatch)
expect(body).toMatch(/if \(streamEndResync\.frame\(\)\) setResyncCount\(\(n\) => n \+ 1\)/)
// the signal the transcript (pages/session.tsx) and the entity rail read
expect(sdkSource).toMatch(/resyncCount,\s*\n/)
})
})
Loading
Loading