131
return channel;
132
}
134
>
/**
135
>
* Methods handled by the request dispatcher. Excludes `initialize`,
136
>
* `reconnect`, and `ping`, which are handled directly during message
137
>
* dispatch without requiring an established client context.
138
>
*/
139
>
type RequestMethod = Exclude<keyof CommandMap, 'initialize' | 'reconnect' | 'ping'>;
140
>
141
>
/**
142
>
* Typed handler map: each key is a request method, each value is a handler
143
>
* that receives the correctly-typed params and must return the correctly-typed
144
>
* result. The compiler will error if a handler returns the wrong shape.
145
>
*/
146
>
type RequestHandlerMap = {
147
>
[M in RequestMethod]: (client: IConnectedClient, params: CommandMap[M]['params']) => Promise<CommandMap[M]['result']>;
148
>
};
149
>
150
>
/**
151
>
* Discriminant for {@link ChannelSubscription}. Distinguishes a regular
152
>
* state-bearing channel (root, session, terminal, changeset) from the
153
>
* stateless OTLP signal channels so each subscribe/unsubscribe path can
154
>
* dispatch through a single typed lookup.
155
>
*/
156
>
const enum ChannelKind {
157
>
/**
158
>
* Subscribed via {@link IAgentService.subscribe} and tracked by the
159
>
* server-side refcount. Carries replayable state, participates in
160
>
* action broadcasts ({@link _broadcastAction}) and reconnect
161
>
* snapshot/replay.
162
>
*/
163
>
State = 'state',
164
>
/**
165
>
* Resource-watch channels (`ahp-resource-watch:/<id>`). Tracked
166
>
* separately so subscribe/unsubscribe routes through the agent
167
>
* service's per-watch refcount + grace timer rather than the
168
>
* session-shaped {@link IAgentService.subscribe} path.
169
>
*/
170
>
ResourceWatch = 'resource-watch',
171
>
/**
172
>
* Subscribed against the OTLP logs channel template advertised in
173
>
* {@link InitializeResult.telemetry}. Stateless — no snapshot, no
174
>
* agent-service refcount. The `level` field records the minimum
175
>
* severity the client asked to receive.
176
>
*/
177
>
OtlpLogs = 'otlp-logs',
178
>
}
179
>
180
>
/**
181
>
* Per-channel server-side subscription record. Stored on every
182
>
* {@link IConnectedClient} so each subscribed channel can be routed by
183
>
* its `kind` without re-deriving it from the URI on every dispatch.
184
>
*
185
>
* `uri` is the canonical channel URI string used everywhere a subscription
186
>
* is referenced — the same string is broadcast on outbound notifications
187
>
* and persists across reconnects.
188
>
*/
189
>
type ChannelSubscription =
190
>
| { readonly kind: ChannelKind.State; readonly uri: string }
191
>
| { readonly kind: ChannelKind.ResourceWatch; readonly uri: string }
192
>
| { readonly kind: ChannelKind.OtlpLogs; readonly uri: string; readonly level: OtlpLogLevelName };
193
>
194
>
/**
195
>
* Represents a connected protocol client with its subscription state.
196
>
*/
197
>
interface IConnectedClient {
198
>
readonly clientId: string;
199
>
readonly protocolVersion: string;
200
>
readonly transport: IProtocolTransport;
201
>
/**
202
>
* Every channel the client is currently subscribed to, keyed by the
203
>
* canonical channel URI. OTLP channel URIs are canonicalised to
204
>
* `buildOtlpLogsChannelUri(level)` so URI variants that resolve to
205
>
* the same logical channel collapse to one entry.
206
>
*/
207
>
readonly subscriptions: Map<string, ChannelSubscription>;
208
>
readonly disposables: DisposableStore;
209
>
}
210
>
211
>
/**
212
>
* Per-client server-side record, keyed by clientId in
213
>
* {@link ProtocolServerHandler._clients}. Unlike {@link IConnectedClient},
214
>
* the record OUTLIVES individual transports: multiple overlapping transports
215
>
* for the same logical client are held oldest-first, with the active transport
216
>
* at the end. When the last transport disconnects, the record is retained
217
>
* (until pruned) so the tool-call disconnect-grace machinery can compute the
218
>
* remaining window and hold any armed timeouts.
219
>
*
220
>
* A client is in exactly one of two states, which makes the core invariant
221
>
* unrepresentable in the wrong shape: a client either has one or more live
222
>
* transports ({@link IActiveClientRecord}, never any disconnect-grace timers)
223
>
* or has no transport and is within its disconnect-grace window
224
>
* ({@link IGraceClientRecord}, never any connections). Transitions happen only
225
>
* in {@link ProtocolServerHandler._attachConnection} (→ active, which disposes
226
>
* any grace timers) and the transport `onClose` handler (→ grace, once the last
227
>
* transport is gone).
228
>
*/
229
>
type IClientRecord = IActiveClientRecord | IGraceClientRecord;
230
>
231
>
interface IActiveClientRecord {
232
>
readonly state: 'active';
233
>
/**
234
>
* Live transports for this client, oldest first. The active connection is
235
>
* the last entry (most recent wins). Older entries are kept so that if a
236
>
* reconnecting client registers `A`, then `B`, then `B` closes first, we can
237
>
* fall back to `A` instead of treating the client as disconnected. Never
238
>
* empty: removing the last transport promotes the record to a grace record.
239
>
*/
240
>
readonly connections: IConnectedClient[];
241
>
}
242
>
243
>
interface IGraceClientRecord {
244
>
readonly state: 'grace';
245
>
/**
246
>
* Epoch ms when the client last had a live transport, or when this record
247
>
* was created for a never-connected orphan tool-call stamp. Pins the grace
248
>
* clock so re-arms triggered by later orphaned tool calls shrink the
249
>
* remaining window instead of resetting it. Drives the disconnect-timeout
250
>
* delay (residual window from this instant).
251
>
*/
252
>
lastSeenAt: number;
253
>
/**
254
>
* Pending tool-call disconnect timeouts owned by this client, keyed by
255
>
* session URI. Armed when the client owns a pending client tool call but is
256
>
* not connected; fires a failing completion if it does not (re)connect
257
>
* within the grace window. Reconnecting promotes the record to active and
258
>
* disposes these timers (the grace window no longer applies once a transport
259
>
* is live). Disposing an entry (or the whole map) clears the timer.
260
>
*/
261
>
readonly disconnectTimeouts: DisposableMap<string>;
262
>
}
263
>
264
>
/**
265
>
* Classifies a raw channel URI string into its {@link ChannelKind} and
266
>
* returns the canonical URI to key subscriptions by. Returns `undefined`
267
>
* when the channel is OTLP-flavoured but the URI does not parse into a
268
>
* supported shape (unknown level, missing path) so the caller can
269
>
* silently drop the subscribe rather than installing a broken entry.
270
>
*
271
>
* For state channels the canonical URI is just the input verbatim — the
272
>
* agent service is the authoritative deduplication point and tolerates
273
>
* whatever URI form the client sent.
274
>
*/
275
function classifyChannel(channel: string): ChannelSubscription | undefined {
276
if (channel.toLowerCase().startsWith(`${OTLP_CHANNEL_SCHEME}:`)) {