You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
SSE event stream startup is blocked behind /project/current and /path metadata fetches #189
On a slow or flaky network, the app can sit with no live events — no session status, no streaming, stale list — for up to ~30 seconds after launch, even though the server is reachable. In the worst case (a metadata request that hangs until timeout), the event stream doesn't start until two unrelated requests have given up.
Cause
loadConnections() in src/stores/connections.ts builds the API client, then awaitsproject.current() and path.get() before committing the client to the store:
Everything downstream of startup keys off client appearing in the store — app/_layout.tsx starts the SSE stream and the catalog load in an effect watching client. So the entire live pipeline is serialized behind two metadata requests that it doesn't need: neither the project name nor the server's home path is required to stream events. Each request is capped at REQUEST_TIMEOUT_MS (30s), so a hanging response delays live events by up to that long — on exactly the networks where prompt (re)connection matters most.
This is separate from #186 (dead-stream detection): #186 covers a stream that dies after starting; this covers the stream never being asked to start.
Fix
Commit the client to the store immediately after building it, then let the metadata fetch fill in currentProject/serverHome behind it — guarded so a stale response can't clobber state after the user switches servers mid-fetch. Fix included in #182.
Fixed by the merge of #182 into the Qiiks/opencode-mobile maintenance branch (maint/v0.4.16) — shipping in release v0.4.16-maint.1 (APK will be attached there). Upstream review of #182 is still recommended; this merge keeps the fork usable while it waits.
Claimed: Ember — dev-workflow lead for the #189/#186 SSE fix. Branch: fix/sse-startup-recovery-189-186.
Scope: (1) #189 — commit the API client to the store before the /project/current and /path metadata fetches so the SSE stream can start immediately; (2) #186 — bounded SSE liveness detection and prompt reconnect after network/foreground transitions.
Noted: PR #182 claims this fix but is unmergeable (dirty, 49 files, last updated Aug 23) and spans other leads' work, so this task ships a scoped fix against current main, reusing #182's verified approach where it applies. Investigating first; fix lands via PR with typecheck + full test suite before merge.
Context pack — SSE #189 / #186 (maintained by the lead; artifacts win over this pack)
Stage: 5 + 5.5 DONE (merged 3d7728c, TDD updated). Lead: Ember — replacement dev-workflow lead (continuation claim 6858676393 on #189, pointer 6858676876 on #186), branch fix/sse-startup-recovery-189-186, worktree ~/workspace/opencode-mobile-sse-189-186. Claim: comment 6855126991. Work Log:https://app.notion.com/p/417ac25eb49f815aa751fea1b7345222 (Done 2026-11-14). Covers #186 too — see the pointer comment there. Project TDD/PRD exist (TDD §5 updated at Stage 5.5); the design gap below is the bug-lane design record.
SSE event stream startup is blocked behind /project/current and /path metadata fetches #189 still open, verbatim.src/stores/connections.ts:140-148 awaits project.current() + path.get() before the single set() that publishes the client (:151-160). REQUEST_TIMEOUT_MS = 30_000 (src/lib/sdk.ts:218); the two fetches run in parallel, so worst case is ~30s. app/_layout.tsx:140-144 starts SSE + catalog only when client appears. Second gate the issue does not mention: loadConnections() also awaits resolveApi()/probe first (:131-132, 8s probe timeout src/lib/sdk.ts:772).
Sweep (pattern-expressible, bounded to three files)
Method: code search rate-limited mid-sweep, redone against a full tarball of e8444931 (541 blobs) with local grep -rn. Hits: project.current() — connections.ts:141, 203, 269, 350, 380 (only :380 refreshProject() is a standalone refresh; the other four await metadata before a set() that publishes the client: loadConnections, addConnection, setActiveConnection, updateConnection). path.get() — connections.ts:142, 204, 270, 351 (same four sites). connected: true — single site events.ts:198. reader.read() — sdk.ts:326 (guarded) and sdk-v1.ts:291 (unbounded). NetInfo — zero hits repo-wide. transport — zero hits. reconnectNow — events.ts:56,512 + _layout.tsx:133 only.
Design gap (recorded before the fix starts)
There is no single owner of "SSE liveness" as a state machine. connected is a boolean set at dial time, not derived from transport evidence; the idle watchdog (sdk), the backoff timer (events), and the foreground resume (_layout) were added at different times with no shared transport state; the UI reads a different store (or nothing) than the transport; and metadata fetch and client publication share one set(), so unrelated data gates the transport. The v1/v2 split duplicated the SSE loop, so a fix applied to one silently missed the other. Any fix that patches one site without a shared transport state and a single resume entry point will re-create this drift.
v1 gets the same 45s idle-timeout wrapper as v2 (judgment call; v1 heartbeat interval unknown — a silent stream still reconnects within 45s).
Network recovery: prefer an Expo-SDK dependency already available (expo-network) if it covers the listener; add @react-native-community/netinfo only if it cannot, with the cause recorded (AC 2 requires prompt reconnect on network restoration).
fix: recover SSE promptly after mobile network transitions #186's "unit coverage" criterion conflicts with the standing no-unit-tests rule — the standing rule wins. Extend existing integration/e2e only; the mock server needs a v2 /api/event endpoint first (part of this task). No *.test.ts today imports src/stores/events; src/lib/sse.test.ts covers only the SSEParser.
npm run lint does not exist (package.json scripts: start, ios, android, typecheck, check:versions, test) — that criterion is void. Gate is check:versions + typecheck + FULL test suite.
CUA/device-smoke criterion is MOOT (CUA test removed 2026-11-13 by owner order). Merge validation = CI green + full suite + typecheck; emulator airplane-mode check if an emulator is available, else a recorded residual.
reconnectNow() tearing down a live stream on every foreground is a bug — fix with dedupe (no-op when live or in flight).
Generation route: Google AI Studio (browser) is not available to a background subagent — the implementer authors the patch directly from this brief + file excerpts; verification is the gate + Stage 4 review.
Selected/configured distinguished from verified SSE-live. 2. Foreground AND network restoration trigger prompt, deduplicated reconnect. 3. Silent/half-open stream detected within a bounded interval and reconnected. 4. Backoff resets only on demonstrated transport stability (event/heartbeat). 5. SSE startup not blocked by metadata fetches. 6. Reconnects do not duplicate subscriptions/event handling. 7. check:versions + typecheck + FULL test suite pass. 8. No new unit/mock tests; integration/e2e extended only if coverage is missing.
Diff: connections.ts — all four actions publish the client immediately, metadata behind via shared fetchMetadataBehind() with the get().client === requestClient stale guard; 8s probe kept. events.ts — transport: idle|connecting|live, connected only on first event, 10s timer gone, backoff resets on first event, reconnectNow() deduped (token-guarded flag). sdk-v1.ts — same 45s idle-timeout wrapper as v2. Network listener via expo-network ~8.0.8 (SDK 54, npx expo install; no NetInfo added; listener consults isConnected only). Dot reads transport (green only when live, amber otherwise). Mock server gains v2 GET /api/event; existing mock-server integration test extended (+2 tests, no new test files).
Gate (worktree, sequential): check:versions pass, typecheck pass, full suite 382 pass, 0 fail. Test-count delta +2 (380 → 382, verified against a stashed clean-main baseline that also passed 380/380).
Files touched: app/(tabs)/index.tsx, package-lock.json, package.json, src/lib/sdk-v1.ts, src/lib/session-list.integration.test.ts, src/stores/connections.ts, src/stores/events.ts, tests/fixtures/mock-opencode-server.ts. No app.json/build.gradle change — applicationId untouched. No keyboard/auto-scroll scope.
CI on the head (lead readback): test pass, gitleaks pass; Typecheck and unit tests, build, activation-e2e still in progress at review time. Merge waits on CI green.
Residuals: no emulator was available to the implementer, so no device/airplane-mode check ran; real Wi-Fi↔cellular handover still unvalidated. First-event/dedupe logic has no direct test (cannot run under node --test — zustand + react-native AppState); coverage is the transport-level integration test + typecheck + review. v1 heartbeat interval and v1 prod usage still unknown.
Process note: the claude-code-review skill file referenced by dev-workflow (~/workspace/custom-skills/claude-code-review/SKILL.md) does not exist on this VM — Stage 4 runs on the method as specified inline in dev-workflow §Stage 4 (parallel compliance + bug agents, high-signal only, independent validation per finding).
Leftover: an unapproved permission request from the implementer (Expo dev server connection to cdp.expo.dev, requested 14:10 UTC) — moot, the implementer finished without it; no action.
Stage 4 — compliance review: PASS (2026-11-13)
All 8 acceptance criteria verified against the diff; gates re-run standalone in the worktree (check:versions pass, typecheck pass, full suite 382 pass / 0 fail — matches the implementer exactly). Sweep re-run: zero unfixed hits (project.current()/path.get() only inside fetchMetadataBehind + standalone refreshProject(); connected:true single site inside the first-event branch; both reader.read() sites inside idle-timeout wrappers; NetInfo zero hits; STABLE_CONNECTION_MS zero hits). Standing rules verified: applicationId untouched, no keyboard/auto-scroll scope, test delta +2 AC-mapped, no new test files. Lead decisions 1–9 all honored. PR body accurate on every checkable claim. Filed notes (not fails): first-event/dedupe logic untested directly; real handover unvalidated; v1 heartbeat/usage unknown; 380 baseline not re-run by the reviewer (delta established from the diff + 382 count).
CI on head 9b42be6 (lead readback 2026-11-13): Typecheck and unit tests PASS, test PASS, gitleaks PASS; Unsigned iOS Simulator build, activation-e2e, build still pending. Merge waits on CI green.
Bug review still running at this write; Stage 4 closes only when both verdicts are in.
Two validated HIGH findings (both verified by the lead against the code in the worktree before acting):
Healthy but idle server never goes live: the only path to live is the first yielded event (events.ts:287-290), but on a quiet server nothing is yielded — v2 drops server.connected in the translator (v2-shape.ts:583-589), v1'"'"'s subscribe frame is a comment, heartbeats never surface as yielded events. Dot amber forever, connected false, backoff never resets, resync never runs. Fix: plumb byte-level liveness up; liveness must be observable at the events-generator level so the integration test can assert it on a quiet stream.
Hung SSE dial parks attemptInFlight forever: dial (sdk.ts:366, sdk-v1.ts:305) has no timeout; 45s watchdog only wraps reader.read() after headers. A black-holed dial never unwinds, the finally never runs, reconnectNow() no-ops forever. Fix: bound the dial (preferred) or let reconnectNow() abort-and-restart a stale connecting attempt.
Also in the fix pass: MEDIUM 3 (last-event timestamp so live can go stale; network-restore must rescue stale-live), MEDIUM 4 (the +2 tests pass vacuously re liveness — add a test that fails on Finding 1), LOW 5 (connected:false on connect start) and LOW 6 (breadcrumb timing) if cheap. Filed notes (not fails, no scope expansion): v1 idle-loop risk if real v1 servers send no heartbeats; refreshProject unconditional set and removeConnection stale metadata are pre-existing.
CI on head 9b42be6 (lead readback 2026-11-13): Typecheck and unit tests PASS, test PASS, gitleaks PASS; Unsigned iOS Simulator build, activation-e2e, build still pending at that time.
Stage 4 fix pass — done, new head 2ba4e92 (verified by lead readback 2026-11-13)
PR fix: SSE startup + liveness (#189, #186) #237 head now 2ba4e92150b1c301fd19f693013333cd8d57afcf, commit "fix: SSE quiet-server liveness + bounded dial", 10 files, +632/−118. Base main unmoved at e8444931. CI on the new head: Typecheck and unit tests PASS, test PASS, gitleaks PASS; iOS sim build, activation-e2e, build pending.
Finding 1: both generators yield {type: "sse.liveness", properties: {}} when a chunk carries no session event (sdk.ts:439, sdk-v1.ts:370); events.ts:302-324 records lastByteAt on any yielded event, flips live + resets backoff + runs the resync on quiet reconnects too. Liveness observable at the client.global.events generator level.
Finding 2: dial bounded by a 15s timeout controller in both generators (sdk.ts:371-388, sdk-v1.ts:308-325, throws SSE dial timed out); reconnectNow() aborts-and-restarts an attempt parked past the dial bound with no byte (events.ts:591-592, attemptStartedAt/lastByteAt at :230-231).
Finding 3: live with no byte for 30s is stale and gets rescued (events.ts:587-590); a fresh live stream still no-ops. LOW 5 fixed (connected: false on re-entry, :236). LOW 6 fixed (breadcrumb at publish time, connections.ts:298). refreshProject/removeConnection left untouched as instructed; network-listener unsubscribe handle considered and not added (no teardown site); abort-unwind answer recorded in the PR body.
Tests: +3 in the existing integration test file only (quiet v2 → only sse.liveness; quiet v1 → same; lifecycle still flows through the generator). Fail-before proven: with old generators stashed, all 3 new tests fail, 5 old pass; after the fix all 8 pass in the file. Note: the test file registers an inline node:module resolve hook redirecting expo/fetch to global fetch (expo/fetch has no resolver outside Metro); mock.module was tried first and is unavailable without --experimental-test-module-mocks.
All three FAIL findings plus LOW 5 and LOW 6 verified fixed against the delta 9b42be60..2ba4e92. Finding 1: both generators yield liveness on heartbeat-only chunks; store flips live + resets backoff + resyncs on liveness, before the session switch; sawLiveness is per-attempt; no real event type collides with "sse.liveness"; only consumer of client.global.events is events.ts:294. Finding 2: 15s dial timeout in both generators, timer cleared + listener removed in finally on all paths; timeout throws only when the parent signal did not abort; the error unwinds the loop → finally clears the flag → backoff reconnect runs; parked-attempt rescue fires only when in flight + no byte + parked past the dial bound, and cannot fire spuriously on a healthy dialling attempt. Finding 3: 30s stale-live rescue is coherent with the 45s watchdog (rescue first in the 30–45s window when a resume event arrives; watchdog is the backstop). LOW 5/6 fixed. The +3 tests are sound (real generator against the mock server; fail-before mechanism verified by reading — old code has zero liveness references). No new failing findings. Filed notes (not fails): parked-rescue keys off dial-start not dial-completion (a server violating 15s heartbeat cadence could be restarted by a resume event — cost is a restart, not data loss); 30s stale-live assumes ≤15s heartbeats, v1 interval still unknown; a stale lastByteAt write race (worst case one missed rescue, self-healing); test fidelity (resolve hook swaps expo/fetch for Node fetch); liveness yielded on garbage/partial chunks (benign — bytes did arrive).
CI on head 2ba4e92 (lead readback 2026-11-13): Typecheck and unit tests PASS, test PASS, gitleaks PASS; Unsigned iOS Simulator build, activation-e2e, build still pending.
Stage 4 re-review — both PASS; Stage 4 clean (2026-11-13)
Bug re-review: PASS (all findings + LOW 5/6 verified fixed; no new failings; filed notes only — see the earlier section).
Compliance re-check: PASS (all 8 AC re-verified; gates re-run standalone — check:versions pass, typecheck pass, full suite 385 pass / 0 fail, delta +3 AC-mapped; sweep re-run zero unfixed hits; applicationId untouched; no scope creep; lead decisions 1–9 honored; PR body accurate).
Clean Stage 4 verdict comment on PR fix: SSE startup + liveness (#189, #186) #237: 6856047162, read back by the lead (names head 2ba4e92). The earlier FAIL verdict (6855661220) is superseded.
CI on head 2ba4e92 (lead readback 2026-11-13): Typecheck and unit tests PASS, test PASS, gitleaks PASS; Unsigned iOS Simulator build QUEUED, activation-e2e IN PROGRESS, build IN PROGRESS. Merge HELD on those three — queue delays grant no exception.
CI green on new head b4e124a (readback): Typecheck and unit tests success, test success, build success, activation-e2e success, Unsigned iOS Simulator build success, Secret scan (gitleaks) success; release + build-fdroid skipped.
Design UPDATED: TDD §5 "SSE & Reconnect" + §4 step 3 rewritten to the merged mechanism (transport idle|connecting|live, connected only on first event/liveness, 15s dial bound, 30s stale-live rescue, deduped reconnectNow() + expo-network listener, dot reads transport; client published before metadata fetches). TDD: https://app.notion.com/p/40bac25eb49f8132908ce6f9bcc2dff9 — verified by re-fetch. (Note: update_content only matched with the fetched HTML-ish form — plain-text and markdown-form old_str both failed; lesson recorded here.)
PRD checked — no update needed: §3.4 (SSE real-time streaming), §3.7 (reconnect with exponential backoff + jitter), 15s backoff cap all still hold. PRD P2 line is garbled in the doc ("when the network blips for \P3 — ...") — pre-existing doc defect, not changed by this merge.
Close #189 + #186, mark the Work Log row Done. No release tag (Den's go only). Residuals unchanged: real Wi-Fi↔cellular handover unvalidated; v1 heartbeat interval / v1 prod usage unknown.
Continuation claim: Ember — replacement dev-workflow lead inheriting the claim (comment 6855126991), branch fix/sse-startup-recovery-189-186, PR #237. Original lead ended after Stage 4 clean verdict (comment 6856047162 on PR #237, head 2ba4e92). Proceeding at Stage 5: update branch from main (strict branch protection, PR is behind), re-verify CI on the new head, merge, then Stage 5.5 Design/PRD sync.
Stage 5 — MERGED: PR #237 merged as 3d7728c (main HEAD verified by readback). Branch updated from main first (merge commit b4e124a): gained only main-side changes (keyboard #194/#156, speech #197, auto-scroll #155, docs #239/#240) — no SSE file touched, so the Stage 4 clean verdict (comment 6856047162) still covered the tree. CI green on b4e124a: Typecheck and unit tests, test, build, activation-e2e, Unsigned iOS Simulator build, Secret scan all success; release + build-fdroid skipped.
Stage 5.5 — Design/PRD sync: Design UPDATED — TDD §5 “SSE & Reconnect” and §4 step 3 rewritten to the merged mechanism (transport idle|connecting|live, connected only on first event/liveness, 15s dial bound, 30s stale-live rescue, deduped reconnectNow() + expo-network listener, dot reads transport; client published before metadata fetches). https://app.notion.com/p/40bac25eb49f8132908ce6f9bcc2dff9 — verified by re-fetch. PRD checked — no update needed: §3.4 (SSE real-time streaming), §3.7 (reconnect with exponential backoff + jitter) and the 15s backoff cap all still hold.
Activity
fix(connections): start the event stream before fetching metadata (dz…
fix(connections): start the event stream before fetching metadata (dz…
Qiiks commentedon Aug 21, 2026
Fixed by the merge of #182 into the Qiiks/opencode-mobile maintenance branch (
maint/v0.4.16) — shipping in release v0.4.16-maint.1 (APK will be attached there). Upstream review of #182 is still recommended; this merge keeps the fork usable while it waits.docs: split plan.md into NOW (v2 fix) vs LATER priority tables
dzianisv commentedon Nov 13, 2026
Claimed: Ember — dev-workflow lead for the #189/#186 SSE fix. Branch:
fix/sse-startup-recovery-189-186.Scope: (1) #189 — commit the API client to the store before the
/project/currentand/pathmetadata fetches so the SSE stream can start immediately; (2) #186 — bounded SSE liveness detection and prompt reconnect after network/foreground transitions.Noted: PR #182 claims this fix but is unmergeable (dirty, 49 files, last updated Aug 23) and spans other leads' work, so this task ships a scoped fix against current main, reusing #182's verified approach where it applies. Investigating first; fix lands via PR with typecheck + full test suite before merge.
dzianisv commentedon Nov 13, 2026
Context pack — SSE #189 / #186 (maintained by the lead; artifacts win over this pack)
Stage: 5 + 5.5 DONE (merged 3d7728c, TDD updated). Lead: Ember — replacement dev-workflow lead (continuation claim 6858676393 on #189, pointer 6858676876 on #186), branch
fix/sse-startup-recovery-189-186, worktree~/workspace/opencode-mobile-sse-189-186. Claim: comment 6855126991. Work Log: https://app.notion.com/p/417ac25eb49f815aa751fea1b7345222 (Done 2026-11-14). Covers #186 too — see the pointer comment there. Project TDD/PRD exist (TDD §5 updated at Stage 5.5); the design gap below is the bug-lane design record.Stage 1 verdict (verified against main @ e844493)
src/stores/connections.ts:140-148awaitsproject.current()+path.get()before the singleset()that publishes the client (:151-160).REQUEST_TIMEOUT_MS = 30_000(src/lib/sdk.ts:218); the two fetches run in parallel, so worst case is ~30s.app/_layout.tsx:140-144starts SSE + catalog only whenclientappears. Second gate the issue does not mention:loadConnections()also awaitsresolveApi()/probe first (:131-132, 8s probe timeoutsrc/lib/sdk.ts:772).src/lib/sdk.ts:320SSE_IDLE_TIMEOUT_MS = 45_000,readWithIdleTimeout():322-332). v1 read is STILL unbounded (src/lib/sdk-v1.ts:291). Optimisticconnected:trueSTILL at dial time (src/stores/events.ts:198); notransportfield exists. 10s timer backoff reset STILL there (STABLE_CONNECTION_MS = 10_000, :69, :211-215). BackoffRECONNECT_DELAYS_MS = [1000, 2000, 4000, 8000, 15000](:68), jittered, capped 15s. Foreground hook EXISTS (app/_layout.tsx:130-137→reconnectNow():512-538) butreconnectNow()has NO live/in-flight check — it tears down a live stream on every foreground, despite the _layout comment calling it idempotent. Network-change hook: NONE (no NetInfo anywhere). Connection dot STILL hardcoded green (app/(tabs)/index.tsx:530); no UI readsconnected.Sweep (pattern-expressible, bounded to three files)
Method: code search rate-limited mid-sweep, redone against a full tarball of
e8444931(541 blobs) with localgrep -rn. Hits:project.current()—connections.ts:141, 203, 269, 350, 380(only :380refreshProject()is a standalone refresh; the other four await metadata before aset()that publishes the client:loadConnections,addConnection,setActiveConnection,updateConnection).path.get()—connections.ts:142, 204, 270, 351(same four sites).connected: true— single siteevents.ts:198.reader.read()—sdk.ts:326(guarded) andsdk-v1.ts:291(unbounded).NetInfo— zero hits repo-wide.transport— zero hits.reconnectNow—events.ts:56,512+_layout.tsx:133only.Design gap (recorded before the fix starts)
There is no single owner of "SSE liveness" as a state machine.
connectedis a boolean set at dial time, not derived from transport evidence; the idle watchdog (sdk), the backoff timer (events), and the foreground resume (_layout) were added at different times with no shared transport state; the UI reads a different store (or nothing) than the transport; and metadata fetch and client publication share oneset(), so unrelated data gates the transport. The v1/v2 split duplicated the SSE loop, so a fix applied to one silently missed the other. Any fix that patches one site without a sharedtransportstate and a single resume entry point will re-create this drift.Lead decisions (recorded 2026-11-13)
resolveApi()probe before publish — it chooses which client to build; SSE event stream startup is blocked behind /project/current and /path metadata fetches #189 is about the metadata fetch, not the probe. Split only the metadata behind the publish.expo-network) if it covers the listener; add@react-native-community/netinfoonly if it cannot, with the cause recorded (AC 2 requires prompt reconnect on network restoration)./api/eventendpoint first (part of this task). No*.test.tstoday importssrc/stores/events;src/lib/sse.test.tscovers only theSSEParser.npm run lintdoes not exist (package.jsonscripts: start, ios, android, typecheck, check:versions, test) — that criterion is void. Gate ischeck:versions+ typecheck + FULL test suite.reconnectNow()tearing down a live stream on every foreground is a bug — fix with dedupe (no-op when live or in flight).sse-liveness.tspolicy shape, 35s read timeout,transportstates,resume()dedupe, commit-client-first with stale guard) is reference only.Consolidated acceptance criteria
check:versions+ typecheck + FULL test suite pass. 8. No new unit/mock tests; integration/e2e extended only if coverage is missing.Open residuals
/global/eventSSE endpoint — v2/api/eventneeded for the test extension.Stage 3 verdict (PR #237, verified by lead readback 2026-11-13)
9b42be60fd543144b84fa61fecb9922254ea90fa, base maine8444931, 8 files, +382/−113. Never merged (implementer never merges).connections.ts— all four actions publish the client immediately, metadata behind via sharedfetchMetadataBehind()with theget().client === requestClientstale guard; 8s probe kept.events.ts—transport: idle|connecting|live,connectedonly on first event, 10s timer gone, backoff resets on first event,reconnectNow()deduped (token-guarded flag).sdk-v1.ts— same 45s idle-timeout wrapper as v2. Network listener viaexpo-network ~8.0.8(SDK 54,npx expo install; no NetInfo added; listener consultsisConnectedonly). Dot readstransport(green only when live, amber otherwise). Mock server gains v2GET /api/event; existing mock-server integration test extended (+2 tests, no new test files).check:versionspass,typecheckpass, full suite 382 pass, 0 fail. Test-count delta +2 (380 → 382, verified against a stashed clean-main baseline that also passed 380/380).app/(tabs)/index.tsx,package-lock.json,package.json,src/lib/sdk-v1.ts,src/lib/session-list.integration.test.ts,src/stores/connections.ts,src/stores/events.ts,tests/fixtures/mock-opencode-server.ts. Noapp.json/build.gradlechange — applicationId untouched. No keyboard/auto-scroll scope.testpass, gitleaks pass;Typecheck and unit tests,build,activation-e2estill in progress at review time. Merge waits on CI green.node --test— zustand + react-native AppState); coverage is the transport-level integration test + typecheck + review. v1 heartbeat interval and v1 prod usage still unknown.claude-code-reviewskill file referenced by dev-workflow (~/workspace/custom-skills/claude-code-review/SKILL.md) does not exist on this VM — Stage 4 runs on the method as specified inline in dev-workflow §Stage 4 (parallel compliance + bug agents, high-signal only, independent validation per finding).Stage 4 — compliance review: PASS (2026-11-13)
All 8 acceptance criteria verified against the diff; gates re-run standalone in the worktree (check:versions pass, typecheck pass, full suite 382 pass / 0 fail — matches the implementer exactly). Sweep re-run: zero unfixed hits (project.current()/path.get() only inside fetchMetadataBehind + standalone refreshProject(); connected:true single site inside the first-event branch; both reader.read() sites inside idle-timeout wrappers; NetInfo zero hits; STABLE_CONNECTION_MS zero hits). Standing rules verified: applicationId untouched, no keyboard/auto-scroll scope, test delta +2 AC-mapped, no new test files. Lead decisions 1–9 all honored. PR body accurate on every checkable claim. Filed notes (not fails): first-event/dedupe logic untested directly; real handover unvalidated; v1 heartbeat/usage unknown; 380 baseline not re-run by the reviewer (delta established from the diff + 382 count).
Stage 4 — bug review: FAIL (2026-11-13), fix pass running
Two validated HIGH findings (both verified by the lead against the code in the worktree before acting):
live: the only path toliveis the first yielded event (events.ts:287-290), but on a quiet server nothing is yielded — v2 dropsserver.connectedin the translator (v2-shape.ts:583-589), v1'"'"'s subscribe frame is a comment, heartbeats never surface as yielded events. Dot amber forever,connectedfalse, backoff never resets, resync never runs. Fix: plumb byte-level liveness up; liveness must be observable at the events-generator level so the integration test can assert it on a quiet stream.attemptInFlightforever: dial (sdk.ts:366,sdk-v1.ts:305) has no timeout; 45s watchdog only wrapsreader.read()after headers. A black-holed dial never unwinds, thefinallynever runs,reconnectNow()no-ops forever. Fix: bound the dial (preferred) or letreconnectNow()abort-and-restart a staleconnectingattempt.Also in the fix pass: MEDIUM 3 (last-event timestamp so
livecan go stale; network-restore must rescue stale-live), MEDIUM 4 (the +2 tests pass vacuously re liveness — add a test that fails on Finding 1), LOW 5 (connected:false on connect start) and LOW 6 (breadcrumb timing) if cheap. Filed notes (not fails, no scope expansion): v1 idle-loop risk if real v1 servers send no heartbeats;refreshProjectunconditional set andremoveConnectionstale metadata are pre-existing.Stage 4 fix pass — done, new head 2ba4e92 (verified by lead readback 2026-11-13)
2ba4e92150b1c301fd19f693013333cd8d57afcf, commit "fix: SSE quiet-server liveness + bounded dial", 10 files, +632/−118. Base main unmoved ate8444931. CI on the new head: Typecheck and unit tests PASS, test PASS, gitleaks PASS; iOS sim build, activation-e2e, build pending.src/lib/sse.ts:14,24,33:SSE_LIVENESS_EVENT_TYPE = "sse.liveness",SSE_DIAL_TIMEOUT_MS = 15_000,SSE_STALE_LIVE_MS = 30_000.{type: "sse.liveness", properties: {}}when a chunk carries no session event (sdk.ts:439,sdk-v1.ts:370);events.ts:302-324recordslastByteAton any yielded event, flips live + resets backoff + runs the resync on quiet reconnects too. Liveness observable at theclient.global.eventsgenerator level.sdk.ts:371-388,sdk-v1.ts:308-325, throwsSSE dial timed out);reconnectNow()aborts-and-restarts an attempt parked past the dial bound with no byte (events.ts:591-592,attemptStartedAt/lastByteAtat :230-231).livewith no byte for 30s is stale and gets rescued (events.ts:587-590); a fresh live stream still no-ops. LOW 5 fixed (connected: falseon re-entry, :236). LOW 6 fixed (breadcrumb at publish time,connections.ts:298).refreshProject/removeConnectionleft untouched as instructed; network-listener unsubscribe handle considered and not added (no teardown site); abort-unwind answer recorded in the PR body.sse.liveness; quiet v1 → same; lifecycle still flows through the generator). Fail-before proven: with old generators stashed, all 3 new tests fail, 5 old pass; after the fix all 8 pass in the file. Note: the test file registers an inlinenode:moduleresolve hook redirectingexpo/fetchto global fetch (expo/fetchhas no resolver outside Metro);mock.modulewas tried first and is unavailable without--experimental-test-module-mocks.Stage 4 bug re-review: PASS (2026-11-13)
All three FAIL findings plus LOW 5 and LOW 6 verified fixed against the delta
9b42be60..2ba4e92. Finding 1: both generators yield liveness on heartbeat-only chunks; store flips live + resets backoff + resyncs on liveness, before the session switch;sawLivenessis per-attempt; no real event type collides with"sse.liveness"; only consumer ofclient.global.eventsisevents.ts:294. Finding 2: 15s dial timeout in both generators, timer cleared + listener removed infinallyon all paths; timeout throws only when the parent signal did not abort; the error unwinds the loop →finallyclears the flag → backoff reconnect runs; parked-attempt rescue fires only when in flight + no byte + parked past the dial bound, and cannot fire spuriously on a healthy dialling attempt. Finding 3: 30s stale-live rescue is coherent with the 45s watchdog (rescue first in the 30–45s window when a resume event arrives; watchdog is the backstop). LOW 5/6 fixed. The +3 tests are sound (real generator against the mock server; fail-before mechanism verified by reading — old code has zerolivenessreferences). No new failing findings. Filed notes (not fails): parked-rescue keys off dial-start not dial-completion (a server violating 15s heartbeat cadence could be restarted by a resume event — cost is a restart, not data loss); 30s stale-live assumes ≤15s heartbeats, v1 interval still unknown; a stalelastByteAtwrite race (worst case one missed rescue, self-healing); test fidelity (resolve hook swaps expo/fetch for Node fetch); liveness yielded on garbage/partial chunks (benign — bytes did arrive).Stage 4 re-review — both PASS; Stage 4 clean (2026-11-13)
2ba4e92). The earlier FAIL verdict (6855661220) is superseded.Stage 5 — MERGED (2026-11-14, replacement lead)
Stage 5.5 — Design/PRD sync (2026-11-14)
Next action
Close #189 + #186, mark the Work Log row Done. No release tag (Den's go only). Residuals unchanged: real Wi-Fi↔cellular handover unvalidated; v1 heartbeat interval / v1 prod usage unknown.
fix: SSE quiet-server liveness + bounded dial (Stage 4 fix pass, #189, …
dzianisv commentedon Nov 13, 2026
Continuation claim: Ember — replacement dev-workflow lead inheriting the claim (comment 6855126991), branch
fix/sse-startup-recovery-189-186, PR #237. Original lead ended after Stage 4 clean verdict (comment 6856047162 on PR #237, head 2ba4e92). Proceeding at Stage 5: update branch from main (strict branch protection, PR is behind), re-verify CI on the new head, merge, then Stage 5.5 Design/PRD sync.Merge pull request #237 from dzianisv/fix/sse-startup-recovery-189-186
dzianisv commentedon Nov 13, 2026
Stage 5 — MERGED: PR #237 merged as 3d7728c (main HEAD verified by readback). Branch updated from main first (merge commit b4e124a): gained only main-side changes (keyboard #194/#156, speech #197, auto-scroll #155, docs #239/#240) — no SSE file touched, so the Stage 4 clean verdict (comment 6856047162) still covered the tree. CI green on b4e124a: Typecheck and unit tests, test, build, activation-e2e, Unsigned iOS Simulator build, Secret scan all success; release + build-fdroid skipped.
dzianisv commentedon Nov 13, 2026
Stage 5.5 — Design/PRD sync: Design UPDATED — TDD §5 “SSE & Reconnect” and §4 step 3 rewritten to the merged mechanism (transport
idle|connecting|live,connectedonly on first event/liveness, 15s dial bound, 30s stale-live rescue, dedupedreconnectNow()+expo-networklistener, dot readstransport; client published before metadata fetches). https://app.notion.com/p/40bac25eb49f8132908ce6f9bcc2dff9 — verified by re-fetch. PRD checked — no update needed: §3.4 (SSE real-time streaming), §3.7 (reconnect with exponential backoff + jitter) and the 15s backoff cap all still hold.fix: SSE startup + liveness (dzianisv#189, dzianisv#186)
release: v0.4.18 — keyboard, voice, and stability fixes