@wrongstack/webui-server 0.309.1 → 0.310.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +18031 -18795
  3. package/dist/server/collab/annotations.d.ts +26 -0
  4. package/dist/server/collab/broadcast-scheduler.d.ts +30 -0
  5. package/dist/server/collab/collab-context.d.ts +60 -0
  6. package/dist/server/collab/controller.d.ts +25 -0
  7. package/dist/server/collab/dispatcher.d.ts +27 -0
  8. package/dist/server/collab/injection.d.ts +26 -0
  9. package/dist/server/collab/membership.d.ts +20 -0
  10. package/dist/server/collab/mirror.d.ts +14 -0
  11. package/dist/server/collab/replay.d.ts +20 -0
  12. package/dist/server/collab/session-registry.d.ts +74 -0
  13. package/dist/server/collaboration-ws-handler.d.ts +28 -149
  14. package/dist/server/embedded-host-adapters.d.ts +4 -4
  15. package/dist/server/entry.js +16332 -16840
  16. package/dist/server/frontend-static-serve.d.ts +2 -0
  17. package/dist/server/index.d.ts +25 -25
  18. package/dist/server/pre-context-services.d.ts +2 -1
  19. package/dist/server/provider/catalog.d.ts +15 -0
  20. package/dist/server/provider/custom-models.d.ts +13 -0
  21. package/dist/server/provider/keys-records.d.ts +2 -0
  22. package/dist/server/provider/keys.d.ts +36 -0
  23. package/dist/server/provider/mutations.d.ts +34 -0
  24. package/dist/server/provider/oauth.d.ts +17 -0
  25. package/dist/server/provider/probe.d.ts +9 -0
  26. package/dist/server/provider/projection.d.ts +50 -0
  27. package/dist/server/provider-handlers.d.ts +38 -87
  28. package/dist/server/standalone-session-identity.d.ts +2 -1
  29. package/dist/server/token-estimator.d.ts +19 -8
  30. package/package.json +13 -16
  31. package/dist/protocol/client-conversation.d.ts +0 -3
  32. package/dist/protocol/client-integrations.d.ts +0 -3
  33. package/dist/protocol/client-operations.d.ts +0 -3
  34. package/dist/protocol/client-workspace.d.ts +0 -3
  35. package/dist/protocol/connection-fsm.d.ts +0 -39
  36. package/dist/protocol/decoder.d.ts +0 -6
  37. package/dist/protocol/index.d.ts +0 -8
  38. package/dist/protocol/index.js +0 -960
  39. package/dist/protocol/projections.d.ts +0 -131
  40. package/dist/protocol/registry.d.ts +0 -6
  41. package/dist/protocol/replay-payload.d.ts +0 -41
  42. package/dist/protocol/server-conversation.d.ts +0 -3
  43. package/dist/protocol/server-integrations.d.ts +0 -3
  44. package/dist/protocol/server-operations.d.ts +0 -4
  45. package/dist/protocol/server-workspace.d.ts +0 -3
  46. package/dist/protocol/types.d.ts +0 -24
  47. package/dist/protocol/version.d.ts +0 -26
@@ -0,0 +1,26 @@
1
+ import type { WebSocket } from 'ws';
2
+ import type { CollabContext } from './collab-context.js';
3
+ import type { CollabFeature } from './dispatcher.js';
4
+ /**
5
+ * Annotation flow, Phase 2 (6A-2). Moved verbatim from
6
+ * CollaborationWebSocketHandler: store-availability guard, annotator role
7
+ * gate, payload validation, session pinning, and the broadcast payloads.
8
+ *
9
+ * Deliberate invariant: neither flow calls scheduler.record(). The periodic
10
+ * 2s collab.state tick only re-sends when the participant-set fingerprint
11
+ * changes (join/leave), and annotations do not alter that set — so a record()
12
+ * here would write back the identical fingerprint and change nothing.
13
+ * Annotation payloads are event-stream messages delivered by the direct
14
+ * broadcast plus the replay ring buffer, not collab.state. Do not "fix" this
15
+ * asymmetry without first widening what stateFingerprint covers.
16
+ */
17
+ export declare class AnnotateFeature implements CollabFeature {
18
+ readonly type = "collab.annotate";
19
+ handle(ctx: CollabContext, ws: WebSocket, raw: unknown): Promise<void>;
20
+ }
21
+ /** `collab.resolve` — resolves an annotation and broadcasts the update. */
22
+ export declare class ResolveFeature implements CollabFeature {
23
+ readonly type = "collab.resolve";
24
+ handle(ctx: CollabContext, ws: WebSocket, raw: unknown): Promise<void>;
25
+ }
26
+ //# sourceMappingURL=annotations.d.ts.map
@@ -0,0 +1,30 @@
1
+ import type { CollabSessionRegistry } from './session-registry.js';
2
+ /**
3
+ * CollabBroadcastScheduler — owns the 2s periodic `collab.state` broadcast
4
+ * (6A-4). The interval only re-sends state when a session's participant-set
5
+ * fingerprint changed (join/leave), so an idle session costs one tiny string
6
+ * build per tick instead of a serialize + fan-out. The eager fingerprint
7
+ * record on join/leave is `record()`; the interval compares against it.
8
+ *
9
+ * Extracted behavior-preserving from CollaborationWebSocketHandler:
10
+ * ensure/stop semantics (including `.unref?.()` and fingerprint clearing
11
+ * on stop) are the originals.
12
+ */
13
+ export declare class CollabBroadcastScheduler {
14
+ private readonly registry;
15
+ /** Invoked when a session's state went stale since the last broadcast. */
16
+ private readonly onStaleState;
17
+ private interval;
18
+ private readonly fingerprints;
19
+ constructor(registry: CollabSessionRegistry,
20
+ /** Invoked when a session's state went stale since the last broadcast. */
21
+ onStaleState: (sessionId: string) => void);
22
+ /** Start the periodic loop if not already running (idempotent). */
23
+ ensure(): void;
24
+ /** Record the just-broadcast fingerprint so the tick skips unchanged state. */
25
+ record(sessionId: string): void;
26
+ /** Drop a session's fingerprint when its bucket emptied (leave path). */
27
+ forget(sessionId: string): void;
28
+ stop(): void;
29
+ }
30
+ //# sourceMappingURL=broadcast-scheduler.d.ts.map
@@ -0,0 +1,60 @@
1
+ import type { CollaborationBus, ConsumedInjectionInfo } from '@wrongstack/core/coordination';
2
+ import type { AnnotationsStore, SessionReader } from '@wrongstack/core/storage';
3
+ import type { Logger } from '@wrongstack/core/types';
4
+ import type { WebSocket } from 'ws';
5
+ import type { WSCollabState, WSServerMessage } from '../types.js';
6
+ import type { CollabBroadcastScheduler } from './broadcast-scheduler.js';
7
+ import type { CollabSessionRegistry, Participant } from './session-registry.js';
8
+ export interface CollaborationHandlerOptions {
9
+ /**
10
+ * Resolve the session currently owned by this WebUI runtime. When supplied,
11
+ * joins and live EventBus mirrors are constrained to that session.
12
+ */
13
+ getActiveSessionId?: (() => string) | undefined;
14
+ /**
15
+ * Host-owned authorization for roles that can mutate collaboration state.
16
+ * Omission is intentionally fail-closed: clients may only join as observers.
17
+ */
18
+ authorizeRole?: ((input: {
19
+ ws: WebSocket;
20
+ sessionId: string;
21
+ requestedRole: Exclude<Participant['role'], 'observer'>;
22
+ }) => boolean) | undefined;
23
+ }
24
+ /**
25
+ * Shared wiring every collab feature module operates on. Extracted from
26
+ * CollaborationWebSocketHandler (6A-2): features never reach back into the
27
+ * transport class — they speak to this context, which the handler owns.
28
+ */
29
+ export interface CollabContext {
30
+ readonly registry: CollabSessionRegistry;
31
+ readonly scheduler: CollabBroadcastScheduler;
32
+ readonly logger: Logger;
33
+ readonly reader?: SessionReader | undefined;
34
+ readonly annotations?: AnnotationsStore | undefined;
35
+ readonly bus?: CollaborationBus | undefined;
36
+ readonly options: CollaborationHandlerOptions;
37
+ send(ws: WebSocket, msg: WSServerMessage): void;
38
+ broadcast(sessionId: string, msg: WSServerMessage): void;
39
+ /** Eager state broadcast + fingerprint record (join/leave path). */
40
+ broadcastState(sessionId: string): void;
41
+ stateMessage(sessionId: string): WSCollabState;
42
+ errorMessage(detail: string): WSServerMessage;
43
+ /** Detach close/error listeners tracked by the transport (leave path). */
44
+ detachSocket(ws: WebSocket): void;
45
+ /**
46
+ * Full socket teardown shared by `collab.leave` and WS close/error:
47
+ * client removal, listener detach, leave choreography, and the
48
+ * stop-broadcast-when-empty check. (Original handleDisconnect.)
49
+ */
50
+ removeSocket(ws: WebSocket): void;
51
+ }
52
+ export type { CollaborationBus, ConsumedInjectionInfo };
53
+ /**
54
+ * The 5-step guard boilerplate every privileged handler repeats, collapsed
55
+ * into one call (6A-2): store/bus availability stays in each feature (its
56
+ * error text names the feature), this helper owns join + role checks.
57
+ * Returns the participant on success; null after sending the error itself.
58
+ */
59
+ export declare function requireJoinedRole(ctx: CollabContext, ws: WebSocket, action: string, role: 'annotator' | 'controller'): Participant | null;
60
+ //# sourceMappingURL=collab-context.d.ts.map
@@ -0,0 +1,25 @@
1
+ import type { WebSocket } from 'ws';
2
+ import type { WSServerMessage } from '../types.js';
3
+ import type { CollabContext } from './collab-context.js';
4
+ import type { CollabFeature } from './dispatcher.js';
5
+ /** How long the middleware waits before auto-resuming (mirrors the middleware default). */
6
+ export declare const PAUSE_TIMEOUT_MS = 60000;
7
+ /**
8
+ * Controller flow, Phase 3 (6A-2). Moved verbatim from
9
+ * CollaborationWebSocketHandler: pause/resume/grant_control guards and
10
+ * broadcast payloads, including the "already paused" state surface.
11
+ */
12
+ export declare class RequestPauseFeature implements CollabFeature {
13
+ readonly type = "collab.request_pause";
14
+ handle(ctx: CollabContext, ws: WebSocket, raw: unknown): Promise<void>;
15
+ }
16
+ export declare class ResumeFeature implements CollabFeature {
17
+ readonly type = "collab.resume";
18
+ handle(ctx: CollabContext, ws: WebSocket, raw: unknown): Promise<void>;
19
+ }
20
+ export declare class GrantControlFeature implements CollabFeature {
21
+ readonly type = "collab.grant_control";
22
+ handle(ctx: CollabContext, ws: WebSocket, raw: unknown): Promise<void>;
23
+ }
24
+ export type { WSServerMessage };
25
+ //# sourceMappingURL=controller.d.ts.map
@@ -0,0 +1,27 @@
1
+ import type { WebSocket } from 'ws';
2
+ import type { CollabContext } from './collab-context.js';
3
+ /** A parsed inbound client message off the collaboration wire. */
4
+ export interface CollabInboundMessage {
5
+ type: string;
6
+ payload?: unknown | undefined;
7
+ }
8
+ /**
9
+ * One collaboration protocol feature: owns a single message type and its
10
+ * full handling. Async flows fire-and-forget inside `handle` (the original
11
+ * dispatcher ignored the returned promise with `void`).
12
+ */
13
+ export interface CollabFeature {
14
+ readonly type: string;
15
+ handle(ctx: CollabContext, ws: WebSocket, payload: unknown): void;
16
+ }
17
+ /**
18
+ * CollabDispatcher (6A-3) — maps message types to features. Returns true
19
+ * when the message was recognized and handled; false when the caller should
20
+ * ignore / log, so the upstream router can dispatch non-collab messages.
21
+ */
22
+ export declare class CollabDispatcher {
23
+ private readonly byType;
24
+ constructor(features: readonly CollabFeature[]);
25
+ dispatch(ctx: CollabContext, ws: WebSocket, msg: CollabInboundMessage): boolean;
26
+ }
27
+ //# sourceMappingURL=dispatcher.d.ts.map
@@ -0,0 +1,26 @@
1
+ import type { ConsumedInjectionInfo } from '@wrongstack/core/coordination';
2
+ import type { WebSocket } from 'ws';
3
+ import type { CollabContext } from './collab-context.js';
4
+ import type { CollabFeature } from './dispatcher.js';
5
+ /**
6
+ * Phase 4 — controller's manual tool-call injection (6A-2). Moved verbatim
7
+ * from CollaborationWebSocketHandler: the 5-field payload gate, the
8
+ * queued-duplicate rejection, and the `'(pending match)'` placeholder the
9
+ * middleware later replaces on the `consumed` event.
10
+ */
11
+ export declare class InjectToolFeature implements CollabFeature {
12
+ readonly type = "collab.inject_tool";
13
+ handle(ctx: CollabContext, ws: WebSocket, raw: unknown): Promise<void>;
14
+ }
15
+ /**
16
+ * Bus callback: a queued injection was spliced into a real tool call. Re-emit
17
+ * `collab.injection.granted` with phase `'consumed'` and the now-known tool
18
+ * name. The injection carries no sessionId, so resolve it from the author's
19
+ * current participant. If the author has left, fail closed rather than leak
20
+ * the payload to unrelated sessions.
21
+ *
22
+ * Memory-pinned regression: the injection-listener cleanup coverage relies on
23
+ * this callback being registered via the handler-owned `offs` disposer.
24
+ */
25
+ export declare function broadcastInjectionConsumed(ctx: CollabContext, info: ConsumedInjectionInfo): void;
26
+ //# sourceMappingURL=injection.d.ts.map
@@ -0,0 +1,20 @@
1
+ import type { WebSocket } from 'ws';
2
+ import type { CollabContext } from './collab-context.js';
3
+ import type { CollabFeature } from './dispatcher.js';
4
+ /**
5
+ * Join/leave flow (6A-2). Moved verbatim from CollaborationWebSocketHandler:
6
+ * the join validation chain (active-session pin → no-double-join →
7
+ * role-store/bus availability → fail-closed authorizeRole) and the ordered
8
+ * leave choreography are behavior-critical; the 34-test suite pins them.
9
+ */
10
+ export declare class MembershipFeature implements CollabFeature {
11
+ readonly type = "collab.join";
12
+ handle(ctx: CollabContext, ws: WebSocket, raw: unknown): void;
13
+ private join;
14
+ }
15
+ /** `collab.leave` — identical semantics to a WS close (memory-pinned by tests). */
16
+ export declare class LeaveFeature implements CollabFeature {
17
+ readonly type = "collab.leave";
18
+ handle(ctx: CollabContext, ws: WebSocket): void;
19
+ }
20
+ //# sourceMappingURL=membership.d.ts.map
@@ -0,0 +1,14 @@
1
+ import type { EventBus } from '@wrongstack/core/kernel';
2
+ import type { CollabContext } from './collab-context.js';
3
+ /**
4
+ * Live kernel-event mirror (6A-2). Subscribes to the events an observer
5
+ * cares about and forwards each to all joined participants as a generic
6
+ * `collab.event` envelope so the client can render a flowing activity
7
+ * strip. Filtering / denormalization happens on the client.
8
+ *
9
+ * Moved verbatim from CollaborationWebSocketHandler.subscribe(); the
10
+ * disposers are pushed into the caller-owned `offs` array so dispose()
11
+ * removes every subscription.
12
+ */
13
+ export declare function subscribeCollabMirror(ctx: CollabContext, events: EventBus, offs: Array<() => void>): void;
14
+ //# sourceMappingURL=mirror.d.ts.map
@@ -0,0 +1,20 @@
1
+ import type { WebSocket } from 'ws';
2
+ import type { CollabContext } from './collab-context.js';
3
+ /** How many historical events to replay to a late-joining observer. */
4
+ export declare const REPLAY_LIMIT = 50;
5
+ /**
6
+ * Replay the last `REPLAY_LIMIT` events from the on-disk session log
7
+ * to a single observer (the late joiner). Each event is forwarded as
8
+ * a `collab.event` with `replay: true` so the client can distinguish
9
+ * history from the live stream.
10
+ *
11
+ * The session log stores typed `SessionEvent`s (`user_input`,
12
+ * `llm_response`, `tool_result`, etc.) — different from the kernel's
13
+ * bus events. We translate the most useful subset (`tool.*` and
14
+ * `iteration.*`-shaped ones) into the same `kind` namespace the live
15
+ * mirror uses, so the client can render a single activity strip.
16
+ *
17
+ * Moved verbatim from CollaborationWebSocketHandler (6A-2).
18
+ */
19
+ export declare function replayHistory(ctx: CollabContext, ws: WebSocket, sessionId: string): Promise<void>;
20
+ //# sourceMappingURL=replay.d.ts.map
@@ -0,0 +1,74 @@
1
+ import type { WebSocket } from 'ws';
2
+ import type { CollabRole, WSCollabState } from '../types.js';
3
+ /**
4
+ * Session-scoped membership record for one collaboration participant.
5
+ * Moved verbatim from collaboration-ws-handler.ts (6A-1); the shape is
6
+ * wire-visible via `collab.state` payloads, so field names are frozen.
7
+ */
8
+ export interface Participant {
9
+ participantId: string;
10
+ ws: WebSocket;
11
+ sessionId: string;
12
+ role: CollabRole;
13
+ joinedAt: string;
14
+ }
15
+ /** One removal produced by `removeBySocket`, with leave-choreography facts. */
16
+ export interface SocketRemoval {
17
+ sessionId: string;
18
+ participant: Participant;
19
+ /** True when this removal emptied the session bucket (no broadcast due). */
20
+ sessionEmptied: boolean;
21
+ }
22
+ /**
23
+ * CollabSessionRegistry — the in-memory membership/state store for the
24
+ * collaboration transport. Pure state: no sockets are written to, no
25
+ * timers run, no messages are sent from here. Send/broadcast orchestration
26
+ * stays in the handler + feature modules so ordering semantics
27
+ * (left-event → remove → broadcast) remain in one visible place.
28
+ *
29
+ * Extracted behavior-preserving from CollaborationWebSocketHandler (6A-1):
30
+ * the maps, lookups, and state projections below are line-for-line the
31
+ * originals; only the surrounding class changed.
32
+ */
33
+ export declare class CollabSessionRegistry {
34
+ /** Sockets attached to the collab endpoint (joined or not). */
35
+ readonly clients: Set<WebSocket>;
36
+ /** sessionId → participants currently watching it. */
37
+ private readonly bySession;
38
+ /** True while at least one collaboration participant is attached to a session. */
39
+ hasParticipants(sessionId: string): boolean;
40
+ /** True while ANY session still has a participant (hot-path guard for the mirror). */
41
+ hasAnyParticipants(): boolean;
42
+ /** Register a participant in its session bucket, creating the bucket on demand. */
43
+ addParticipant(participant: Participant): void;
44
+ /**
45
+ * Remove every participant riding the given socket (a socket joins at
46
+ * most one session today, but the loop stays future-proof like the
47
+ * original handleDisconnect). Returns the removals in map order with
48
+ * the owning sessionId so the caller can run the ordered leave
49
+ * choreography (confirm → broadcast → state) exactly as before.
50
+ */
51
+ removeBySocket(ws: WebSocket): SocketRemoval[];
52
+ /**
53
+ * Look up the participant record for a given WS across all sessions.
54
+ * Returns null when the WS hasn't joined (e.g. the client sent a
55
+ * `collab.annotate` before `collab.join`).
56
+ */
57
+ findParticipant(ws: WebSocket): Participant | null;
58
+ findParticipantById(sessionId: string, participantId: string): Participant | null;
59
+ /** Session ids with at least one participant (snapshot for iteration). */
60
+ sessionIds(): string[];
61
+ /** Live iterator over one session's participants (no copy). */
62
+ participantIterator(sessionId: string): IterableIterator<Participant> | undefined;
63
+ stateMessage(sessionId: string): WSCollabState;
64
+ /**
65
+ * Cheap participant-set identity: participant ids are unique per join and
66
+ * `joinedAt` distinguishes a rejoin, so the fingerprint changes exactly on
67
+ * join/leave — the only events that alter `collab.state`.
68
+ */
69
+ stateFingerprint(sessionId: string): string;
70
+ /** Resolve the session a participant id currently lives in, if any. */
71
+ sessionIdOfParticipant(participantId: string): string | null;
72
+ clear(): void;
73
+ }
74
+ //# sourceMappingURL=session-registry.d.ts.map
@@ -3,38 +3,20 @@ import type { EventBus } from '@wrongstack/core/kernel';
3
3
  import type { AnnotationsStore, SessionReader } from '@wrongstack/core/storage';
4
4
  import type { Logger } from '@wrongstack/core/types';
5
5
  import type { WebSocket } from 'ws';
6
- import type { CollabRole } from './types.js';
7
- export interface CollaborationHandlerOptions {
8
- /**
9
- * Resolve the session currently owned by this WebUI runtime. When supplied,
10
- * joins and live EventBus mirrors are constrained to that session.
11
- */
12
- getActiveSessionId?: (() => string) | undefined;
13
- /**
14
- * Host-owned authorization for roles that can mutate collaboration state.
15
- * Omission is intentionally fail-closed: clients may only join as observers.
16
- */
17
- authorizeRole?: ((input: {
18
- ws: WebSocket;
19
- sessionId: string;
20
- requestedRole: Exclude<CollabRole, 'observer'>;
21
- }) => boolean) | undefined;
22
- }
6
+ import type { CollaborationHandlerOptions } from './collab/collab-context.js';
7
+ import { type CollabInboundMessage } from './collab/dispatcher.js';
8
+ export type { CollaborationHandlerOptions };
23
9
  /**
24
10
  * CollaborationWebSocketHandler — session-scoped collaboration transport.
25
11
  * Mirrors `WorktreeWebSocketHandler` and `GoalWebSocketHandler`.
26
12
  *
27
- * Capabilities in this phase:
28
- * - A second human (or any client) joins an active agent run as an
29
- * `observer` and receives a live mirror of the kernel's iteration /
30
- * tool / subagent events.
31
- * - The observer declares a `sessionId` on join. Hosts provide the live
32
- * runtime session id so replay and EventBus routing stay session-scoped.
33
- * - The observer can leave at any time; cleanup runs on WS close/error.
34
- * - Privileged roles are never accepted from the wire by themselves.
35
- * The host must explicitly authorize annotator/controller grants.
13
+ * Split (6A): this file is now the thin transport + wiring shell; the
14
+ * protocol lives in collab/ feature modules dispatched by CollabDispatcher.
15
+ * Behavior is preserved verbatim the 34-test suite in
16
+ * tests/collaboration-ws-handler.test.ts pins the wire protocol, replay
17
+ * semantics, and the dispose/leave ordering.
36
18
  *
37
- * Protocol additions (see `packages/webui/src/types.ts`):
19
+ * Protocol (see `packages/webui/src/types.ts`):
38
20
  * client → server: collab.join { sessionId, role: 'observer' }
39
21
  * collab.leave { sessionId }
40
22
  * server → client: collab.state (initial + 2s periodic)
@@ -45,145 +27,42 @@ export interface CollaborationHandlerOptions {
45
27
  export declare class CollaborationWebSocketHandler {
46
28
  private readonly events;
47
29
  private readonly logger;
48
- /**
49
- * Optional reader over the on-disk session log. When provided, late
50
- * joiners receive the last `REPLAY_LIMIT` events of the joined
51
- * session before live mirroring begins. Without a reader, joining
52
- * is still allowed — the observer simply starts from "now" with no
53
- * historical context.
54
- */
55
30
  private readonly reader?;
56
- /**
57
- * Optional sidecar store for collaboration annotations. Required
58
- * for the `annotator` role — without it, `collab.annotate` messages
59
- * are rejected with an error.
60
- */
61
31
  private readonly annotations?;
62
- /**
63
- * Optional kernel-level pause/resume bus. Required for the
64
- * `controller` role — without it, `collab.request_pause` is rejected
65
- * with an error. Wired to the agent's `toolCall` pipeline via
66
- * `collabPauseMiddleware` in the webui server boot.
67
- */
68
32
  private readonly bus?;
69
33
  private readonly options;
70
- private readonly clients;
71
- /** sessionId → participants currently watching it. */
72
- private readonly bySession;
73
- private broadcastInterval;
74
- /**
75
- * Last-broadcast participant-set fingerprint per session. The 2s interval
76
- * only re-sends `collab.state` when the fingerprint changed (join/leave),
77
- * so an idle session costs one tiny string build per tick instead of a
78
- * serialize + fan-out.
79
- */
80
- private readonly lastStateFingerprints;
34
+ private readonly registry;
35
+ private readonly scheduler;
36
+ private readonly dispatcher;
81
37
  private readonly offs;
82
- constructor(events: EventBus, logger: Logger,
83
- /**
84
- * Optional reader over the on-disk session log. When provided, late
85
- * joiners receive the last `REPLAY_LIMIT` events of the joined
86
- * session before live mirroring begins. Without a reader, joining
87
- * is still allowed — the observer simply starts from "now" with no
88
- * historical context.
89
- */
90
- reader?: SessionReader | undefined,
91
- /**
92
- * Optional sidecar store for collaboration annotations. Required
93
- * for the `annotator` role — without it, `collab.annotate` messages
94
- * are rejected with an error.
95
- */
96
- annotations?: AnnotationsStore | undefined,
97
- /**
98
- * Optional kernel-level pause/resume bus. Required for the
99
- * `controller` role — without it, `collab.request_pause` is rejected
100
- * with an error. Wired to the agent's `toolCall` pipeline via
101
- * `collabPauseMiddleware` in the webui server boot.
102
- */
103
- bus?: CollaborationBus | undefined, options?: CollaborationHandlerOptions);
38
+ /** Per-socket disconnect removers so dispose() can detach still-live sockets. */
39
+ private readonly socketOffs;
40
+ private readonly ctx;
41
+ constructor(events: EventBus, logger: Logger, reader?: SessionReader | undefined, annotations?: AnnotationsStore | undefined, bus?: CollaborationBus | undefined, options?: CollaborationHandlerOptions);
104
42
  addClient(ws: WebSocket): void;
105
43
  /** True while at least one collaboration participant is attached to a session. */
106
44
  hasParticipants(sessionId: string): boolean;
107
45
  dispose(): void;
108
46
  /**
109
47
  * Dispatch a parsed client message. Returns true when the message was
110
- * recognized and handled; false when the caller should ignore / log.
111
- * Phase 1 only knows `collab.join` and `collab.leave`; unknown types
112
- * return false so the upstream router can decide.
113
- */
114
- handleMessage(ws: WebSocket, msg: {
115
- type: string;
116
- payload?: unknown | undefined;
117
- }): boolean;
118
- private join;
119
- private leave;
120
- private handleDisconnect;
121
- /**
122
- * Look up the participant record for a given WS across all sessions.
123
- * Returns null when the WS hasn't joined (e.g. the client sent a
124
- * `collab.annotate` before `collab.join`).
125
- */
126
- private findParticipant;
127
- private findParticipantById;
128
- private handleAnnotate;
129
- private handleResolve;
130
- private subscribe;
131
- private broadcastEvent;
132
- /**
133
- * Replay the last `REPLAY_LIMIT` events from the on-disk session log
134
- * to a single observer (the late joiner). Each event is forwarded as
135
- * a `collab.event` with `replay: true` so the client can distinguish
136
- * history from the live stream.
137
- *
138
- * The session log stores typed `SessionEvent`s (`user_input`,
139
- * `llm_response`, `tool_result`, etc.) — different from the kernel's
140
- * bus events. We translate the most useful subset (`tool.*` and
141
- * `iteration.*`-shaped ones) into the same `kind` namespace the live
142
- * mirror uses, so the client can render a single activity strip.
48
+ * recognized and handled; false when the caller should ignore / log so
49
+ * the upstream router can dispatch non-collab messages.
143
50
  */
144
- private replayHistory;
51
+ handleMessage(ws: WebSocket, msg: CollabInboundMessage): boolean;
52
+ private makeContext;
145
53
  /**
146
- * Map a stored `SessionEvent` to a `collab.event.kind` so the live
147
- * strip and the history strip can share a single rendering path.
148
- * Returns null for events that don't have a meaningful live analog
149
- * (e.g. `session_start`, file-snapshot bookkeeping, rewind markers).
150
- */
151
- private historyEventToKind;
152
- private stateMessage;
153
- /**
154
- * Cheap participant-set identity: participant ids are unique per join and
155
- * `joinedAt` distinguishes a rejoin, so the fingerprint changes exactly on
156
- * join/leave — the only events that alter `collab.state`.
157
- */
158
- private stateFingerprint;
159
- /**
160
- * Broadcast the current state for a session and record the fingerprint.
161
- * Used on join/leave (eager broadcast) and by the periodic interval.
54
+ * Shared `collab.leave` / WS close-error path. Order matters:
55
+ * 1. Remove from the client set and detach tracked listeners.
56
+ * 2. Send `participant.left` to the leaving ws, then delete from bucket.
57
+ * 3. Broadcast the fresh state to remaining observers.
58
+ * 4. Stop the interval once nobody can be broadcast to.
162
59
  */
60
+ private removeSocket;
61
+ private detachSocket;
62
+ /** Broadcast the current state for a session and record the fingerprint. */
163
63
  private broadcastState;
164
- private ensureBroadcast;
165
- private stopBroadcast;
166
64
  private broadcast;
167
65
  private send;
168
66
  private errorMessage;
169
- private handleRequestPause;
170
- private handleResume;
171
- private handleGrantControl;
172
- /**
173
- * Phase 4 — handle a controller's manual tool-call injection.
174
- * Validates the payload, queues it on the bus, and broadcasts
175
- * the grant so observers see what just happened. The actual
176
- * splice into the agent's pipeline is performed by the
177
- * `collabInjectMiddleware` on the next tool call.
178
- */
179
- private handleInjectTool;
180
- /**
181
- * Bus callback: a queued injection was spliced into a real tool call. Re-emit
182
- * `collab.injection.granted` with phase `'consumed'` and the now-known tool
183
- * name. The injection carries no sessionId, so resolve it from the author's
184
- * current participant. If the author has left, fail closed rather than leak
185
- * the payload to unrelated sessions.
186
- */
187
- private broadcastInjectionConsumed;
188
67
  }
189
68
  //# sourceMappingURL=collaboration-ws-handler.d.ts.map
@@ -40,7 +40,8 @@ export declare function createEmbeddedProviderOperations(ctx: EmbeddedProviderCo
40
40
  handleProviderModels: (ws: WebSocket, providerId: string) => Promise<void>;
41
41
  handleProviderModelsSearch: (ws: WebSocket, query: string, limit?: number | undefined) => Promise<void>;
42
42
  adoptDefaultProviderIfUnset: (providerId: string) => Promise<void>;
43
- broadcastSaved: (providers: Record<string, ProviderConfig>) => void;
43
+ handleCustomModelSet: (ws: WebSocket, providerId: string, modelId: string, definition: NonNullable<ProviderConfig['customModels']>[string]) => Promise<void>;
44
+ handleCustomModelRemove: (ws: WebSocket, providerId: string, modelId: string) => Promise<void>;
44
45
  handleKeyUpsert: (ws: WebSocket, providerId: string, label: string, apiKey: string) => Promise<void>;
45
46
  handleKeyDelete: (ws: WebSocket, providerId: string, label: string) => Promise<void>;
46
47
  handleKeySetActive: (ws: WebSocket, providerId: string, label: string) => Promise<void>;
@@ -54,8 +55,6 @@ export declare function createEmbeddedProviderOperations(ctx: EmbeddedProviderCo
54
55
  }) => Promise<boolean>;
55
56
  handleProviderRemove: (ws: WebSocket, providerId: string) => Promise<void>;
56
57
  handleProviderClearModels: (ws: WebSocket, providerId: string) => Promise<void>;
57
- handleCustomModelSet: (ws: WebSocket, providerId: string, modelId: string, definition: NonNullable<ProviderConfig['customModels']>[string]) => Promise<void>;
58
- handleCustomModelRemove: (ws: WebSocket, providerId: string, modelId: string) => Promise<void>;
59
58
  handleProviderUndoClear: (ws: WebSocket, providerId: string, previousModels: string[]) => Promise<void>;
60
59
  handleProviderUpdate: (ws: WebSocket, payload: {
61
60
  id: string;
@@ -65,10 +64,11 @@ export declare function createEmbeddedProviderOperations(ctx: EmbeddedProviderCo
65
64
  models?: string[] | undefined;
66
65
  customModels?: ProviderConfig['customModels'] | undefined;
67
66
  }) => Promise<void>;
68
- handleProviderProbe: (ws: WebSocket, providerId: string, timeoutMs?: number) => Promise<void>;
69
67
  handleOAuthStart: (ws: WebSocket, kind: import("@wrongstack/providers/oauth").OAuthKind, customProviderId?: string) => Promise<void>;
70
68
  handleOAuthCode: (ws: WebSocket, kind: import("@wrongstack/providers/oauth").OAuthKind, input: string) => Promise<void>;
71
69
  handleOAuthCancel: (ws: WebSocket, kind: import("@wrongstack/providers/oauth").OAuthKind) => void;
70
+ handleProviderProbe: (ws: WebSocket, providerId: string, timeoutMs?: number) => Promise<void>;
71
+ broadcastSaved: (providers: Record<string, ProviderConfig>) => void;
72
72
  loadConfigProviders: () => Promise<Record<string, ProviderConfig>>;
73
73
  };
74
74
  export interface EmbeddedConversationContext extends EmbeddedHostTransport {