@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.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +18031 -18795
- package/dist/server/collab/annotations.d.ts +26 -0
- package/dist/server/collab/broadcast-scheduler.d.ts +30 -0
- package/dist/server/collab/collab-context.d.ts +60 -0
- package/dist/server/collab/controller.d.ts +25 -0
- package/dist/server/collab/dispatcher.d.ts +27 -0
- package/dist/server/collab/injection.d.ts +26 -0
- package/dist/server/collab/membership.d.ts +20 -0
- package/dist/server/collab/mirror.d.ts +14 -0
- package/dist/server/collab/replay.d.ts +20 -0
- package/dist/server/collab/session-registry.d.ts +74 -0
- package/dist/server/collaboration-ws-handler.d.ts +28 -149
- package/dist/server/embedded-host-adapters.d.ts +4 -4
- package/dist/server/entry.js +16332 -16840
- package/dist/server/frontend-static-serve.d.ts +2 -0
- package/dist/server/index.d.ts +25 -25
- package/dist/server/pre-context-services.d.ts +2 -1
- package/dist/server/provider/catalog.d.ts +15 -0
- package/dist/server/provider/custom-models.d.ts +13 -0
- package/dist/server/provider/keys-records.d.ts +2 -0
- package/dist/server/provider/keys.d.ts +36 -0
- package/dist/server/provider/mutations.d.ts +34 -0
- package/dist/server/provider/oauth.d.ts +17 -0
- package/dist/server/provider/probe.d.ts +9 -0
- package/dist/server/provider/projection.d.ts +50 -0
- package/dist/server/provider-handlers.d.ts +38 -87
- package/dist/server/standalone-session-identity.d.ts +2 -1
- package/dist/server/token-estimator.d.ts +19 -8
- package/package.json +13 -16
- package/dist/protocol/client-conversation.d.ts +0 -3
- package/dist/protocol/client-integrations.d.ts +0 -3
- package/dist/protocol/client-operations.d.ts +0 -3
- package/dist/protocol/client-workspace.d.ts +0 -3
- package/dist/protocol/connection-fsm.d.ts +0 -39
- package/dist/protocol/decoder.d.ts +0 -6
- package/dist/protocol/index.d.ts +0 -8
- package/dist/protocol/index.js +0 -960
- package/dist/protocol/projections.d.ts +0 -131
- package/dist/protocol/registry.d.ts +0 -6
- package/dist/protocol/replay-payload.d.ts +0 -41
- package/dist/protocol/server-conversation.d.ts +0 -3
- package/dist/protocol/server-integrations.d.ts +0 -3
- package/dist/protocol/server-operations.d.ts +0 -4
- package/dist/protocol/server-workspace.d.ts +0 -3
- package/dist/protocol/types.d.ts +0 -24
- 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 {
|
|
7
|
-
|
|
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
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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
|
|
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
|
|
71
|
-
|
|
72
|
-
private readonly
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
51
|
+
handleMessage(ws: WebSocket, msg: CollabInboundMessage): boolean;
|
|
52
|
+
private makeContext;
|
|
145
53
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
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
|
-
|
|
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 {
|