docs(design): transport SSOT architecture#430
Conversation
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 5 issue(s) (2 warning).
docs/design/transport-ssot.md
Well-researched design doc with accurate diagnosis of the transport bug taxonomy. Two file path inaccuracies in the Files Involved table (connection-registry.ts is in packages/harness/, not server/), and the is_active column removal in Phase 1 conflicts with its continued use in handleReconnect which isn't removed until Phase 3.
- 🟡 bugs (L292): Files Involved table lists
server/connection-registry.ts, but the actual file ispackages/harness/src/connection-registry.ts. The harness package owns this module. Using the wrong path will mislead implementers.[fixable] - 🔵 style (L7): "Supersedes: Parts of
session-state-machine.md" — that doc is still Status: Proposed (never implemented). Consider saying "Incorporates and replaces" rather than "Supersedes" since there's nothing live to supersede. The crash recovery concept (recoverStaleSessions) originates from that doc's Phase 4 and is pulled forward here to Phase 0, which is worth calling out explicitly.[fixable] - 🟡 unsafe_assumptions (L253): Phase 1 step 5 says "Remove
is_activeboolean column from EventStore" — butis_activeis used in 4+ SQL queries in event-store.ts (session listing, meta endpoint, reconnect checks) and exposed via the/api/sessions/:id/metaresponse (isActivefield). Removing it in Phase 1 whilehandleReconnectstill exists (removed in Phase 3) creates a gap. Either the migration plan needs a compatibility shim in Phase 1, oris_activeremoval should move to Phase 3 alongsidehandleReconnectremoval.[fixable] - 🔵 unsafe_assumptions (L272): Phase 2 step 4 proposes "retry with exponential backoff on POST failure (max 3 attempts)" but doesn't address idempotency. If the POST succeeds server-side but the response is lost (network error), the client would retry and send the message again. The
seqnumber in the 202 response could serve as an idempotency key, but the design doesn't specify this. Worth noting in the design or in Open Questions.[fixable] - 🔵 style (L288): The Files Involved tables list server-side files as
server/paths butconnection-registry.tsandsession-registry.tsactually live inpackages/harness/src/. Theevent-store.tspath is correctly listed aspackages/protocol/src/event-store.ts. For consistency, harness files should use their actual package paths (packages/harness/src/connection-registry.ts).[fixable]
| | `server/app.ts` | Add `state` to meta endpoint response | | ||
| | `packages/protocol/src/event-store.ts` | Remove `is_active` column; emit events from `setSessionState()` | | ||
| | `packages/protocol/src/types.ts` | Add `SessionStateEvent` type | | ||
| | `server/connection-registry.ts` | Remove periodic sync; simplify to cursor-based replay | |
There was a problem hiding this comment.
🟡 bugs: Files Involved table lists server/connection-registry.ts, but the actual file is packages/harness/src/connection-registry.ts. The harness package owns this module. Using the wrong path will mislead implementers. [fixable]
| **Author:** Claude Opus 4.6 + Dimitri Saridakis | ||
| **Created:** 2026-07-02 | ||
| **Telos:** 97361f814c5f54df | ||
| **Supersedes:** Parts of `session-state-machine.md`, `streaming-input-session-control.md` |
There was a problem hiding this comment.
🔵 style: "Supersedes: Parts of session-state-machine.md" — that doc is still Status: Proposed (never implemented). Consider saying "Incorporates and replaces" rather than "Supersedes" since there's nothing live to supersede. The crash recovery concept (recoverStaleSessions) originates from that doc's Phase 4 and is pulled forward here to Phase 0, which is worth calling out explicitly. [fixable]
| 3. Client awaits POST response before marking message as sent | ||
| 4. Add retry with exponential backoff on POST failure (max 3 attempts) | ||
| 5. Add `sendError` UI state for permanent failures | ||
| 6. Heartbeat liveness: 20s ping, 40s timeout, auto-reconnect |
There was a problem hiding this comment.
🟡 unsafe_assumptions: Phase 1 step 5 says "Remove is_active boolean column from EventStore" — but is_active is used in 4+ SQL queries in event-store.ts (session listing, meta endpoint, reconnect checks) and exposed via the /api/sessions/:id/meta response (isActive field). Removing it in Phase 1 while handleReconnect still exists (removed in Phase 3) creates a gap. Either the migration plan needs a compatibility shim in Phase 1, or is_active removal should move to Phase 3 alongside handleReconnect removal. [fixable]
| **Dead code removal:** `handleReconnect` (ws-handler-v2.ts), `doReconnectPost` (sse-connection.ts), periodic sync timer and `shouldSync` logic (connection-registry.ts), `reconnect` message type. | ||
|
|
||
| ### Phase 4: Cleanup | ||
|
|
There was a problem hiding this comment.
🔵 unsafe_assumptions: Phase 2 step 4 proposes "retry with exponential backoff on POST failure (max 3 attempts)" but doesn't address idempotency. If the POST succeeds server-side but the response is lost (network error), the client would retry and send the message again. The seq number in the 202 response could serve as an idempotency key, but the design doesn't specify this. Worth noting in the design or in Open Questions. [fixable]
| | `server/query-loop.ts` | Emit `session_state_changed` on state transitions | | ||
| | `server/chat.ts` | Emit state events from `startChat`/`sendToChat`; crash recovery on startup | | ||
| | `server/ws-handler-v2.ts` | Remove `handleReconnect`; replace `is_active` checks with state-based | | ||
| | `server/index.ts` | Call `recoverStaleSessions()` on startup | |
There was a problem hiding this comment.
🔵 style: The Files Involved tables list server-side files as server/ paths but connection-registry.ts and session-registry.ts actually live in packages/harness/src/. The event-store.ts path is correctly listed as packages/protocol/src/event-store.ts. For consistency, harness files should use their actual package paths (packages/harness/src/connection-registry.ts). [fixable]
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 5 issue(s) (2 warning).
docs/design/transport-ssot.md
Well-researched design doc with accurate codebase references. All major claims verified against real code. A few naming inaccuracies (shouldSync doesn't exist) and the requires_action state needs a clearer emission path since it's orthogonal to the SessionState machine.
- 🟡 bugs (L270):
shouldSync()is referenced as dead code to remove fromconnection-registry.ts, but no function by that name exists. The actual methods arestartPeriodicSync()andstopPeriodicSync(). Same issue on line 326. Update to reference the real function names to avoid confusion during implementation.[fixable] - 🟡 unsafe_assumptions (L139): The
SessionStateEventmapspermission_request -> 'requires_action', butpermission_requestis a v2 protocol event type, not aSessionStateenum value. The 7-stateSessionStatehas norequires_actionequivalent — it's a cross-cutting concern (permissions can arrive in ACTIVE state). The doc should clarify thatrequires_actionis derived from permission events, not fromsetSessionState(), which contradicts the "Emitsession_state_changedon everysetSessionState()call" instruction in Section 3. This needs a second emission path (frompermission_requestevents) that isn't mentioned in the implementation plan.[fixable] - 🔵 style (L81): The architecture diagram shows
POST /sendbut Section 4 (Write Acknowledgment) referencesPOST /api/chat/send. The existing codebase uses the/api/chat/sendpath (viachat-rest-handler.ts). The diagram should use the full path for consistency and to avoid ambiguity during implementation.[fixable] - 🔵 unsafe_assumptions (L249): Phase 2 references
clientMsgIdas an idempotency key and claims "server already has idempotency checking (#386)". This should be verified during implementation — if the idempotency check is keyed differently or has been removed/refactored since #386, retried POSTs could create duplicate messages. Consider adding a Phase 2 prerequisite to verify the idempotency mechanism exists and is keyed onclientMsgId. - 🔵 style (L176): Phase 0 proposes
recoverStaleSessions()to transition ACTIVE/STARTING/DETACHED sessions to ENDED on startup, but doesn't mention SUSPENDED. TheSessionStatetype includes SUSPENDED as a state where the session is not cleanly ended. If the server crashes while a session is SUSPENDED, it should also be recovered. Consider adding SUSPENDED to the recovery set, or document why it's excluded.[fixable]
|
|
||
| **Tests:** Verify reconnect replays missed events correctly. Verify no duplicate events. Verify generation continues during disconnect and events are available on reconnect. | ||
|
|
||
| **Dead code removal:** `handleReconnect` (ws-handler-v2.ts), `doReconnectPost` (packages/client/src/sse-connection.ts), periodic sync timer and `shouldSync` logic (packages/harness/src/connection-registry.ts), `reconnect` message type, `markSessionInactive()`, `is_active` column. |
There was a problem hiding this comment.
🟡 bugs: shouldSync() is referenced as dead code to remove from connection-registry.ts, but no function by that name exists. The actual methods are startPeriodicSync() and stopPeriodicSync(). Same issue on line 326. Update to reference the real function names to avoid confusion during implementation. [fixable]
| type SessionStateEvent = { | ||
| type: 'session_state_changed'; | ||
| sessionId: string; | ||
| state: 'idle' | 'running' | 'requires_action'; |
There was a problem hiding this comment.
🟡 unsafe_assumptions: The SessionStateEvent maps permission_request -> 'requires_action', but permission_request is a v2 protocol event type, not a SessionState enum value. The 7-state SessionState has no requires_action equivalent — it's a cross-cutting concern (permissions can arrive in ACTIVE state). The doc should clarify that requires_action is derived from permission events, not from setSessionState(), which contradicts the "Emit session_state_changed on every setSessionState() call" instruction in Section 3. This needs a second emission path (from permission_request events) that isn't mentioned in the implementation plan. [fixable]
| ------ ------ | ||
|
|
||
| |--- POST /send ----------------->| | ||
| |<-- 202 { accepted, jobId } -----| (write acknowledged) |
There was a problem hiding this comment.
🔵 style: The architecture diagram shows POST /send but Section 4 (Write Acknowledgment) references POST /api/chat/send. The existing codebase uses the /api/chat/send path (via chat-rest-handler.ts). The diagram should use the full path for consistency and to avoid ambiguity during implementation. [fixable]
| 1. Remove `wasRunning` branch from `sendMessage()` | ||
| 2. Remove `pendingSend[]`, `PENDING_SEND_TIMEOUT_MS`, drain logic, timer, callbacks | ||
| 3. Client awaits POST response before marking message as sent | ||
| 4. Add retry with exponential backoff on POST failure (max 3 attempts). Use `clientMsgId` as idempotency key -- server already has idempotency checking (#386), so retried POSTs are deduplicated server-side |
There was a problem hiding this comment.
🔵 unsafe_assumptions: Phase 2 references clientMsgId as an idempotency key and claims "server already has idempotency checking (#386)". This should be verified during implementation — if the idempotency check is keyed differently or has been removed/refactored since #386, retried POSTs could create duplicate messages. Consider adding a Phase 2 prerequisite to verify the idempotency mechanism exists and is keyed on clientMsgId.
|
|
||
| - Remove `is_active` boolean column from EventStore (redundant with `state`) | ||
| - Replace `handleReconnect`'s `storeMeta.isActive` check with state-based logic | ||
| - Add `recoverStaleSessions()` on startup: any session in ACTIVE/STARTING/DETACHED → ENDED |
There was a problem hiding this comment.
🔵 style: Phase 0 proposes recoverStaleSessions() to transition ACTIVE/STARTING/DETACHED sessions to ENDED on startup, but doesn't mention SUSPENDED. The SessionState type includes SUSPENDED as a state where the session is not cleanly ended. If the server crashes while a session is SUSPENDED, it should also be recovered. Consider adding SUSPENDED to the recovery set, or document why it's excluded. [fixable]
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 4 issue(s) (1 warning).
docs/design/transport-ssot.md
Well-grounded design doc — all referenced code artifacts verified as existing. One inaccurate file reference for the clientMsgId idempotency guard (it's in chat.ts, not chat-rest-handler.ts) and minor clarification opportunities.
- 🟡 unsafe_assumptions (L305): Phase 2 prerequisite says to verify
clientMsgIdidempotency guard is "present and keyed correctly inchat-rest-handler.ts". The guard actually lives inserver/chat.ts(storeAndEchoIfNew()at line ~1162, which callseventStore.hasUserMessage()).chat-rest-handler.tspassesclientMsgIdthrough tohandleSendV2()which callssendToChat()inchat.ts, but the dedup logic itself is not inchat-rest-handler.ts. The prerequisite should referencechat.ts(or both files) so an implementer doesn't look in the wrong place and conclude the guard is missing.[fixable] - 🔵 style (L8):
streaming-input-session-control.mdstatus is listed implicitly as never implemented (via the "Incorporates and replaces" line grouping it withsession-state-machine.md). However,streaming-input-session-control.mdhasStatus: Implemented (v0.1.0)and was shipped with protocol v2 (PR #33). The relationship is different — this doc replaces a shipped design, not a proposed one. Consider clarifying the distinction (e.g., "replaces session-state-machine.md (never implemented) and supersedes streaming-input-session-control.md (implemented, but scope expanded here)").[fixable] - 🔵 unsafe_assumptions (L349): The Files Involved table lists
frontend/src/client-store.tsfor the SSE default transport flip. This file does exist and contains the transport selector logic (localStorage.getItem('mitzo:transport') === 'sse'). However, if the SSE flip also involves changes topackages/client/src/store.ts(where theMitzoConnectionis instantiated), that file should be listed in the table as well. Currently the Server table lists the store file for removingwasRunningbut the Phase 0 SSE flip may span both locations.[fixable] - 🔵 style (L181): The
SessionStateEventtype comment sayssetSessionState()mapsDETACHED/SUSPENDED -> 'preserve last emitted (server buffers)'. This is an important behavioral detail but it's buried in a code comment. The buffering mechanism (how the server remembers and replays the last emitted client-facing state after a transient internal state) should be called out explicitly in the design text, as implementers need to build this state tracking.[fixable]
|
|
||
| ### Phase 2: Always-send (remove client routing) | ||
|
|
||
| **Prerequisite:** Verify `clientMsgId` idempotency guard from PR #386 is still present and keyed correctly in `chat-rest-handler.ts`. If refactored or removed, re-implement before enabling retries. |
There was a problem hiding this comment.
🟡 unsafe_assumptions: Phase 2 prerequisite says to verify clientMsgId idempotency guard is "present and keyed correctly in chat-rest-handler.ts". The guard actually lives in server/chat.ts (storeAndEchoIfNew() at line ~1162, which calls eventStore.hasUserMessage()). chat-rest-handler.ts passes clientMsgId through to handleSendV2() which calls sendToChat() in chat.ts, but the dedup logic itself is not in chat-rest-handler.ts. The prerequisite should reference chat.ts (or both files) so an implementer doesn't look in the wrong place and conclude the guard is missing. [fixable]
| **Created:** 2026-07-02 | ||
| **Telos:** 97361f814c5f54df | ||
| **Incorporates and replaces:** `session-state-machine.md` (Status: Proposed, never implemented), `streaming-input-session-control.md`. Crash recovery from `session-state-machine.md` Phase 4 is pulled forward to Phase 0 here. | ||
| **Related PRs:** #396 (never merged), #401 (merged, reverted via #403), #407 |
There was a problem hiding this comment.
🔵 style: streaming-input-session-control.md status is listed implicitly as never implemented (via the "Incorporates and replaces" line grouping it with session-state-machine.md). However, streaming-input-session-control.md has Status: Implemented (v0.1.0) and was shipped with protocol v2 (PR #33). The relationship is different — this doc replaces a shipped design, not a proposed one. Consider clarifying the distinction (e.g., "replaces session-state-machine.md (never implemented) and supersedes streaming-input-session-control.md (implemented, but scope expanded here)"). [fixable]
| | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | ||
| | `server/query-loop.ts` | Emit `session_state_changed` on state transitions | | ||
| | `server/chat.ts` | Emit state events from `startChat`/`sendToChat`; crash recovery on startup | | ||
| | `server/ws-handler-v2.ts` | Replace `is_active` checks with state-based (P1); remove `handleReconnect` (P3); remove file (P4) | |
There was a problem hiding this comment.
🔵 unsafe_assumptions: The Files Involved table lists frontend/src/client-store.ts for the SSE default transport flip. This file does exist and contains the transport selector logic (localStorage.getItem('mitzo:transport') === 'sse'). However, if the SSE flip also involves changes to packages/client/src/store.ts (where the MitzoConnection is instantiated), that file should be listed in the table as well. Currently the Server table lists the store file for removing wasRunning but the Phase 0 SSE flip may span both locations. [fixable]
|
|
||
| - Add `state` column alongside `is_active` in EventStore (backwards-compatible; Phase 0) | ||
| - Write both `state` and `is_active` in `setSessionState()` during transition period | ||
| - Migrate all `is_active` readers to use `state` (Phase 1) |
There was a problem hiding this comment.
🔵 style: The SessionStateEvent type comment says setSessionState() maps DETACHED/SUSPENDED -> 'preserve last emitted (server buffers)'. This is an important behavioral detail but it's buried in a code comment. The buffering mechanism (how the server remembers and replays the last emitted client-facing state after a transient internal state) should be called out explicitly in the design text, as implementers need to build this state tracking. [fixable]
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 7 issue(s) (4 warning).
docs/design/transport-ssot.md
Well-researched design doc with accurate problem analysis. Main issues: Phase 0 describes already-implemented work (state column migration) without noting it, the is_active/state synchronization gap during transition needs explicit risk callout, and the UX impact of moving from optimistic to acknowledged sends should be addressed.
- 🟡 style (L284): Phase 0 item 7 ('Add
statecolumn to EventStore sessions table alongsideis_active') is already implemented — the migration exists inpackages/protocol/src/event-store.ts:282-293andsetSessionState()already writes to it. This should be marked as 'Done' or noted as pre-existing to avoid confusion during implementation. Similarly, thesetSessionStatemethod andVALID_TRANSITIONStable already exist.[fixable] - 🔵 style (L109): The doc says the
wasRunningbranch is '~50 lines' to remove. The actualwasRunningbranch inpackages/client/src/store.ts:377-398is 22 lines. The ~50 figure may include related code scattered elsewhere (pendingSenddeclarations, timer helpers, drain logic), but as written it overstates the size of the single branch. Consider saying '~50 lines across store and parser' for accuracy.[fixable] - 🟡 unsafe_assumptions (L280): Phase 0 item 8 says 'Migrate
setSessionState()to write bothstateandis_active'. CurrentlysetSessionState()(event-store.ts:190-196) only writesstateandlast_state_change— it does NOT writeis_active. MeanwhilemarkSessionInactive()only writesis_active. This means during the transition period, a session ended viasetSessionState(ENDED)won't flipis_active = 0, causing staleis_active = 1for ended sessions. The doc correctly identifies this gap but should explicitly call out the risk: until this migration step is done, any code path that usessetSessionState()to end a session without also callingmarkSessionInactive()will leave a staleis_active.[fixable] - 🟡 unsafe_assumptions (L247): The Multi-Client Fan-Out section says 'Concurrent sends are serialized. If two clients POST simultaneously, AsyncQueue serializes them. The SDK processes one turn at a time.' This assumes the SDK handles interleaved user messages gracefully. If the SDK's
query()doesn't support multiple sequential user messages pushed mid-generation (i.e., a second message arriving while the first is still generating), this could produce unexpected behavior. The doc should note whether this is a tested SDK capability or an assumption that needs verification.[fixable] - 🔵 style (L215): The
SessionStateEventtype comment says 'CREATED/ENDED -> idle', 'STARTING/ACTIVE -> running'. The DETACHED/SUSPENDED mapping says 'preserve last emitted (server buffers)'. However, CLOSING state is not mentioned in the mapping. Given the 7-state server model includes CLOSING (referenced by the 'Why 3 states, not 7' paragraph), the mapping should specify CLOSING's client-facing state — likely 'running' (still generating) or 'idle' (winding down).[fixable] - 🟡 regressions (L316): Phase 2 removes
sendMessage()'swasRunningguard and makes all sends go via HTTP POST with acknowledgment. But the currentsendMessage()fallback path (line 400-403 in store.ts) handlesconnection.send()returning false with asendErrormessage. Phase 2 step 3 says 'Client awaits POST response before marking message as sent' — this changes the UX from immediate-optimistic (message appears instantly, error shown later) to acknowledged (message appears only after 202). The doc should explicitly note this UX change and whether theUSER_SENDreducer action (which adds the user bubble immediately at line 364) should be deferred until acknowledgment or kept as-is with a 'pending' state.[fixable] - 🔵 style (L336): Phase 3 item 6 says 'ConnectionRegistry fan-out: track set of connections per session, not single owner'. But the current ConnectionRegistry (packages/harness/src/connection-registry.ts) already tracks per-connection per-session cursors via a two-level Map, and connections have
watchedSessions: Set<string>. The inverse mapping (session -> connections) may need addition, but the doc should clarify what specifically needs to change vs what's already there.[fixable]
| 3. Add `state` field to `/api/sessions/:id/meta` response | ||
| 4. Client receives and logs `session_state_changed` but doesn't act on it yet | ||
| 5. Add crash recovery: `recoverStaleSessions()` on server startup | ||
| 6. Flip SSE to default transport (currently WS default, SSE opt-in via localStorage) |
There was a problem hiding this comment.
🟡 style: Phase 0 item 7 ('Add state column to EventStore sessions table alongside is_active') is already implemented — the migration exists in packages/protocol/src/event-store.ts:282-293 and setSessionState() already writes to it. This should be marked as 'Done' or noted as pre-existing to avoid confusion during implementation. Similarly, the setSessionState method and VALID_TRANSITIONS table already exist. [fixable]
|
|
||
| The server already handles send-while-running correctly: `handleSendV2()` routes to `sendToChat()` which pushes onto the `AsyncQueue`. The SDK processes it on the next turn. This path is well-tested (used for takeover/reattach scenarios). The client-side queue is redundant and is the primary source of the current bug. | ||
|
|
||
| **Remove:** |
There was a problem hiding this comment.
🔵 style: The doc says the wasRunning branch is '~50 lines' to remove. The actual wasRunning branch in packages/client/src/store.ts:377-398 is 22 lines. The ~50 figure may include related code scattered elsewhere (pendingSend declarations, timer helpers, drain logic), but as written it overstates the size of the single branch. Consider saying '~50 lines across store and parser' for accuracy. [fixable]
| ### Phase 0: Foundation (no behavior change) | ||
|
|
||
| 1. Add `session_state_changed` event to the protocol | ||
| 2. Emit it from `setSessionState()` alongside existing events |
There was a problem hiding this comment.
🟡 unsafe_assumptions: Phase 0 item 8 says 'Migrate setSessionState() to write both state and is_active'. Currently setSessionState() (event-store.ts:190-196) only writes state and last_state_change — it does NOT write is_active. Meanwhile markSessionInactive() only writes is_active. This means during the transition period, a session ended via setSessionState(ENDED) won't flip is_active = 0, causing stale is_active = 1 for ended sessions. The doc correctly identifies this gap but should explicitly call out the risk: until this migration step is done, any code path that uses setSessionState() to end a session without also calling markSessionInactive() will leave a stale is_active. [fixable]
|
|
||
| **Current:** `ConnectionRegistry` maps one connection per session (owner model). | ||
|
|
||
| **Target:** `ConnectionRegistry` tracks a **set** of connections per session: |
There was a problem hiding this comment.
🟡 unsafe_assumptions: The Multi-Client Fan-Out section says 'Concurrent sends are serialized. If two clients POST simultaneously, AsyncQueue serializes them. The SDK processes one turn at a time.' This assumes the SDK handles interleaved user messages gracefully. If the SDK's query() doesn't support multiple sequential user messages pushed mid-generation (i.e., a second message arriving while the first is still generating), this could produce unexpected behavior. The doc should note whether this is a tested SDK capability or an assumption that needs verification. [fixable]
|
|
||
| **Current:** EventStore already assigns monotonic sequence numbers to events. The periodic sync mechanism uses cursors. But reconnect still attempts full state reconstruction via `handleReconnect`. | ||
|
|
||
| **Target:** SSE reconnect sends `?from=<lastSeq>`. Server replays events from that sequence number. No full state reconstruction, no `handleReconnect` ownership dance. |
There was a problem hiding this comment.
🔵 style: The SessionStateEvent type comment says 'CREATED/ENDED -> idle', 'STARTING/ACTIVE -> running'. The DETACHED/SUSPENDED mapping says 'preserve last emitted (server buffers)'. However, CLOSING state is not mentioned in the mapping. Given the 7-state server model includes CLOSING (referenced by the 'Why 3 states, not 7' paragraph), the mapping should specify CLOSING's client-facing state — likely 'running' (still generating) or 'idle' (winding down). [fixable]
| 5. Add `sendError` UI state for permanent failures | ||
| 6. Heartbeat liveness: 20s ping, 40s timeout, auto-reconnect | ||
|
|
||
| **Tests:** Verify send-while-running works (server queues via AsyncQueue). Verify POST failure shows error UI. Verify heartbeat timeout triggers reconnect. Verify no message loss on reconnect during generation. |
There was a problem hiding this comment.
🟡 regressions: Phase 2 removes sendMessage()'s wasRunning guard and makes all sends go via HTTP POST with acknowledgment. But the current sendMessage() fallback path (line 400-403 in store.ts) handles connection.send() returning false with a sendError message. Phase 2 step 3 says 'Client awaits POST response before marking message as sent' — this changes the UX from immediate-optimistic (message appears instantly, error shown later) to acknowledged (message appears only after 202). The doc should explicitly note this UX change and whether the USER_SEND reducer action (which adds the user bubble immediately at line 364) should be deferred until acknowledgment or kept as-is with a 'pending' state. [fixable]
| ### Phase 4: Cleanup | ||
|
|
||
| 1. Remove WS transport code (SSE is default since Phase 0) | ||
| 2. Remove `SessionSseRegistry` if consolidated into `ConnectionRegistry` |
There was a problem hiding this comment.
🔵 style: Phase 3 item 6 says 'ConnectionRegistry fan-out: track set of connections per session, not single owner'. But the current ConnectionRegistry (packages/harness/src/connection-registry.ts) already tracks per-connection per-session cursors via a two-level Map, and connections have watchedSessions: Set<string>. The inverse mapping (session -> connections) may need addition, but the doc should clarify what specifically needs to change vs what's already there. [fixable]
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 6 issue(s) (4 warning).
docs/design/transport-ssot.md
Well-structured design doc with strong problem analysis and phased migration plan. Key concern: the ConnectionRegistry "Current" vs "Target" comparison misrepresents the existing data model (it's already connection→sessions, not session→connection), which could mislead implementers about the scope of multi-client fan-out changes.
- 🟡 bugs (L251): The "Current" pseudo-code
sessions: Map<sessionId, { connectionId, cursor }>misrepresents ConnectionRegistry's actual structure. In reality, the primary map isconnections: Map<connectionId, Connection>where each Connection haswatchedSessions: Set<string>, and cursors are tracked separately incursors: Map<connectionId, Map<sessionId, number>>. The relationship is inverted (connection→sessions, not session→connection). The current code already supports multiple connections watching the same session. This means the "Target" change may be smaller than described, and implementers working from this doc will start from the wrong mental model.[fixable] - 🟡 bugs (L254): The target type
Set<{ connectionId: string; cursor: number }>won't work correctly in TypeScript/JavaScript —Setuses reference equality for objects, making lookup and deletion by value impossible. AMap<string, number>(connectionId → cursor) would be the correct data structure here. Given that the existingConnectionRegistryalready uses per-connection cursor maps, the target should be described in terms of the actual existing structure rather than an invented one.[fixable] - 🟡 unsafe_assumptions (L111): The doc says the
wasRunningbranch is "~50 lines" but the actual implementation spans lines 339-404 inpackages/client/src/store.ts(66 lines). Minor inaccuracy, but an implementer using this doc to scope removal work would underestimate.[fixable] - 🟡 unsafe_assumptions (L318): Phase 2 prerequisite says idempotency guard
storeAndEchoIfNew()lives inserver/chat.tsand callseventStore.hasUserMessage(). This is accurate today, but the doc warns it may have been "refactored or removed" since PR #386 — this safety net is good. However, the Phase 2 plan adds retry with exponential backoff (up to 3 attempts) relying on this dedup, but doesn't specify whether the HTTP POST handler (chat-rest-handler.ts) also needs dedup at the HTTP layer (e.g., returning 202 for already-accepted messages instead of processing them again up to the dedup check). If the dedup is only instoreAndEchoIfNew(), the agent will still receive the message twice via AsyncQueue before dedup fires. - 🔵 style (L148): The
SessionStateEventtype comment says DETACHED/SUSPENDED → 'preserve last emitted (server buffers)', but the 3-state type'idle' | 'running' | 'requires_action'doesn't include a 'no change' sentinel. The prose below explainslastEmittedClientStatebuffering, but the inline type comment could mislead — someone reading just the type definition might expect a fourth state value. Consider clarifying inline that these server states suppress emission entirely (no event sent) rather than emit a special value.[fixable] - 🔵 style (L360): The Files Involved table lists
frontend/src/client-store.tsfor "Flip SSE to default transport (P0)" but the actual SSE/WS transport selection logic lives inpackages/client/src/store.ts(whereMitzoConnectionis configured).frontend/src/client-store.tsre-exports from the package. If the flip is just a config default change, the doc should reference the actual file where the transport choice is made.[fixable]
|
|
||
| ```typescript | ||
| // Current | ||
| sessions: Map<sessionId, { connectionId: string; cursor: number }>; |
There was a problem hiding this comment.
🟡 bugs: The "Current" pseudo-code sessions: Map<sessionId, { connectionId, cursor }> misrepresents ConnectionRegistry's actual structure. In reality, the primary map is connections: Map<connectionId, Connection> where each Connection has watchedSessions: Set<string>, and cursors are tracked separately in cursors: Map<connectionId, Map<sessionId, number>>. The relationship is inverted (connection→sessions, not session→connection). The current code already supports multiple connections watching the same session. This means the "Target" change may be smaller than described, and implementers working from this doc will start from the wrong mental model. [fixable]
| sessions: Map<sessionId, { connectionId: string; cursor: number }>; | ||
|
|
||
| // Target | ||
| sessions: Map<sessionId, Set<{ connectionId: string; cursor: number }>>; |
There was a problem hiding this comment.
🟡 bugs: The target type Set<{ connectionId: string; cursor: number }> won't work correctly in TypeScript/JavaScript — Set uses reference equality for objects, making lookup and deletion by value impossible. A Map<string, number> (connectionId → cursor) would be the correct data structure here. Given that the existing ConnectionRegistry already uses per-connection cursor maps, the target should be described in terms of the actual existing structure rather than an invented one. [fixable]
|
|
||
| **Remove:** | ||
|
|
||
| - `wasRunning` branch in `store.sendMessage()` (~50 lines) |
There was a problem hiding this comment.
🟡 unsafe_assumptions: The doc says the wasRunning branch is "~50 lines" but the actual implementation spans lines 339-404 in packages/client/src/store.ts (66 lines). Minor inaccuracy, but an implementer using this doc to scope removal work would underestimate. [fixable]
|
|
||
| **UX note:** The `USER_SEND` reducer action (which renders the user bubble immediately) stays as-is -- the UI remains optimistic. The POST acknowledgment controls whether the message is considered _delivered_, not _displayed_. On POST failure, the existing bubble gets a "failed to send" indicator with retry. This preserves the instant-feel UX while adding delivery confidence. | ||
|
|
||
| **Tests:** Verify send-while-running works (server queues via AsyncQueue). Verify POST failure shows error UI with retry. Verify heartbeat timeout triggers reconnect. Verify no message loss on reconnect during generation. |
There was a problem hiding this comment.
🟡 unsafe_assumptions: Phase 2 prerequisite says idempotency guard storeAndEchoIfNew() lives in server/chat.ts and calls eventStore.hasUserMessage(). This is accurate today, but the doc warns it may have been "refactored or removed" since PR #386 — this safety net is good. However, the Phase 2 plan adds retry with exponential backoff (up to 3 attempts) relying on this dedup, but doesn't specify whether the HTTP POST handler (chat-rest-handler.ts) also needs dedup at the HTTP layer (e.g., returning 202 for already-accepted messages instead of processing them again up to the dedup check). If the dedup is only in storeAndEchoIfNew(), the agent will still receive the message twice via AsyncQueue before dedup fires.
| // DETACHED/SUSPENDED -> preserve last emitted (server buffers) | ||
| // | ||
| // 2. From permission_request v2 events (cross-cutting, not a SessionState): | ||
| // permission_request -> 'requires_action' |
There was a problem hiding this comment.
🔵 style: The SessionStateEvent type comment says DETACHED/SUSPENDED → 'preserve last emitted (server buffers)', but the 3-state type 'idle' | 'running' | 'requires_action' doesn't include a 'no change' sentinel. The prose below explains lastEmittedClientState buffering, but the inline type comment could mislead — someone reading just the type definition might expect a fourth state value. Consider clarifying inline that these server states suppress emission entirely (no event sent) rather than emit a special value. [fixable]
| | `packages/protocol/src/types.ts` | Add `SessionStateEvent` type | | ||
| | `packages/harness/src/connection-registry.ts` | Multi-client fan-out: Set of connections per session (P3); remove periodic sync | | ||
| | `frontend/src/client-store.ts` | Flip SSE to default transport (P0) | | ||
|
|
There was a problem hiding this comment.
🔵 style: The Files Involved table lists frontend/src/client-store.ts for "Flip SSE to default transport (P0)" but the actual SSE/WS transport selection logic lives in packages/client/src/store.ts (where MitzoConnection is configured). frontend/src/client-store.ts re-exports from the package. If the flip is just a config default change, the doc should reference the actual file where the transport choice is made. [fixable]
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 5 issue(s) (2 warning).
docs/design/transport-ssot.md
Well-researched design doc with accurate codebase references. Main concern: Phase 0 items are already implemented but presented as future work, and the DETACHED/SUSPENDED buffering design doesn't match the current implementation — either the doc or the code needs updating to close the gap.
- 🟡 unsafe_assumptions (L141): The design specifies DETACHED/SUSPENDED should 'preserve last emitted' client state via a
lastEmittedClientStatefield per session and suppress emission. However, the already-implementedtoClientState()inpackages/protocol/src/event-store.ts:35-37returns'idle'as a safe default for DETACHED/SUSPENDED with a comment noting it can't track last-emitted state at the EventStore level, and emission is not suppressed. The design should either acknowledge this gap as a Phase 1+ follow-up or be updated to match the current behavior.[fixable] - 🔵 style (L284): Phase 0 items 1-2 (session_state_changed event, emission from setSessionState), item 5 (recoverStaleSessions), items 7-8 (state column, setSessionState writing both columns) are already implemented in the codebase. The migration plan reads as future work but these are completed. Consider marking them as done (e.g., with checkboxes or a 'Status: Complete' annotation) to avoid confusion about what remains.
[fixable] - 🟡 unsafe_assumptions (L238): The ConnectionRegistry multi-client fan-out section shows the target structure as
Map<sessionId, Set<{ connectionId, cursor }>>, but object literals in a Set won't deduplicate by value — Set uses reference equality. This would need to be a Map<sessionId, Map<connectionId, { cursor }>> or similar structure in practice. Since this is a design doc (not code), this is minor, but implementers may copy the pattern verbatim.[fixable] - 🔵 unsafe_assumptions (L185): The HTTP POST send endpoint returns 202 with
{ accepted, sessionId, seq }. The design doesn't specify behavior when the client POSTs to a session that exists in EventStore but has no live process in SessionRegistry (e.g., session in ENDED or DETACHED state without a running agent). Should this return 404, 409, or attempt to restart? This edge case is worth documenting since it's the exact failure mode the design is trying to eliminate.[fixable] - 🔵 style (L375): The Files Involved table lists
frontend/src/client-store.tsfor 'Flip SSE to default transport (P0)', but the table header column says 'File' with paths likeserver/query-loop.ts(no leading slash). The client-store entry is the only frontend file in the Server table — consider moving it to the Client table or adding a note that this is a cross-cutting change.[fixable]
| sessionId: string; | ||
| state: 'idle' | 'running' | 'requires_action'; | ||
| // Two emission paths: | ||
| // |
There was a problem hiding this comment.
🟡 unsafe_assumptions: The design specifies DETACHED/SUSPENDED should 'preserve last emitted' client state via a lastEmittedClientState field per session and suppress emission. However, the already-implemented toClientState() in packages/protocol/src/event-store.ts:35-37 returns 'idle' as a safe default for DETACHED/SUSPENDED with a comment noting it can't track last-emitted state at the EventStore level, and emission is not suppressed. The design should either acknowledge this gap as a Phase 1+ follow-up or be updated to match the current behavior. [fixable]
| 3. Add `state` field to `/api/sessions/:id/meta` response | ||
| 4. Client receives and logs `session_state_changed` but doesn't act on it yet | ||
| 5. Add crash recovery: `recoverStaleSessions()` on server startup | ||
| 6. Flip SSE to default transport (currently WS default, SSE opt-in via localStorage) |
There was a problem hiding this comment.
🔵 style: Phase 0 items 1-2 (session_state_changed event, emission from setSessionState), item 5 (recoverStaleSessions), items 7-8 (state column, setSessionState writing both columns) are already implemented in the codebase. The migration plan reads as future work but these are completed. Consider marking them as done (e.g., with checkboxes or a 'Status: Complete' annotation) to avoid confusion about what remains. [fixable]
| Multiple clients can connect to the same session simultaneously (e.g. phone + laptop). This is designed in from the start, not bolted on later. | ||
|
|
||
| #### Model | ||
|
|
There was a problem hiding this comment.
🟡 unsafe_assumptions: The ConnectionRegistry multi-client fan-out section shows the target structure as Map<sessionId, Set<{ connectionId, cursor }>>, but object literals in a Set won't deduplicate by value — Set uses reference equality. This would need to be a Map<sessionId, Map<connectionId, { cursor }>> or similar structure in practice. Since this is a design doc (not code), this is minor, but implementers may copy the pattern verbatim. [fixable]
| - Write both `state` and `is_active` in `setSessionState()` during transition period | ||
| - Migrate all `is_active` readers to use `state` (Phase 1) | ||
| - Drop `is_active` column once all consumers migrated (Phase 4) | ||
| - Replace `handleReconnect`'s `storeMeta.isActive` check with state-based logic |
There was a problem hiding this comment.
🔵 unsafe_assumptions: The HTTP POST send endpoint returns 202 with { accepted, sessionId, seq }. The design doesn't specify behavior when the client POSTs to a session that exists in EventStore but has no live process in SessionRegistry (e.g., session in ENDED or DETACHED state without a running agent). Should this return 404, 409, or attempt to restart? This edge case is worth documenting since it's the exact failure mode the design is trying to eliminate. [fixable]
|
|
||
| ``` | ||
| // Phase 1 — Server-authoritative state | ||
| - SET_RUNNING action type (replaced by session_state_changed) |
There was a problem hiding this comment.
🔵 style: The Files Involved table lists frontend/src/client-store.ts for 'Flip SSE to default transport (P0)', but the table header column says 'File' with paths like server/query-loop.ts (no leading slash). The client-store entry is the only frontend file in the Server table — consider moving it to the Client table or adding a note that this is a cross-cutting change. [fixable]
Proposes server-authoritative transport architecture to permanently fix the recurring "send twice to wake agent" bug (45+ fix attempts across 3 transport architectures). Based on industry analysis of Anthropic, OpenAI, LibreChat, and Vercel AI SDK patterns. Key changes: remove client-side routing on running state, explicit server state events, unified server-side state machine as SSOT, write acknowledgment, sequence-based resumption, heartbeat liveness. 4-phase migration plan with dead code removal at each phase. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix file paths: connection-registry.ts is in packages/harness/src/ - Change "Supersedes" to "Incorporates and replaces" (prior doc was never implemented) - Move is_active removal from Phase 1 to Phase 3 (handleReconnect still depends on it until Phase 3) - Add idempotency note for POST retries (clientMsgId from PR #386) - Fix dead code section to reflect correct phase assignments Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Clarify two emission paths for session_state_changed (lifecycle transitions from setSessionState vs permission_request events) - Fix diagram to use /api/chat/send path matching codebase - Add SUSPENDED to crash recovery set in recoverStaleSessions() - Fix shouldSync() references to real names: startPeriodicSync() and stopPeriodicSync() - Add Phase 2 prerequisite to verify clientMsgId idempotency Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix idempotency guard location: chat.ts not chat-rest-handler.ts - Clarify streaming-input-session-control.md was implemented (PR #33) - Add explicit DETACHED/SUSPENDED buffering design (lastEmittedClientState) - Note both client store locations for SSE transport flip Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Mark state column as already implemented in Phase 0 - Add is_active/state sync gap risk callout with ordering guidance - Add CLOSING to client-facing state mapping (maps to running) - Clarify UX stays optimistic (USER_SEND renders immediately) - Note AsyncQueue multi-client serialization needs verification - Clarify ConnectionRegistry already has per-session cursors - Fix ~50 lines scope to "across store and parser" Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update Telos reference to correct parent item b138b20e472bd324. Address all 7 findings from 4th Centaur review: - Mark state column as pre-existing in Phase 0 - Add is_active sync gap risk + ordering guidance - Map CLOSING state to 'running' in client events - Clarify optimistic UX preserved (USER_SEND stays immediate) - Note AsyncQueue multi-client assumption needs verification - Clarify ConnectionRegistry existing per-session cursors - Fix ~50 lines scope description Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
be20d3f to
544f97e
Compare
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 4 issue(s) (1 warning).
docs/design/transport-ssot.md
Well-structured design doc with clear problem taxonomy and phased migration. Main concern: the DETACHED/SUSPENDED buffering spec is not implemented — current toClientState() contradicts the design by mapping those states to 'idle' instead of preserving the last emitted state. This will matter at Phase 3 (multi-client fan-out). Three minor suggestions on pseudocode accuracy and phase tracking.
- 🟡 unsafe_assumptions (L129): DETACHED/SUSPENDED buffering described here (
preserve last emitted (server buffers)withlastEmittedClientState) is not implemented. The currenttoClientState()inpackages/protocol/src/event-store.ts:35-36maps both DETACHED and SUSPENDED to'idle'and always emits — no suppression, no buffering. During multi-client fan-out (Phase 3), other clients watching the same session would incorrectly seeidlewhen one client's transport drops while the agent is still running. The doc should note this as an open implementation gap, or the mapping should be fixed before Phase 3.[fixable] - 🔵 style (L125): The
SessionStateEventcomment block mapsCREATED/ENDED -> 'idle'andSTARTING/ACTIVE -> 'running'but omitsCLOSING. The actualtoClientState()implementation maps CLOSING to'idle'. Add it to the CREATED/ENDED line for completeness:CREATED/CLOSING/ENDED -> 'idle'.[fixable] - 🔵 style (L244): The ConnectionRegistry pseudocode shows
sessions: Map<sessionId, { connectionId, cursor }>as the current structure, but the actual implementation is connection-centric:Map<connectionId, Connection>where eachConnectionhaswatchedSessions: Set<string>, with cursors tracked in a separateMap<connectionId, Map<sessionId, number>>. The 'Target' pseudocode should be revised to match the actual data model being extended, not an idealized version that doesn't correspond to the code.[fixable] - 🔵 style (L281): Phase 0 items (state column, recoverStaleSessions, SSE default, session_state_changed emission) and Phase 1 items (syncRunningState removal, SET_RUNNING removal) are already implemented in the codebase but the doc reads as if they are future work. Consider adding completion markers (e.g. checkboxes or a status column) to each phase so readers know the current progress.
[fixable]
| **Current:** Client derives `running` from: | ||
|
|
||
| - `SET_RUNNING` actions dispatched in various handlers | ||
| - `session_end` events (set `running: false`) |
There was a problem hiding this comment.
🟡 unsafe_assumptions: DETACHED/SUSPENDED buffering described here (preserve last emitted (server buffers) with lastEmittedClientState) is not implemented. The current toClientState() in packages/protocol/src/event-store.ts:35-36 maps both DETACHED and SUSPENDED to 'idle' and always emits — no suppression, no buffering. During multi-client fan-out (Phase 3), other clients watching the same session would incorrectly see idle when one client's transport drops while the agent is still running. The doc should note this as an open implementation gap, or the mapping should be fixed before Phase 3. [fixable]
| - `interruptMessage()` guard on `!running` (interrupt only makes sense when running) | ||
|
|
||
| #### 2. Explicit Server State Events | ||
|
|
There was a problem hiding this comment.
🔵 style: The SessionStateEvent comment block maps CREATED/ENDED -> 'idle' and STARTING/ACTIVE -> 'running' but omits CLOSING. The actual toClientState() implementation maps CLOSING to 'idle'. Add it to the CREATED/ENDED line for completeness: CREATED/CLOSING/ENDED -> 'idle'. [fixable]
| - **Concurrent sends are serialized.** If two clients POST simultaneously, AsyncQueue serializes them. The SDK processes one turn at a time. Both clients see both messages and both responses in sequence order. | ||
|
|
||
| #### ConnectionRegistry Changes | ||
|
|
There was a problem hiding this comment.
🔵 style: The ConnectionRegistry pseudocode shows sessions: Map<sessionId, { connectionId, cursor }> as the current structure, but the actual implementation is connection-centric: Map<connectionId, Connection> where each Connection has watchedSessions: Set<string>, with cursors tracked in a separate Map<connectionId, Map<sessionId, number>>. The 'Target' pseudocode should be revised to match the actual data model being extended, not an idealized version that doesn't correspond to the code. [fixable]
|
|
||
| 1. Add `session_state_changed` event to the protocol | ||
| 2. Emit it from `setSessionState()` alongside existing events | ||
| 3. Add `state` field to `/api/sessions/:id/meta` response |
There was a problem hiding this comment.
🔵 style: Phase 0 items (state column, recoverStaleSessions, SSE default, session_state_changed emission) and Phase 1 items (syncRunningState removal, SET_RUNNING removal) are already implemented in the codebase but the doc reads as if they are future work. Consider adding completion markers (e.g. checkboxes or a status column) to each phase so readers know the current progress. [fixable]
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 6 issue(s) (4 warning).
docs/design/transport-ssot.md
Design doc is well-structured and the architecture is sound, but several claims about the current codebase are stale — key Phase 0/1 items (SSE default, SESSION_STATE_CHANGED action type, state column) appear already implemented, and the ConnectionRegistry data model description is inaccurate. These should be corrected before implementation begins to avoid confusion.
- 🟡 bugs (L131): Section 2 claims the client uses
SET_RUNNINGactions and lists removingSET_RUNNINGas Phase 1 dead code. However, the codebase already usesSESSION_STATE_CHANGEDaction type (inpackages/client/src/slices/messages.ts:160) with the exact 3-state model (idle | running | requires_action) this doc proposes. The protocol-parser already dispatches{ type: 'SESSION_STATE_CHANGED', state }in multiple places and the reducer derivesrunningfrom it. Phase 1 steps 1-2 appear to be already implemented — the doc describes the current state as the target state, which will confuse implementors.[fixable] - 🟡 bugs (L258): Phase 0 step 6 ('Flip SSE to default transport') is already done.
frontend/src/client-store.tslines 19-26 explicitly document SSE + HTTP POST as the default transport with a comment: 'This is an intentional flip from WS-default per the transport-ssot design doc.' The migration plan should note which steps are already complete vs. remaining.[fixable] - 🟡 bugs (L225): The ConnectionRegistry current-state description ('maps one connection per session (owner model)') is inaccurate. The actual model in
packages/harness/src/connection-registry.tsis one connection per browser tab/client, where each connection watches multiple sessions and has one active session. TheConnectionRegistryusesconnections: Map<string, Connection>(keyed by connectionId) withwatchedSessions: Set<string>per connection — not a session→connection map. The proposed 'Target' change toMap<sessionId, Set<...>>is actually inverting the current key structure, which is a larger change than described.[fixable] - 🟡 unsafe_assumptions (L259): Phase 0 step 7 ('Add state column to EventStore sessions table alongside is_active') may already be partially done. The EventStore in
packages/protocol/src/event-store.tsalready has astateproperty in SessionMeta (line 80) and asetSessionState()method (line 609). The doc should clarify what's already present vs. what needs to be added to avoid redundant migration work.[fixable] - 🔵 style (L364): The 'Files Involved' table lists
frontend/src/client-store.tsfor Phase 0 ('Flip SSE to default transport'), but this is already implemented per that file's current contents. Consider adding a 'Status' column to the files table or marking completed items, so implementors can distinguish completed from remaining work.[fixable] - 🔵 style (L373): The 'Dead Code Candidates' section lists
SET_RUNNING action typefor Phase 1 removal, but this action type doesn't exist in the codebase (onlySESSION_STATE_CHANGEDdoes). This entry should be removed or corrected to reflect the actual dead code that needs cleaning up.[fixable]
| - `SET_RUNNING` actions dispatched in various handlers | ||
| - `session_end` events (set `running: false`) | ||
| - `reconnected` message (carries `running: boolean`) | ||
| - `syncRunningState()` REST polling on foreground |
There was a problem hiding this comment.
🟡 bugs: Section 2 claims the client uses SET_RUNNING actions and lists removing SET_RUNNING as Phase 1 dead code. However, the codebase already uses SESSION_STATE_CHANGED action type (in packages/client/src/slices/messages.ts:160) with the exact 3-state model (idle | running | requires_action) this doc proposes. The protocol-parser already dispatches { type: 'SESSION_STATE_CHANGED', state } in multiple places and the reducer derives running from it. Phase 1 steps 1-2 appear to be already implemented — the doc describes the current state as the target state, which will confuse implementors. [fixable]
| ``` | ||
|
|
||
| Every `session_state_changed` and protocol event fans out to all connections in the set. Connection add/remove is independent -- one client disconnecting doesn't affect others. | ||
|
|
There was a problem hiding this comment.
🟡 bugs: Phase 0 step 6 ('Flip SSE to default transport') is already done. frontend/src/client-store.ts lines 19-26 explicitly document SSE + HTTP POST as the default transport with a comment: 'This is an intentional flip from WS-default per the transport-ssot design doc.' The migration plan should note which steps are already complete vs. remaining. [fixable]
|
|
||
| #### 6. Heartbeat Liveness | ||
|
|
||
| **Current:** Various heartbeat mechanisms across transports, inconsistent. |
There was a problem hiding this comment.
🟡 bugs: The ConnectionRegistry current-state description ('maps one connection per session (owner model)') is inaccurate. The actual model in packages/harness/src/connection-registry.ts is one connection per browser tab/client, where each connection watches multiple sessions and has one active session. The ConnectionRegistry uses connections: Map<string, Connection> (keyed by connectionId) with watchedSessions: Set<string> per connection — not a session→connection map. The proposed 'Target' change to Map<sessionId, Set<...>> is actually inverting the current key structure, which is a larger change than described. [fixable]
|
|
||
| Every `session_state_changed` and protocol event fans out to all connections in the set. Connection add/remove is independent -- one client disconnecting doesn't affect others. | ||
|
|
||
| #### Event Fan-Out |
There was a problem hiding this comment.
🟡 unsafe_assumptions: Phase 0 step 7 ('Add state column to EventStore sessions table alongside is_active') may already be partially done. The EventStore in packages/protocol/src/event-store.ts already has a state property in SessionMeta (line 80) and a setSessionState() method (line 609). The doc should clarify what's already present vs. what needs to be added to avoid redundant migration work. [fixable]
| ### Client | ||
|
|
||
| | File | Changes | | ||
| | ---------------------------------------- | -------------------------------------------------------------------------------- | |
There was a problem hiding this comment.
🔵 style: The 'Files Involved' table lists frontend/src/client-store.ts for Phase 0 ('Flip SSE to default transport'), but this is already implemented per that file's current contents. Consider adding a 'Status' column to the files table or marking completed items, so implementors can distinguish completed from remaining work. [fixable]
|
|
||
| ### Dead Code Candidates (by phase) | ||
|
|
||
| ``` |
There was a problem hiding this comment.
🔵 style: The 'Dead Code Candidates' section lists SET_RUNNING action type for Phase 1 removal, but this action type doesn't exist in the codebase (only SESSION_STATE_CHANGED does). This entry should be removed or corrected to reflect the actual dead code that needs cleaning up. [fixable]
Summary
Resolved Decisions
statealongsideis_active, migrate readers, dropis_activein P4MITZO_HEARTBEAT_INTERVAL_MSTest plan
🤖 Generated with Claude Code