@tangle-network/agent-app 0.43.46 → 0.43.47

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.
@@ -0,0 +1,551 @@
1
+ import { R as ReconcileStaleTurnLockOptions } from '../stale-turn-lock-C8Na1cFZ.js';
2
+ import { d as TurnEventStore } from '../turn-buffer-C9mEgoop.js';
3
+
4
+ /**
5
+ * `/turn-stream` core — the pure, substrate-free half of the shared durable
6
+ * turn replay/broadcast/lock channel (issue #221).
7
+ *
8
+ * Extracted from the reference consumer's hand-rolled Durable Object
9
+ * (gtm-agent `SessionStreamDO` + `session-broadcast.ts`): the per-turn
10
+ * segment store that backs reconnect replay over a live socket, the
11
+ * single-flight chat-turn lock record and its release fences, and the wire
12
+ * contract (channel keys, endpoint paths, request/response bodies) shared by
13
+ * the DO transport shell (`./do`) and the worker-side adapters
14
+ * (`./adapters`). Everything here is plain data + functions — no
15
+ * `cloudflare:workers`, no storage, no sockets — so the semantics are
16
+ * unit-testable in Node and the DO stays a thin shell.
17
+ */
18
+ /** One event on a turn-stream channel. `seq` is monotonic within a turn
19
+ * segment and assigned by {@link appendSegmentEvent} on arrival at the DO. */
20
+ interface TurnStreamEvent {
21
+ type: string;
22
+ data?: unknown;
23
+ timestamp: number;
24
+ seq?: number;
25
+ }
26
+ /** Terminal run markers: they close a turn segment and auto-release the
27
+ * channel's chat-turn lock for the segment's execution. */
28
+ declare function isTerminalRunEvent(type: string): boolean;
29
+ type TurnLockScope = 'thread' | 'workspace';
30
+ declare function threadChannelKey(workspaceId: string, threadId: string): string;
31
+ declare function workspaceChannelKey(workspaceId: string): string;
32
+ /** The channel a lock lives on: workspace-scope locks serialize every thread
33
+ * in the workspace (one shared sandbox), thread-scope locks serialize one
34
+ * thread (router lane). Same keying as the reference consumer, so a product
35
+ * swapping its fork for this package contends on identical instances. */
36
+ declare function turnLockChannelKey(workspaceId: string, threadId: string, scope: TurnLockScope): string;
37
+ declare function turnStorageChannelKey(turnId: string): string;
38
+ declare function scopeIndexChannelKey(scopeId: string): string;
39
+ interface TurnSegment {
40
+ events: TurnStreamEvent[];
41
+ maxSeq: number;
42
+ terminal: boolean;
43
+ }
44
+ interface SegmentStore {
45
+ segments: Map<string, TurnSegment>;
46
+ activeExecutionId: string | null;
47
+ }
48
+ /** Per-turn replay window. Generous enough for normal turns; a turn that
49
+ * exceeds it loses its earliest deltas from replay (a late resumer
50
+ * self-heals via the final `result` event + loader revalidation). */
51
+ declare const MAX_SEGMENT_EVENTS = 2000;
52
+ /** Recent `thread.created` markers kept for late-connecting sidebars. */
53
+ declare const MAX_RECENT_CREATED = 50;
54
+ /** A responding marker older than this is treated as stale, so a dropped
55
+ * `end` broadcast can't leave a permanently-stuck "responding" dot. */
56
+ declare const ACTIVITY_TTL_MS: number;
57
+ declare function createSegmentStore(): SegmentStore;
58
+ /**
59
+ * Append a per-turn event to its execution's segment, assigning a monotonic
60
+ * `seq`. A `session.run.started` (or the first-seen event for an execution)
61
+ * opens a fresh segment, makes it active, and drops prior turns' buffers so a
62
+ * resumer only ever replays the current turn. A terminal run event marks the
63
+ * segment terminal. Returns the seq-stamped event to broadcast.
64
+ */
65
+ declare function appendSegmentEvent(store: SegmentStore, executionId: string, incoming: TurnStreamEvent, maxEvents?: number): TurnStreamEvent;
66
+ /**
67
+ * Events of the active, non-terminal turn with `seq > afterSeq` — what a
68
+ * (re)connecting client replays before going live. A terminal (finished) turn
69
+ * replays nothing: the client falls back to the loader's persisted row.
70
+ */
71
+ declare function replayActiveSegment(store: SegmentStore, afterSeq: number): TurnStreamEvent[];
72
+ /**
73
+ * Remove responding entries (threadId → startedAt) older than `ttlMs`, so a
74
+ * dropped `end` broadcast can't leave a permanently-stuck dot. Mutates
75
+ * `active` and returns the removed thread ids.
76
+ */
77
+ declare function pruneStaleThreads(active: Map<string, number>, now: number, ttlMs: number): string[];
78
+ /** Default lifetime of an unreleased lock. Long enough that a legitimately
79
+ * slow sandbox turn never loses its guard mid-run; the way OUT of a wedge is
80
+ * never the TTL but `reconcileStaleTurnLock` (in `/chat-routes`), which
81
+ * probes the execution's actual state. */
82
+ declare const TURN_LOCK_TTL_MS: number;
83
+ /** The stored single-flight lock. Field-compatible with the reference
84
+ * consumer's `ChatTurnLock` so adoption is a swap, not a migration. */
85
+ interface DurableTurnLock {
86
+ workspaceId: string;
87
+ threadId: string;
88
+ scope: TurnLockScope;
89
+ executionId: string;
90
+ lockId: string;
91
+ startedAt: number;
92
+ expiresAt: number;
93
+ turnId?: string;
94
+ /** The turn released this lock, but a product-owned post-turn task (e.g.
95
+ * file persistence reading the box) is still running, so the release is
96
+ * parked on the lock until the task settles. Written only through the DO's
97
+ * defer seam — the base package never sets it on its own. */
98
+ releasePending?: boolean;
99
+ }
100
+ interface TurnLockAcquireInput {
101
+ workspaceId: string;
102
+ threadId: string;
103
+ scope: TurnLockScope;
104
+ executionId: string;
105
+ lockId: string;
106
+ turnId?: string;
107
+ }
108
+ type TurnLockAcquireResult = {
109
+ acquired: true;
110
+ lock: DurableTurnLock;
111
+ } | {
112
+ acquired: false;
113
+ active: DurableTurnLock;
114
+ };
115
+ interface TurnLockReleaseInput {
116
+ workspaceId: string;
117
+ threadId: string;
118
+ scope: TurnLockScope;
119
+ executionId: string;
120
+ lockId: string;
121
+ }
122
+ /** Fenced out-of-band release (stop button, stale-lock reconciliation). The
123
+ * fences make it refuse a SUCCESSOR lock: `interruptedAt` must not precede
124
+ * the lock's own start, and when either side names a turn, both must name
125
+ * the same one. */
126
+ interface TurnLockInterruptedReleaseInput {
127
+ workspaceId: string;
128
+ threadId: string;
129
+ /** Try only this scope; omit to try workspace then thread. */
130
+ scope?: TurnLockScope;
131
+ interruptedAt: number;
132
+ turnId?: string;
133
+ }
134
+ /** `stored` is what the DO read from storage; expired locks are dead. */
135
+ declare function activeTurnLock(stored: DurableTurnLock | undefined, now: number): DurableTurnLock | null;
136
+ declare function createTurnLock(input: TurnLockAcquireInput, now: number, ttlMs?: number): DurableTurnLock;
137
+ /** A cooperative release must present the lock's own identity — both the
138
+ * execution and the lockId minted at acquire — so a retry of a PREVIOUS turn
139
+ * can never release the current one. `lockId` is optional only for the DO's
140
+ * internal terminal-event auto-release, which knows the execution but not
141
+ * the caller-held lockId. */
142
+ declare function turnLockMatchesRelease(active: DurableTurnLock, input: {
143
+ executionId: string;
144
+ lockId?: string;
145
+ }): boolean;
146
+ /**
147
+ * The interrupted/stale release fence. `interruptedAt` is the instant the
148
+ * releasing evidence was observed (the stop click, the stale-lock probe) —
149
+ * a lock STARTED after that instant is a successor the evidence says nothing
150
+ * about, so it survives. When the lock recorded a client turnId, the release
151
+ * must name the same turn; a lock without one refuses a turn-specific
152
+ * release (it cannot prove it is that turn).
153
+ */
154
+ declare function interruptedReleaseApplies(active: DurableTurnLock, input: {
155
+ threadId: string;
156
+ interruptedAt: number;
157
+ turnId?: string;
158
+ }): boolean;
159
+ declare const TURN_STREAM_PATHS: {
160
+ readonly broadcast: "/broadcast";
161
+ readonly lockAcquire: "/chat-turn-lock/acquire";
162
+ readonly lockRelease: "/chat-turn-lock/release";
163
+ readonly lockReleaseInterrupted: "/chat-turn-lock/release-interrupted";
164
+ readonly turnEventsAppend: "/turn-events/append";
165
+ readonly turnEventsRead: "/turn-events/read";
166
+ readonly turnStatusSet: "/turn-status/set";
167
+ readonly turnStatusGet: "/turn-status/get";
168
+ readonly scopeStatusSet: "/turn-scope/set";
169
+ readonly scopeRunningList: "/turn-scope/running";
170
+ };
171
+ /** Storage keys inside a DO instance. Exported for subclass coexistence —
172
+ * a product extending the DO must not collide with these. */
173
+ declare const TURN_STREAM_STORAGE_KEYS: {
174
+ readonly lock: "chatTurnLock";
175
+ readonly activeThreads: "activeThreads";
176
+ readonly turnStatus: "turnStatus";
177
+ readonly turnScope: "turnScopeIndex";
178
+ readonly turnEventPrefix: "turnEvent:";
179
+ };
180
+ /** Zero-padded seq so DO storage `list({ prefix })` returns rows in replay
181
+ * order without a sort. 10 digits holds any realistic turn. */
182
+ declare function turnEventStorageKey(seq: number): string;
183
+
184
+ /**
185
+ * `TurnStreamDO` — the shared Durable Object transport shell over the pure
186
+ * core (`./core`). One class serves every channel family; the instance NAME
187
+ * decides which endpoints a given instance ever sees:
188
+ *
189
+ * - **thread channel** (`${workspaceId}:${threadId}`) — the live chat turn:
190
+ * WebSocket fanout, per-turn segments with `sync`/`afterSeq` reconnect
191
+ * replay, and the thread-scope lock.
192
+ * - **workspace channel** (`${workspaceId}`) — coarse sidebar signals
193
+ * (`thread.activity` responding set, durable across eviction;
194
+ * `thread.created` recent list) and the workspace-scope lock.
195
+ * - **turn storage** (`turn:${turnId}`) — the durable `TurnEventStore` rows +
196
+ * status for one buffered turn (replay survives DO eviction — this is what
197
+ * graduates the vertical's `turnStore` from no-op).
198
+ * - **scope index** (`scope:${scopeId}`) — the running-turn index backing
199
+ * `TurnEventStore.listRunning` reconnect discovery.
200
+ *
201
+ * The class is a PLAIN class over a structural {@link TurnStreamDOState} —
202
+ * no `cloudflare:workers` import, so this package stays substrate-free and
203
+ * the DO is unit-testable in Node. Cloudflare's `DurableObjectState`
204
+ * satisfies the interface; a product binds it in wrangler by re-exporting:
205
+ *
206
+ * // worker entry
207
+ * export { TurnStreamDO } from '@tangle-network/agent-app/turn-stream'
208
+ *
209
+ * Fan-out enumerates `state.getWebSockets()` (never an in-memory socket map)
210
+ * and reads per-socket metadata from the serialized attachment, so it is
211
+ * correct across WebSocket hibernation.
212
+ *
213
+ * Product extension (how the reference consumer keeps its Vault machinery
214
+ * while deleting its fork): subclass and override
215
+ * {@link TurnStreamDO.handleProductRequest} (extra POST endpoints),
216
+ * {@link TurnStreamDO.shouldDeferLockRelease} /
217
+ * {@link TurnStreamDO.completeDeferredLockRelease} (park a lock release
218
+ * behind a product-owned post-turn task), and
219
+ * {@link TurnStreamDO.productSyncEvents} (extra state replayed to a
220
+ * late-connecting socket). Product storage keys must avoid
221
+ * {@link TURN_STREAM_STORAGE_KEYS}.
222
+ */
223
+
224
+ /** The socket surface the DO touches. Cloudflare's hibernatable `WebSocket`
225
+ * satisfies it. */
226
+ interface TurnStreamSocket {
227
+ send(data: string): void;
228
+ close(code?: number, reason?: string): void;
229
+ serializeAttachment(value: unknown): void;
230
+ deserializeAttachment(): unknown;
231
+ }
232
+ /** The storage surface the DO touches (Cloudflare `DurableObjectStorage`
233
+ * satisfies it structurally). `list` must return keys in ascending order —
234
+ * the turn-event rows rely on it for replay order. */
235
+ interface TurnStreamStorage {
236
+ get<T = unknown>(key: string): Promise<T | undefined>;
237
+ put<T = unknown>(key: string, value: T): Promise<void>;
238
+ delete(key: string): Promise<boolean | void>;
239
+ list<T = unknown>(options: {
240
+ prefix: string;
241
+ start?: string;
242
+ }): Promise<Map<string, T>>;
243
+ }
244
+ /** The `DurableObjectState` surface the DO uses. */
245
+ interface TurnStreamDOState {
246
+ storage: TurnStreamStorage;
247
+ acceptWebSocket(ws: TurnStreamSocket): void;
248
+ getWebSockets(): TurnStreamSocket[];
249
+ }
250
+ interface TurnStreamDOOptions {
251
+ /** Override {@link TURN_LOCK_TTL_MS}. */
252
+ lockTtlMs?: number;
253
+ /** Override {@link MAX_SEGMENT_EVENTS}. */
254
+ maxSegmentEvents?: number;
255
+ /** Override {@link ACTIVITY_TTL_MS}. */
256
+ activityTtlMs?: number;
257
+ }
258
+ declare class TurnStreamDO {
259
+ protected readonly state: TurnStreamDOState;
260
+ protected readonly env: unknown;
261
+ protected readonly options: TurnStreamDOOptions;
262
+ private segments;
263
+ private recentCreated;
264
+ private activeThreads;
265
+ constructor(state: TurnStreamDOState, env?: unknown, options?: TurnStreamDOOptions);
266
+ fetch(request: Request): Promise<Response>;
267
+ /** Called for any request no base endpoint claimed (before the 404), so a
268
+ * subclass adds product endpoints without touching base routing. Return
269
+ * `null` to decline. */
270
+ protected handleProductRequest(_request: Request, _url: URL): Promise<Response | null>;
271
+ /** Consulted before any lock release (cooperative, interrupted, or the
272
+ * terminal-event auto-release). Return `true` while a product-owned
273
+ * post-turn task for `executionId` must keep the scope serialized (e.g.
274
+ * file persistence still reading the box) — the release is then parked as
275
+ * `releasePending` on the lock and completed via
276
+ * {@link completeDeferredLockRelease}. Base: never defer. */
277
+ protected shouldDeferLockRelease(_executionId: string): Promise<boolean>;
278
+ /** Complete a release parked by {@link shouldDeferLockRelease}. A subclass
279
+ * calls this when its deferred condition settles. */
280
+ protected completeDeferredLockRelease(executionId: string): Promise<boolean>;
281
+ /** Extra product state replayed to a socket during its `sync`, after the
282
+ * base replay for its scope (e.g. an in-flight persistence status card).
283
+ * Base: none. */
284
+ protected productSyncEvents(_scope: 'thread' | 'workspace', _meta: {
285
+ sessionId: string;
286
+ }): Promise<TurnStreamEvent[]>;
287
+ private handleWebSocketUpgrade;
288
+ /**
289
+ * First (and only) client message after open: `{ type: 'sync', afterSeq }`.
290
+ * Replays the current state for the socket's scope, then marks it `synced`
291
+ * so live broadcasts start flowing. Because the DO is single-threaded, the
292
+ * replay snapshot and the synced flip are atomic w.r.t. broadcasts — every
293
+ * event reaches the socket exactly once, in order, via replay XOR live
294
+ * fan-out.
295
+ */
296
+ webSocketMessage(ws: TurnStreamSocket, message: string | ArrayBuffer): Promise<void>;
297
+ webSocketClose(ws: TurnStreamSocket, code: number, reason: string): void;
298
+ webSocketError(): void;
299
+ private trySend;
300
+ private handleBroadcast;
301
+ private loadActiveThreads;
302
+ private persistActiveThreads;
303
+ protected loadActiveLock(now?: number): Promise<DurableTurnLock | null>;
304
+ private releaseActiveLock;
305
+ /** Park a release on the lock itself; {@link completeDeferredLockRelease}
306
+ * finishes it once the product's deferred condition settles. */
307
+ private deferLockRelease;
308
+ private handleLockAcquire;
309
+ private handleLockRelease;
310
+ private handleLockReleaseInterrupted;
311
+ private handleTurnEventsAppend;
312
+ private handleTurnEventsRead;
313
+ private handleTurnStatusSet;
314
+ private handleTurnStatusGet;
315
+ private handleScopeStatusSet;
316
+ private handleScopeRunningList;
317
+ }
318
+
319
+ /**
320
+ * Worker-side adapters over {@link TurnStreamDO}: the concrete implementations
321
+ * of the chat vertical's `turnStore` and `turnLock` seams, the WebSocket
322
+ * upgrade forwarder, the best-effort broadcast helpers, and an in-process
323
+ * memory harness for tests and keyless local dev.
324
+ *
325
+ * Everything takes the namespace STRUCTURALLY ({@link TurnStreamNamespaceLike}
326
+ * — Cloudflare's `DurableObjectNamespace` satisfies it), so nothing here
327
+ * imports Cloudflare types and the same adapters run against the memory
328
+ * harness in vitest.
329
+ *
330
+ * Live fanout is deliberately NOT a side effect of the turn-event store: the
331
+ * store is keyed by turnId/scopeId while viewer sockets live on the
332
+ * `${workspaceId}:${threadId}` channel, and only the product's per-turn
333
+ * context knows both. Products wire {@link broadcastTurnStreamEvent} (and the
334
+ * workspace helpers) into `createChatTurnRoutes`' `onEvent` — the same
335
+ * contract the reference consumer already runs.
336
+ */
337
+
338
+ interface TurnStreamStubLike {
339
+ fetch(input: Request | string, init?: RequestInit): Promise<Response>;
340
+ }
341
+ /** The surface of `DurableObjectNamespace` the adapters use. */
342
+ interface TurnStreamNamespaceLike {
343
+ idFromName(name: string): unknown;
344
+ get(id: unknown): TurnStreamStubLike;
345
+ }
346
+ /**
347
+ * A {@link TurnEventStore} backed by {@link TurnStreamDO} storage — the
348
+ * production implementation of `createChatTurnRoutes`' `turnStore` seam for
349
+ * apps that don't run D1 for turn events (or want replay co-located with the
350
+ * live channel). Each buffered turn lives on its own `turn:<turnId>` DO
351
+ * instance; `listRunning` reconnect discovery rides a per-scope index
352
+ * instance. Drops in wherever `createD1TurnEventStore(env.DB)` would.
353
+ */
354
+ declare function createDurableObjectTurnEventStore(namespace: TurnStreamNamespaceLike): TurnEventStore;
355
+ interface AcquireDurableTurnLockInput {
356
+ workspaceId: string;
357
+ threadId: string;
358
+ scope: TurnLockScope;
359
+ executionId: string;
360
+ turnId?: string;
361
+ /** Supply to reclaim/retry with a stable id; default mints a UUID. */
362
+ lockId?: string;
363
+ }
364
+ declare function acquireDurableTurnLock(namespace: TurnStreamNamespaceLike, input: AcquireDurableTurnLockInput): Promise<TurnLockAcquireResult>;
365
+ declare function releaseDurableTurnLock(namespace: TurnStreamNamespaceLike, input: TurnLockReleaseInput): Promise<{
366
+ released: boolean;
367
+ deferred?: boolean;
368
+ }>;
369
+ interface ReleaseInterruptedDurableTurnLockInput {
370
+ workspaceId: string;
371
+ threadId: string;
372
+ /** Try only this scope; omit to try workspace then thread (a stop button
373
+ * doesn't know which lane the wedged turn ran on). */
374
+ scope?: TurnLockScope;
375
+ interruptedAt: number;
376
+ turnId?: string;
377
+ }
378
+ /** Fenced out-of-band release — the DO refuses a successor lock (started
379
+ * after `interruptedAt`) and a turnId mismatch. Returns whether any scope
380
+ * released. */
381
+ declare function releaseInterruptedDurableTurnLock(namespace: TurnStreamNamespaceLike, input: ReleaseInterruptedDurableTurnLockInput): Promise<boolean>;
382
+ interface ReconcileStaleDurableTurnLockOptions extends Pick<ReconcileStaleTurnLockOptions, 'probeSandbox' | 'probeSession' | 'graceMs' | 'terminalGraceMs' | 'context' | 'log' | 'now'> {
383
+ namespace: TurnStreamNamespaceLike;
384
+ workspaceId: string;
385
+ threadId: string;
386
+ /** The lock the acquire attempt was refused on. */
387
+ active: DurableTurnLock;
388
+ }
389
+ /**
390
+ * `/chat-routes`' `reconcileStaleTurnLock` policy wired to the DO: the
391
+ * product supplies the probes (which box, what its session says), the policy
392
+ * decides, and a release lands as a FENCED interrupted release —
393
+ * `interruptedAt` is the policy's `fence.observedAt`, so a successor lock
394
+ * acquired while the probes were in flight survives by construction.
395
+ */
396
+ declare function reconcileStaleDurableTurnLock(options: ReconcileStaleDurableTurnLockOptions): Promise<{
397
+ released: boolean;
398
+ diagnostics: Record<string, unknown>;
399
+ }>;
400
+ /** The subset of `ChatTurnProduceArgs` the lock adapter reads — structural,
401
+ * so this module needs no import from `/chat-routes/turn-routes` (which
402
+ * would drag the `agent-runtime` peer into every `/turn-stream` consumer). */
403
+ interface TurnLockSeamArgs<TContext> {
404
+ identity: {
405
+ tenantId: string;
406
+ sessionId: string;
407
+ };
408
+ executionId: string;
409
+ context: TContext;
410
+ body: {
411
+ turnId?: string;
412
+ };
413
+ }
414
+ /** Verdict shape of the vertical's `turnLock.acquire`. */
415
+ type TurnLockSeamResult = {
416
+ acquired: true;
417
+ handle?: unknown;
418
+ } | {
419
+ acquired: false;
420
+ response: Response;
421
+ };
422
+ interface CreateDurableTurnLockOptions<TContext> {
423
+ namespace: TurnStreamNamespaceLike;
424
+ /** Which lane serializes this turn: `'workspace'` (shared sandbox — one
425
+ * turn per workspace) or `'thread'` (router lane — one turn per thread). */
426
+ scopeOf(args: TurnLockSeamArgs<TContext>): TurnLockScope;
427
+ /** Override the execution id the lock records (e.g. a follow-up reclaiming
428
+ * the execution it already dispatched). Default: the turn's own. */
429
+ lockExecutionIdOf?(args: TurnLockSeamArgs<TContext>): string | undefined;
430
+ /** Client turn id recorded on the lock (fences interrupted releases to the
431
+ * right turn). Default: the request body's `turnId`. */
432
+ clientTurnIdOf?(args: TurnLockSeamArgs<TContext>): string | undefined;
433
+ /**
434
+ * Attempt stale-lock recovery after a refused acquire. Return whether the
435
+ * held lock was released (the adapter then retries the acquire once).
436
+ * Products wire {@link reconcileStaleDurableTurnLock} with their sandbox +
437
+ * session probes here. Omit → a refused acquire is final.
438
+ */
439
+ reconcile?(args: TurnLockSeamArgs<TContext>, active: DurableTurnLock): Promise<{
440
+ released: boolean;
441
+ diagnostics?: Record<string, unknown>;
442
+ }>;
443
+ /** Build the refusal `Response`. Default: a 409 with the shared body shape
444
+ * (`code`, `message`, lock identity + age, reconcile diagnostics). */
445
+ onRefused?(active: DurableTurnLock, diagnostics: Record<string, unknown> | undefined): Response;
446
+ }
447
+ /**
448
+ * The vertical's `turnLock` seam on the shared DO: dual-scope single-flight
449
+ * acquire (with one reconcile-then-retry pass when the product supplies a
450
+ * stale-lock reconciler) and cooperative release on settle. The returned
451
+ * object satisfies `createChatTurnRoutes`' `ChatTurnLock<TContext>`
452
+ * structurally.
453
+ */
454
+ declare function createDurableTurnLock<TContext>(options: CreateDurableTurnLockOptions<TContext>): {
455
+ acquire(args: TurnLockSeamArgs<TContext>): Promise<TurnLockSeamResult>;
456
+ release(handle: unknown): Promise<void>;
457
+ };
458
+ /**
459
+ * Fan a turn event out to the per-thread channel. `executionId` groups events
460
+ * into a per-turn segment with a monotonic seq, so a reconnecting client
461
+ * replays only the active turn and resumes from a cursor. Callers MUST await
462
+ * (the DO assigns seq on arrival — emission order matters); failures are
463
+ * swallowed (fanout is best-effort and never breaks chat delivery).
464
+ */
465
+ declare function broadcastTurnStreamEvent(namespace: TurnStreamNamespaceLike, input: {
466
+ workspaceId: string;
467
+ threadId: string;
468
+ executionId: string;
469
+ event: {
470
+ type: string;
471
+ data?: Record<string, unknown>;
472
+ };
473
+ }): Promise<void>;
474
+ /** Coarse per-workspace marker that a thread's turn started / ended — drives
475
+ * a sidebar "agent responding" indicator subscribed once per workspace. */
476
+ declare function broadcastWorkspaceActivity(namespace: TurnStreamNamespaceLike, workspaceId: string, threadId: string, phase: 'start' | 'end'): Promise<void>;
477
+ /** Per-workspace marker that a new thread was created, so an already-open
478
+ * history list prepends it without a reload. */
479
+ declare function broadcastThreadCreated(namespace: TurnStreamNamespaceLike, workspaceId: string, thread: {
480
+ threadId: string;
481
+ title: string;
482
+ }): Promise<void>;
483
+ type TurnStreamUpgradeAuthorization = {
484
+ ok: true;
485
+ } | {
486
+ ok: false;
487
+ response: Response;
488
+ };
489
+ interface CreateTurnStreamUpgradeHandlerOptions {
490
+ namespace: TurnStreamNamespaceLike;
491
+ /** The worker route serving the stream. Default `/api/session-stream`. */
492
+ path?: string;
493
+ /** Viewer access check (session cookie → workspace membership). */
494
+ authorize(request: Request, target: {
495
+ workspaceId: string;
496
+ threadId: string | null;
497
+ }): Promise<TurnStreamUpgradeAuthorization>;
498
+ }
499
+ /**
500
+ * The worker-entry WebSocket forwarder. Call BEFORE the app router (a router
501
+ * loader cannot return a 101); returns `null` for requests that are not a
502
+ * WebSocket upgrade on the configured path.
503
+ *
504
+ * GET {path}?workspaceId=... → workspace channel (sidebar)
505
+ * GET {path}?workspaceId=...&threadId=… → thread channel (turn resume)
506
+ *
507
+ * After the 101, the client sends `{type:'sync', afterSeq}` and receives the
508
+ * replay-then-live stream (see {@link TurnStreamDO.webSocketMessage}).
509
+ */
510
+ declare function createTurnStreamUpgradeHandler(options: CreateTurnStreamUpgradeHandlerOptions): (request: Request) => Promise<Response | null>;
511
+
512
+ /**
513
+ * In-process turn-stream harness: a fake namespace that routes `idFromName`
514
+ * to REAL {@link TurnStreamDO} instances over an in-memory storage/socket
515
+ * state. The adapters and the DO run their production code paths — only the
516
+ * Cloudflare runtime (isolation, hibernation, real sockets) is simulated.
517
+ *
518
+ * For vitest composition tests and keyless local dev (the same role
519
+ * `createMemoryTurnEventStore` plays for the D1 store). Not for production:
520
+ * state is per-process and evaporates on restart.
521
+ */
522
+
523
+ /** A test-side viewer socket: records frames sent by the DO and lets the
524
+ * test drive the `sync` handshake. */
525
+ interface MemoryTurnStreamSocket extends TurnStreamSocket {
526
+ readonly frames: string[];
527
+ readonly closed: boolean;
528
+ }
529
+ interface MemoryTurnStreamChannel {
530
+ /** Attach a viewer socket to this channel (bypasses the HTTP 101 — the
531
+ * upgrade handshake is Cloudflare-runtime-only) and run its `sync`. */
532
+ connect(input: {
533
+ sessionId: string;
534
+ scope: 'thread' | 'workspace';
535
+ afterSeq?: number;
536
+ }): Promise<MemoryTurnStreamSocket>;
537
+ readonly instance: TurnStreamDO;
538
+ }
539
+ interface MemoryTurnStreamHarness {
540
+ namespace: TurnStreamNamespaceLike;
541
+ /** The channel (creating its DO instance if needed) for a channel key —
542
+ * e.g. `threadChannelKey(ws, thread)` — to connect test viewers. */
543
+ channel(name: string): MemoryTurnStreamChannel;
544
+ }
545
+ /**
546
+ * Build the harness. `createInstance` lets a product test run its own
547
+ * `TurnStreamDO` subclass through the same wiring.
548
+ */
549
+ declare function createMemoryTurnStreamHarness(createInstance?: (state: TurnStreamDOState) => TurnStreamDO, _options?: TurnStreamDOOptions): MemoryTurnStreamHarness;
550
+
551
+ export { ACTIVITY_TTL_MS, type AcquireDurableTurnLockInput, type CreateDurableTurnLockOptions, type CreateTurnStreamUpgradeHandlerOptions, type DurableTurnLock, MAX_RECENT_CREATED, MAX_SEGMENT_EVENTS, type MemoryTurnStreamChannel, type MemoryTurnStreamHarness, type MemoryTurnStreamSocket, type ReconcileStaleDurableTurnLockOptions, type ReleaseInterruptedDurableTurnLockInput, type SegmentStore, TURN_LOCK_TTL_MS, TURN_STREAM_PATHS, TURN_STREAM_STORAGE_KEYS, type TurnLockAcquireInput, type TurnLockAcquireResult, type TurnLockInterruptedReleaseInput, type TurnLockReleaseInput, type TurnLockScope, type TurnLockSeamArgs, type TurnLockSeamResult, type TurnSegment, TurnStreamDO, type TurnStreamDOOptions, type TurnStreamDOState, type TurnStreamEvent, type TurnStreamNamespaceLike, type TurnStreamSocket, type TurnStreamStorage, type TurnStreamStubLike, type TurnStreamUpgradeAuthorization, acquireDurableTurnLock, activeTurnLock, appendSegmentEvent, broadcastThreadCreated, broadcastTurnStreamEvent, broadcastWorkspaceActivity, createDurableObjectTurnEventStore, createDurableTurnLock, createMemoryTurnStreamHarness, createSegmentStore, createTurnLock, createTurnStreamUpgradeHandler, interruptedReleaseApplies, isTerminalRunEvent, pruneStaleThreads, reconcileStaleDurableTurnLock, releaseDurableTurnLock, releaseInterruptedDurableTurnLock, replayActiveSegment, scopeIndexChannelKey, threadChannelKey, turnEventStorageKey, turnLockChannelKey, turnLockMatchesRelease, turnStorageChannelKey, workspaceChannelKey };