The backend stamps `seq` once per event before its transport fan-out, so the same frame arriving on two of the renderer's sockets to one process carries the same (session_id, seq); `JsonRpcGateway` is per socket and cannot see the other copy. `JsonRpcGateway.dispatchEvent` now tags each event with the `replay_epoch` its socket adopted from `gateway.ready` (per process, not per socket), and both fan-ins in `use-gateway-boot.ts` (primary + registry secondaries) pass through one `createGatewayEventDedupe()` gate keyed by (epoch, session_id, seq) before any store runs. A duplicate is the same key within 30 s. Not "seq not above the highest seen": the backend restarts a session's counter at 1 when the session leaves its 64-session replay ring, and a high-water mark would swallow the whole restart of any short session. Seq-less / session-less events always pass. Bounded: 2048 seqs per session, 256 sessions LRU. Live, with the router fix reverted so two sockets still join the chat: the DOM never doubled a word (0/38 samples vs 4/40 on main) and the doubled "reply was cut short" card is back to one. Closes #120007. Part of #120005.
59 lines
2.8 KiB
TypeScript
59 lines
2.8 KiB
TypeScript
/**
|
|
* Wire types for the `tui_gateway` JSON-RPC surface shared by the Ink TUI, Desktop and the
|
|
* web dashboard.
|
|
*
|
|
* Every shape here is GENERATED from `tui_gateway/contracts` (Python is the single source):
|
|
* `./gateway-contract.generated.ts` carries `RpcMethods` (client→server method → params/result),
|
|
* `ServerRequestMap` (server→client request → params/result), `GatewayEventMap` (notification
|
|
* type → payload) and every value shape. `scripts/gen_gateway_contracts.py` regenerates it and
|
|
* `tests/tui_gateway/contracts/test_generated.py` fails when the committed file is stale, so a field the
|
|
* backend stops sending fails `tsc` here instead of drifting.
|
|
*
|
|
* This module adds only what the wire does not carry: the client-local synthetic events the TUI
|
|
* transport publishes into the same handler stream, and the `GatewayEvent` envelope.
|
|
*/
|
|
import type { BackendGatewayEventMap } from './gateway-contract.generated.js'
|
|
|
|
export * from './gateway-contract.generated.js'
|
|
|
|
/**
|
|
* Client-local synthetic events. Never emitted by `tui_gateway`; the Ink TUI's `gatewayClient`
|
|
* publishes them into the same handler stream to report transport state.
|
|
*/
|
|
export interface ClientLocalGatewayEventMap {
|
|
'dashboard.new_session_requested': { reason?: string }
|
|
'gateway.protocol_error': { preview?: string }
|
|
'gateway.reconnecting': { attempt?: number; delay_ms?: number }
|
|
'gateway.start_timeout': { cwd?: string; python?: string; stderr_tail?: string }
|
|
'gateway.stderr': { line: string }
|
|
}
|
|
|
|
export interface GatewayEventMap extends BackendGatewayEventMap, ClientLocalGatewayEventMap {}
|
|
|
|
export type GatewayEventName = keyof GatewayEventMap
|
|
|
|
/** One `event` notification's `params`. */
|
|
export interface GatewayEvent<K extends GatewayEventName = GatewayEventName> {
|
|
/** Client-local: recovered/held during reconnect, not fresh user-facing work. */
|
|
replayed?: boolean
|
|
/** Client-local: the backend process's `replay_epoch` the delivering socket had adopted
|
|
* (from `gateway.ready`) when it dispatched this event. Two sockets to one process share
|
|
* it, so a renderer can recognise the same frame arriving on both. */
|
|
replayEpoch?: string
|
|
/** Registry connection whose socket delivered the event (renderer-side tag;
|
|
* absent for the local/legacy primary path). */
|
|
connectionId?: string
|
|
payload?: GatewayEventMap[K]
|
|
/** Renderer-side source tag added by the Desktop gateway registry. */
|
|
profile?: string
|
|
/** Per-session monotonic counter stamped by `tui_gateway/event_replay.py::_stamp_event`;
|
|
* absent on session-less broadcasts. */
|
|
seq?: number
|
|
session_id?: string
|
|
type: K
|
|
}
|
|
|
|
/** Backend-emitted notification names (generated `GATEWAY_EVENT_TYPES`), re-exported under the
|
|
* name the consumers already use. */
|
|
export { GATEWAY_EVENT_TYPES as BACKEND_EVENT_NAMES } from './gateway-contract.generated.js'
|