@tangle-network/agent-app 0.60.210 → 0.60.212

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/turn-stream/core.ts","../../src/turn-stream/do.ts","../../src/turn-stream/adapters.ts","../../src/turn-stream/memory.ts"],"sourcesContent":["/**\n * Channel tokens, lock records, workspace signals, and durable event keys.\n * Interactive turn replay belongs to the sandbox session gateway.\n * An optional turn-event backend stores durable rows and a running-turn index.\n * This core uses WebCrypto and plain data without a Cloudflare runtime import.\n */\n\nimport { base64UrlEncodeText, constantTimeEqual, hmacSha256Base64Url } from '../crypto/web-token'\n\n// ── events ──────────────────────────────────────────────────────────────────\n\n/** One event on a turn-stream channel. Workspace-signal events\n * (`thread.created`, `thread.activity`, product events) carry no `seq` —\n * ordering is delivery order on the DO's single-threaded event loop. */\nexport interface TurnStreamEvent {\n type: string\n data?: unknown\n timestamp: number\n seq?: never\n}\n\n// ── channel-bound capability tokens (issue #746) ─────────────────────────────\n//\n// The DO's endpoints are not publicly routed, but \"safe because nothing\n// forwards to it\" is convention, not enforcement: one accidental\n// `stub.fetch(publicRequest)` and every channel's lock, turn events, and\n// signals are cross-tenant writable. Every request the DO serves must now\n// carry a short-lived HMAC capability token minted for THIS DO's channel\n// name — a valid token for one channel is refused by every other, and a\n// token without the secret cannot exist. Worker code mints with the same\n// secret via the adapters (`authSecret`) and the WS upgrade forwarder;\n// nothing else needs to know the format.\n\n/** Default capability lifetime. Tokens are minted per worker request and\n * verified immediately; a WS token gates only the upgrade handshake — an\n * accepted socket lives until it closes. */\nexport const TURN_STREAM_TOKEN_TTL_MS = 60_000\n\n/** Request header the adapters attach (and the DO accepts). The worker also\n * attaches it when forwarding an authorized browser WebSocket handshake. */\nexport const TURN_STREAM_TOKEN_HEADER = 'x-turn-stream-token'\n\n/** `v1.<base64url-channel>.<exp-ms-36>.<base64url-mac>` — the expiry is inside the signed payload so\n * it cannot be extended by editing the token. */\nexport async function mintTurnStreamToken(\n channelName: string,\n secret: string,\n now: () => number = Date.now,\n ttlMs: number = TURN_STREAM_TOKEN_TTL_MS,\n): Promise<string> {\n if (!secret || secret.length < 32) throw new Error('turn-stream token secret must be at least 32 characters')\n const expiresAt = now() + ttlMs\n const payload = `v1.${base64UrlEncodeText(channelName)}.${expiresAt.toString(36)}`\n return `${payload}.${await hmacSha256Base64Url(payload, secret)}`\n}\n\n/** Verify a token for `channelName`: constant-time MAC compare with the\n * exact channel name embedded in the signed payload, plus freshness. A\n * token minted for any other channel — or a tampered expiry — is false. */\nexport async function verifyTurnStreamToken(\n channelName: string,\n token: string,\n secret: string,\n now: () => number = Date.now,\n): Promise<boolean> {\n if (!secret || secret.length < 32) return false\n const parts = token.split('.')\n if (parts.length !== 4 || parts[0] !== 'v1') return false\n const [, name, expires36, mac] = parts as [string, string, string, string]\n if (name !== base64UrlEncodeText(channelName)) return false\n if (!/^[0-9a-z]+$/.test(expires36)) return false\n const expiresAt = parseInt(expires36, 36)\n if (!Number.isSafeInteger(expiresAt) || expiresAt <= now()) return false\n const expected = await hmacSha256Base64Url(`v1.${name}.${expires36}`, secret)\n return constantTimeEqual(mac, expected)\n}\n\n// ── channel keys ────────────────────────────────────────────────────────────\n//\n// One DO instance per channel key. Four families:\n// thread `${workspaceId}:${threadId}` — thread-scope lock.\n// workspace `${workspaceId}` — sidebar activity + thread.created\n// + the workspace-scope lock.\n// turn `turn:${turnId}` — durable turn-event rows + status\n// (the TurnEventStore contract; replay survives DO eviction).\n// scope `scope:${scopeId}` — running-turn index for a thread,\n// backing `TurnEventStore.listRunning`.\n\n/** Define the scope level for acquiring a turn lock within thread or workspace contexts */\nexport type TurnLockScope = 'thread' | 'workspace'\n\n/** Channel for a thread-scoped lock. */\nexport function threadChannelKey(workspaceId: string, threadId: string): string {\n return `${workspaceId}:${threadId}`\n}\n\n/** Channel for workspace signals and the workspace-scoped lock. */\nexport function workspaceChannelKey(workspaceId: string): string {\n return workspaceId\n}\n\n/** The channel a lock lives on: workspace-scope locks serialize every thread\n * in the workspace (one shared sandbox), thread-scope locks serialize one\n * thread (router lane). Same keying as the reference consumer, so a product\n * swapping its fork for this package contends on identical instances. */\nexport function turnLockChannelKey(workspaceId: string, threadId: string, scope: TurnLockScope): string {\n return scope === 'workspace' ? workspaceChannelKey(workspaceId) : threadChannelKey(workspaceId, threadId)\n}\n\n/** Channel for the optional Durable Object turn-event backend. */\nexport function turnStorageChannelKey(turnId: string): string {\n return `turn:${turnId}`\n}\n\n/** Running-turn index for the optional Durable Object turn-event backend. */\nexport function scopeIndexChannelKey(scopeId: string): string {\n return `scope:${scopeId}`\n}\n\n/** Recent thread-created markers kept for late-connecting sidebars. */\nexport const MAX_RECENT_CREATED = 50\n\n/** A responding marker older than this is treated as stale, so a dropped\n * `end` broadcast can't leave a permanently-stuck \"responding\" dot. */\nexport const ACTIVITY_TTL_MS = 15 * 60 * 1000\n\n/** Remove responding markers older than ttlMs; return their thread IDs. */\nexport function pruneStaleThreads(active: Map<string, number>, now: number, ttlMs: number): string[] {\n const removed: string[] = []\n for (const [threadId, startedAt] of active) {\n if (now - startedAt > ttlMs) {\n active.delete(threadId)\n removed.push(threadId)\n }\n }\n return removed\n}\n\n// ── chat-turn lock record + fences ──────────────────────────────────────────\n\n/** Fallback lifetime for an unreleased lock. Use stale-lock reconciliation to\n * recover earlier when execution state proves that its holder stopped. */\nexport const TURN_LOCK_TTL_MS = 30 * 60 * 1000\n\n/** The stored single-flight lock. Field-compatible with the reference\n * consumer's `ChatTurnLock` so adoption is a swap, not a migration. */\nexport interface DurableTurnLock {\n workspaceId: string\n threadId: string\n scope: TurnLockScope\n executionId: string\n lockId: string\n startedAt: number\n expiresAt: number\n turnId?: string\n /** The turn released this lock, but a product-owned post-turn task (e.g.\n * file persistence reading the box) is still running, so the release is\n * parked on the lock until the task settles. Written only through the DO's\n * defer seam — the base package never sets it on its own. */\n releasePending?: boolean\n}\n\n/** Define input parameters required to acquire a turn-based lock in a workspace thread */\nexport interface TurnLockAcquireInput {\n workspaceId: string\n threadId: string\n scope: TurnLockScope\n executionId: string\n lockId: string\n turnId?: string\n}\n\n/** Resolve the result of attempting to acquire a turn lock indicating success or active lock status */\nexport type TurnLockAcquireResult =\n | { acquired: true; lock: DurableTurnLock }\n | { acquired: false; active: DurableTurnLock }\n\n/** Define input parameters required to release a turn lock in a specific workspace thread */\nexport interface TurnLockReleaseInput {\n workspaceId: string\n threadId: string\n scope: TurnLockScope\n executionId: string\n lockId: string\n}\n\n/** Fenced out-of-band release (stop button, stale-lock reconciliation). The\n * fences make it refuse a SUCCESSOR lock: `interruptedAt` must not precede\n * the lock's own start, and when either side names a turn, both must name\n * the same one. */\nexport interface TurnLockInterruptedReleaseInput {\n workspaceId: string\n threadId: string\n /** Try only this scope; omit to try workspace then thread. */\n scope?: TurnLockScope\n interruptedAt: number\n turnId?: string\n}\n\n/** `stored` is what the DO read from storage; expired locks are dead. */\nexport function activeTurnLock(stored: DurableTurnLock | undefined, now: number): DurableTurnLock | null {\n if (!stored) return null\n // Locks written before the scope field existed default to thread scope.\n const lock = stored.scope ? stored : { ...stored, scope: 'thread' as const }\n return lock.expiresAt > now ? lock : null\n}\n\n/** Create a durable turn lock object with timing and scope based on input parameters */\nexport function createTurnLock(input: TurnLockAcquireInput, now: number, ttlMs = TURN_LOCK_TTL_MS): DurableTurnLock {\n return {\n workspaceId: input.workspaceId,\n threadId: input.threadId,\n scope: input.scope,\n executionId: input.executionId,\n lockId: input.lockId,\n startedAt: now,\n expiresAt: now + ttlMs,\n ...(input.turnId ? { turnId: input.turnId } : {}),\n }\n}\n\n/** A cooperative release matches the execution and, when supplied, lock ID. */\nexport function turnLockMatchesRelease(\n active: DurableTurnLock,\n input: { executionId: string; lockId?: string },\n): boolean {\n if (active.executionId !== input.executionId) return false\n if (input.lockId && active.lockId !== input.lockId) return false\n return true\n}\n\n/**\n * The interrupted/stale release fence. `interruptedAt` is the instant the\n * releasing evidence was observed (the stop click, the stale-lock probe) —\n * a lock STARTED after that instant is a successor the evidence says nothing\n * about, so it survives. When the lock recorded a client turnId, the release\n * must name the same turn; a lock without one refuses a turn-specific\n * release (it cannot prove it is that turn).\n */\nexport function interruptedReleaseApplies(\n active: DurableTurnLock,\n input: { threadId: string; interruptedAt: number; turnId?: string },\n): boolean {\n if (active.threadId !== input.threadId) return false\n if (active.startedAt > input.interruptedAt) return false\n if (active.turnId) {\n if (active.turnId !== input.turnId) return false\n } else if (input.turnId) {\n return false\n }\n return true\n}\n\n// ── wire contract (adapter ↔ DO endpoint paths) ─────────────────────────────\n//\n// Kept byte-identical to the reference consumer's DO where an endpoint\n// existed there, so a product deletes its fork by re-pointing a binding, not\n// by re-speaking a protocol.\n\n/** Provide constant paths for managing chat turn streams and locks */\nexport const TURN_STREAM_PATHS = {\n broadcast: '/broadcast',\n lockAcquire: '/chat-turn-lock/acquire',\n /** Read the active lock without changing it; also brings the object up before a turn needs it. */\n lockPeek: '/chat-turn-lock/peek',\n lockRelease: '/chat-turn-lock/release',\n lockReleaseInterrupted: '/chat-turn-lock/release-interrupted',\n turnEventsAppend: '/turn-events/append',\n turnEventsRead: '/turn-events/read',\n turnStatusSet: '/turn-status/set',\n turnStatusGet: '/turn-status/get',\n scopeStatusSet: '/turn-scope/set',\n scopeRunningList: '/turn-scope/running',\n} as const\n\n/** Storage keys inside a DO instance. Exported for subclass coexistence —\n * a product extending the DO must not collide with these. */\nexport const TURN_STREAM_STORAGE_KEYS = {\n lock: 'chatTurnLock',\n activeThreads: 'activeThreads',\n turnStatus: 'turnStatus',\n turnScope: 'turnScopeIndex',\n turnEventPrefix: 'turnEvent:',\n} as const\n\n/** Zero-padded seq so DO storage `list({ prefix })` returns rows in replay\n * order without a sort. 10 digits holds any realistic turn. */\nexport function turnEventStorageKey(seq: number): string {\n return `${TURN_STREAM_STORAGE_KEYS.turnEventPrefix}${String(seq).padStart(10, '0')}`\n}\n","/**\n * Durable Object transport for thread/workspace locks, workspace signals,\n * detached turn-event rows, and running-turn discovery.\n * Cloudflare's named object identity binds every request's capability token.\n * Worker and DO code share TURN_STREAM_AUTH_SECRET; absent configuration fails closed.\n *\n * Sockets use hibernation attachments; products may add authenticated endpoints,\n * defer lock release during persistence, and contribute workspace sync events.\n * Product storage keys must avoid TURN_STREAM_STORAGE_KEYS.\n */\n\nimport { DEFAULT_RUNNING_TURN_LEASE_MS } from '../stream/turn-buffer'\n\nimport {\n ACTIVITY_TTL_MS,\n MAX_RECENT_CREATED,\n TURN_LOCK_TTL_MS,\n TURN_STREAM_PATHS,\n TURN_STREAM_STORAGE_KEYS,\n TURN_STREAM_TOKEN_HEADER,\n activeTurnLock,\n createTurnLock,\n interruptedReleaseApplies,\n pruneStaleThreads,\n turnEventStorageKey,\n turnLockMatchesRelease,\n verifyTurnStreamToken,\n type DurableTurnLock,\n type TurnLockAcquireResult,\n type TurnLockScope,\n type TurnStreamEvent,\n} from './core'\n\n// ── structural Cloudflare surface ───────────────────────────────────────────\n\n/** The socket surface the DO touches. Cloudflare's hibernatable `WebSocket`\n * satisfies it. */\nexport interface TurnStreamSocket {\n send(data: string): void\n close(code?: number, reason?: string): void\n serializeAttachment(value: unknown): void\n deserializeAttachment(): unknown\n}\n\n/** The storage surface the DO touches (Cloudflare `DurableObjectStorage`\n * satisfies it structurally). `list` must return keys in ascending order —\n * the turn-event rows rely on it for replay order. */\nexport interface TurnStreamStorage {\n get<T = unknown>(key: string): Promise<T | undefined>\n put<T = unknown>(key: string, value: T): Promise<void>\n delete(key: string): Promise<boolean | void>\n list<T = unknown>(options: { prefix: string; start?: string }): Promise<Map<string, T>>\n}\n\n/** The `DurableObjectState` surface the DO uses. */\nexport interface TurnStreamDOState {\n storage: TurnStreamStorage\n acceptWebSocket(ws: TurnStreamSocket): void\n getWebSockets(): TurnStreamSocket[]\n /** The instance's named identity — Cloudflare's `DurableObjectState.id`\n * satisfies this structurally (`state.id.name` for `idFromName` bindings).\n * Capability tokens are bound to this name, so it must be present for the\n * DO to serve any request. */\n id?: { name?: string | null }\n}\n\ninterface SocketMeta {\n sessionId: string\n scope: 'thread' | 'workspace'\n /** Live fan-out is withheld until the client has run its initial `sync`\n * (see {@link TurnStreamDO.webSocketMessage}). This makes replay and live\n * delivery mutually exclusive per socket, so frames can neither interleave\n * out of order nor drop in the connect race. */\n synced: boolean\n}\n\n// ── request/response plumbing ───────────────────────────────────────────────\n\nfunction jsonResponse(body: unknown, status = 200): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'Content-Type': 'application/json' },\n })\n}\n\nfunction stringValue(value: unknown): string | null {\n return typeof value === 'string' && value.trim() ? value.trim() : null\n}\n\nasync function jsonBody(request: Request): Promise<Record<string, unknown> | null> {\n const body = await request.json().catch(() => null)\n return body && typeof body === 'object' && !Array.isArray(body) ? (body as Record<string, unknown>) : null\n}\n\n/** Per-scope running-turn index entry (one record per buffered turn). */\ninterface ScopeTurnEntry {\n status: 'running' | 'complete' | 'error'\n updatedAt: number\n}\n\n/** Cap on remembered turns per scope; oldest terminal entries are pruned\n * first, so a busy thread's index stays bounded without ever dropping a\n * still-running turn. */\nconst MAX_SCOPE_TURNS = 100\n\n// ── the DO ──────────────────────────────────────────────────────────────────\n\n/** Define options to override default TTL and event limits for TURN stream DO operations */\nexport interface TurnStreamDOOptions {\n /** Override {@link TURN_LOCK_TTL_MS}. */\n lockTtlMs?: number\n /** Override {@link ACTIVITY_TTL_MS}. */\n activityTtlMs?: number\n /** How long an unrenewed running turn remains discoverable. */\n runningTurnLeaseMs?: number\n /**\n * HMAC secret for the channel-bound capability tokens every request must\n * carry (issue #746). When omitted, the DO reads `TURN_STREAM_AUTH_SECRET`\n * from its `env`. Fail-closed: absent or shorter than 32 chars, every\n * request answers 500 with the fix in the message — there is deliberately\n * no unauthenticated mode.\n */\n authSecret?: string\n}\n\n/** Durable turn-event storage, workspace signals, and the single-flight turn\n * lock, on one channel-bound Durable Object. Every request — including\n * subclass product routes — passes the capability-token gate in\n * {@link TurnStreamDO.fetch} (issue #746). */\nexport class TurnStreamDO {\n protected readonly state: TurnStreamDOState\n protected readonly env: unknown\n protected readonly options: TurnStreamDOOptions\n\n // Workspace channel: recent thread.created markers (in-memory, best-effort)\n // + durable responding set (threadId → startedAt) that survives eviction.\n private recentCreated: TurnStreamEvent[] = []\n private activeThreads: Map<string, number> | null = null\n\n constructor(state: TurnStreamDOState, env?: unknown, options: TurnStreamDOOptions = {}) {\n this.state = state\n this.env = env\n this.options = options\n }\n\n /** The token-verification secret: explicit option first, then the\n * `TURN_STREAM_AUTH_SECRET` env binding. Protected so a product subclass\n * with its own secret store overrides ONE method instead of copying the\n * resolution. */\n protected resolveAuthSecret(): string | undefined {\n const explicit = this.options.authSecret\n if (explicit) return explicit\n const fromEnv = (this.env as { TURN_STREAM_AUTH_SECRET?: unknown } | undefined)?.TURN_STREAM_AUTH_SECRET\n return typeof fromEnv === 'string' && fromEnv ? fromEnv : undefined\n }\n\n /**\n * The single authentication gate for EVERYTHING this DO serves — base\n * endpoints, the WebSocket upgrade, and subclass product routes (they are\n * dispatched from {@link fetch} after this check). A request must present a\n * capability token minted for THIS instance's channel name: cross-channel\n * replay is impossible, an unauthenticated forward gets 401, and a missing\n * secret fails closed with a 500 that names the fix (issue #746).\n */\n private async authorizeRequest(request: Request): Promise<Response | null> {\n const name = this.state.id?.name\n if (typeof name !== 'string' || !name) {\n return Response.json(\n { error: 'turn-stream DO has no channel name; bind with idFromName so capability tokens can be bound to it' },\n { status: 500 },\n )\n }\n const secret = this.resolveAuthSecret()\n if (!secret || secret.length < 32) {\n return Response.json(\n { error: 'turn-stream DO requires authSecret (option or TURN_STREAM_AUTH_SECRET, >= 32 chars); refusing unauthenticated mode' },\n { status: 500 },\n )\n }\n const token = request.headers.get(TURN_STREAM_TOKEN_HEADER) ?? ''\n if (await verifyTurnStreamToken(name, token, secret)) return null\n return Response.json({ error: 'invalid or missing turn-stream capability token' }, { status: 401 })\n }\n\n async fetch(request: Request): Promise<Response> {\n const url = new URL(request.url)\n\n const unauthorized = await this.authorizeRequest(request)\n if (unauthorized) return unauthorized\n\n if (request.headers.get('Upgrade')?.toLowerCase() === 'websocket') {\n return this.handleWebSocketUpgrade(request, url)\n }\n\n if (request.method === 'POST') {\n switch (url.pathname) {\n case TURN_STREAM_PATHS.broadcast:\n return this.handleBroadcast(request)\n case TURN_STREAM_PATHS.lockAcquire:\n return this.handleLockAcquire(request)\n case TURN_STREAM_PATHS.lockPeek:\n return jsonResponse({ active: await this.loadActiveLock() })\n case TURN_STREAM_PATHS.lockRelease:\n return this.handleLockRelease(request)\n case TURN_STREAM_PATHS.lockReleaseInterrupted:\n return this.handleLockReleaseInterrupted(request)\n case TURN_STREAM_PATHS.turnEventsAppend:\n return this.handleTurnEventsAppend(request)\n case TURN_STREAM_PATHS.turnEventsRead:\n return this.handleTurnEventsRead(request)\n case TURN_STREAM_PATHS.turnStatusSet:\n return this.handleTurnStatusSet(request)\n case TURN_STREAM_PATHS.turnStatusGet:\n return this.handleTurnStatusGet()\n case TURN_STREAM_PATHS.scopeStatusSet:\n return this.handleScopeStatusSet(request)\n case TURN_STREAM_PATHS.scopeRunningList:\n return this.handleScopeRunningList()\n }\n }\n\n const product = await this.handleProductRequest(request, url)\n if (product) return product\n return request.method === 'POST'\n ? jsonResponse({ error: `Unknown turn-stream endpoint ${url.pathname}` }, 404)\n : new Response('Method not allowed', { status: 405 })\n }\n\n // ── product extension seam ────────────────────────────────────────────────\n\n /** Called for any request no base endpoint claimed (before the 404), so a\n * subclass adds product endpoints without touching base routing. Return\n * `null` to decline. */\n protected async handleProductRequest(_request: Request, _url: URL): Promise<Response | null> {\n return null\n }\n\n /** Consulted before any lock release (cooperative or interrupted). Return `true` while a product-owned\n * post-turn task for `executionId` must keep the scope serialized (e.g.\n * file persistence still reading the box) — the release is then parked as\n * `releasePending` on the lock and completed via\n * {@link completeDeferredLockRelease}. Base: never defer. */\n protected async shouldDeferLockRelease(_executionId: string): Promise<boolean> {\n return false\n }\n\n /** Complete a release parked by {@link shouldDeferLockRelease}. A subclass\n * calls this when its deferred condition settles. */\n protected async completeDeferredLockRelease(executionId: string): Promise<boolean> {\n const active = await this.loadActiveLock()\n if (!active?.releasePending || active.executionId !== executionId) return false\n await this.state.storage.delete(TURN_STREAM_STORAGE_KEYS.lock)\n return true\n }\n\n /** Extra product state replayed to a socket during its `sync`, after the\n * base replay for its scope (e.g. an in-flight persistence status card).\n * Base: none. */\n protected async productSyncEvents(_scope: 'thread' | 'workspace', _meta: { sessionId: string }): Promise<TurnStreamEvent[]> {\n return []\n }\n\n // ── WebSocket channel ─────────────────────────────────────────────────────\n\n private handleWebSocketUpgrade(_request: Request, url: URL): Response {\n const sessionId = url.searchParams.get('sessionId')\n if (!sessionId) return new Response('Missing sessionId', { status: 400 })\n const scope = url.searchParams.get('scope') === 'workspace' ? 'workspace' : 'thread'\n\n const PairCtor = (globalThis as { WebSocketPair?: new () => Record<number, unknown> }).WebSocketPair\n if (!PairCtor) {\n return new Response('WebSocket upgrades require the Cloudflare runtime', { status: 501 })\n }\n const pair = new PairCtor()\n const client = pair[0]\n const server = pair[1] as TurnStreamSocket\n\n // Accept for hibernation and record routing metadata. No replay is pushed\n // here: sending before the client finishes the 101 handshake is racy\n // (frames can drop). The client issues a `sync` once open; replay happens\n // there.\n this.state.acceptWebSocket(server)\n server.serializeAttachment({ sessionId, scope, synced: false } satisfies SocketMeta)\n\n return new Response(null, { status: 101, webSocket: client } as ResponseInit)\n }\n\n /**\n * First (and only) client message after open: `{ type: 'sync', afterSeq }`.\n * Replays the current state for the socket's scope, then marks it `synced`\n * so live broadcasts start flowing. Because the DO is single-threaded, the\n * replay snapshot and the synced flip are atomic w.r.t. broadcasts — every\n * event reaches the socket exactly once, in order, via replay XOR live\n * fan-out.\n */\n async webSocketMessage(ws: TurnStreamSocket, message: string | ArrayBuffer): Promise<void> {\n const meta = ws.deserializeAttachment() as SocketMeta | null\n if (!meta || meta.synced) return\n try {\n const parsed = JSON.parse(typeof message === 'string' ? message : '') as { type?: string }\n if (parsed.type !== 'sync') return\n } catch {\n return\n }\n\n {\n // Replay the current responding state and recent thread-created markers.\n const active = await this.loadActiveThreads()\n const removed = pruneStaleThreads(active, Date.now(), this.options.activityTtlMs ?? ACTIVITY_TTL_MS)\n if (removed.length > 0) await this.persistActiveThreads(active)\n for (const [threadId, startedAt] of active) {\n this.trySend(ws, {\n type: 'thread.activity',\n data: { threadId, phase: 'start', sessionId: meta.sessionId },\n timestamp: startedAt,\n })\n }\n for (const event of this.recentCreated) this.trySend(ws, event)\n }\n for (const event of await this.productSyncEvents(meta.scope, { sessionId: meta.sessionId })) {\n this.trySend(ws, event)\n }\n\n ws.serializeAttachment({ ...meta, synced: true } satisfies SocketMeta)\n }\n\n webSocketClose(ws: TurnStreamSocket, code: number, reason: string): void {\n try {\n ws.close(code, reason)\n } catch {\n // Already closed.\n }\n }\n\n webSocketError(): void {\n // Socket dropped; getWebSockets() will no longer include it.\n }\n\n private trySend(ws: TurnStreamSocket, event: TurnStreamEvent): void {\n try {\n ws.send(JSON.stringify(event))\n } catch {\n // Client may not be ready yet, or is closing.\n }\n }\n\n // ── broadcast (live fanout + activity) ─────────────────────────────────────\n\n private async handleBroadcast(request: Request): Promise<Response> {\n const incoming = (await request.json()) as TurnStreamEvent\n const data = (incoming.data ?? {}) as Record<string, unknown>\n const sessionId = typeof data.sessionId === 'string' ? data.sessionId : undefined\n\n if (incoming.type === 'thread.activity') {\n const threadId = typeof data.threadId === 'string' ? data.threadId : undefined\n if (threadId) {\n const active = await this.loadActiveThreads()\n if (data.phase === 'end') active.delete(threadId)\n else active.set(threadId, Date.now())\n await this.persistActiveThreads(active)\n }\n } else if (incoming.type === 'thread.created') {\n this.recentCreated.push(incoming)\n if (this.recentCreated.length > MAX_RECENT_CREATED) {\n this.recentCreated = this.recentCreated.slice(-MAX_RECENT_CREATED)\n }\n }\n // (The per-turn rebroadcast lane that used to live here — segment\n // buffering + terminal-event lock auto-release — was REMOVED with the\n // deprecated segment store. Lock release is owned by the cooperative\n // release and the stale-lock reconciler, which are the paths that\n // actually free a wedged lane.)\n\n // Fan out to live sockets for this session. Enumerate accepted sockets so\n // fan-out is correct after WebSocket hibernation (no in-memory socket map).\n // Only `synced` sockets receive live frames; an un-synced socket is still\n // mid-handshake and will pick this event up in its `sync` replay snapshot.\n const message = JSON.stringify(incoming)\n for (const ws of this.state.getWebSockets()) {\n const meta = ws.deserializeAttachment() as SocketMeta | null\n if (!meta?.synced) continue\n if (!sessionId || meta.sessionId === sessionId) {\n try {\n ws.send(message)\n } catch {\n // Dead/closing socket — getWebSockets() will stop returning it.\n }\n }\n }\n\n return new Response('OK', { status: 200 })\n }\n\n private async loadActiveThreads(): Promise<Map<string, number>> {\n if (this.activeThreads === null) {\n const stored = await this.state.storage.get<Record<string, number>>(TURN_STREAM_STORAGE_KEYS.activeThreads)\n this.activeThreads = new Map(Object.entries(stored ?? {}))\n }\n return this.activeThreads\n }\n\n private async persistActiveThreads(map: Map<string, number>): Promise<void> {\n await this.state.storage.put(TURN_STREAM_STORAGE_KEYS.activeThreads, Object.fromEntries(map))\n }\n\n // ── chat-turn lock ────────────────────────────────────────────────────────\n\n protected async loadActiveLock(now = Date.now()): Promise<DurableTurnLock | null> {\n const stored = await this.state.storage.get<DurableTurnLock>(TURN_STREAM_STORAGE_KEYS.lock)\n const lock = activeTurnLock(stored, now)\n if (stored && !lock) await this.state.storage.delete(TURN_STREAM_STORAGE_KEYS.lock)\n return lock\n }\n\n private async releaseActiveLock(input: { executionId: string; lockId?: string }): Promise<boolean> {\n const active = await this.loadActiveLock()\n if (!active || !turnLockMatchesRelease(active, input)) return false\n await this.state.storage.delete(TURN_STREAM_STORAGE_KEYS.lock)\n return true\n }\n\n /** Park a release on the lock itself; {@link completeDeferredLockRelease}\n * finishes it once the product's deferred condition settles. */\n private async deferLockRelease(input: { executionId: string; lockId?: string }): Promise<boolean> {\n const active = await this.loadActiveLock()\n if (!active || !turnLockMatchesRelease(active, input)) return false\n if (active.releasePending) return true\n await this.state.storage.put(TURN_STREAM_STORAGE_KEYS.lock, { ...active, releasePending: true })\n return true\n }\n\n private async handleLockAcquire(request: Request): Promise<Response> {\n const body = await jsonBody(request)\n const workspaceId = stringValue(body?.workspaceId)\n const threadId = stringValue(body?.threadId)\n const executionId = stringValue(body?.executionId)\n const lockId = stringValue(body?.lockId)\n const scope: TurnLockScope | null =\n body?.scope === 'workspace' ? 'workspace' : body?.scope === 'thread' ? 'thread' : null\n const turnId = stringValue(body?.turnId) ?? undefined\n if (!workspaceId || !threadId || !executionId || !lockId || !scope) {\n return jsonResponse({ error: 'Missing workspaceId, threadId, executionId, lockId, or scope' }, 400)\n }\n\n const active = await this.loadActiveLock()\n if (active) {\n return jsonResponse({ acquired: false, active } satisfies TurnLockAcquireResult, 409)\n }\n\n const lock = createTurnLock(\n { workspaceId, threadId, scope, executionId, lockId, ...(turnId ? { turnId } : {}) },\n Date.now(),\n this.options.lockTtlMs ?? TURN_LOCK_TTL_MS,\n )\n await this.state.storage.put(TURN_STREAM_STORAGE_KEYS.lock, lock)\n return jsonResponse({ acquired: true, lock } satisfies TurnLockAcquireResult)\n }\n\n private async handleLockRelease(request: Request): Promise<Response> {\n const body = await jsonBody(request)\n const executionId = stringValue(body?.executionId)\n const lockId = stringValue(body?.lockId)\n if (!executionId || !lockId) {\n return jsonResponse({ error: 'Missing executionId or lockId' }, 400)\n }\n\n if (await this.shouldDeferLockRelease(executionId)) {\n const deferred = await this.deferLockRelease({ executionId, lockId })\n return jsonResponse({ released: false, deferred })\n }\n const released = await this.releaseActiveLock({ executionId, lockId })\n return jsonResponse({ released })\n }\n\n private async handleLockReleaseInterrupted(request: Request): Promise<Response> {\n const body = await jsonBody(request)\n const threadId = stringValue(body?.threadId)\n const interruptedAt =\n typeof body?.interruptedAt === 'number' && Number.isFinite(body.interruptedAt) ? body.interruptedAt : undefined\n if (!threadId || interruptedAt === undefined) {\n return jsonResponse({ error: 'Missing threadId or interruptedAt' }, 400)\n }\n const turnId = stringValue(body?.turnId) ?? undefined\n\n const active = await this.loadActiveLock()\n if (!active) return jsonResponse({ released: false })\n // A deferred product task holds the scope even against an out-of-band\n // release — the box is still being read.\n if (await this.shouldDeferLockRelease(active.executionId)) {\n return jsonResponse({ released: false })\n }\n if (!interruptedReleaseApplies(active, { threadId, interruptedAt, ...(turnId ? { turnId } : {}) })) {\n return jsonResponse({ released: false })\n }\n await this.state.storage.delete(TURN_STREAM_STORAGE_KEYS.lock)\n return jsonResponse({ released: true })\n }\n\n // ── durable turn-event storage (TurnEventStore backing) ───────────────────\n\n private async handleTurnEventsAppend(request: Request): Promise<Response> {\n const body = await jsonBody(request)\n const events = Array.isArray(body?.events) ? body.events : null\n if (!events) return jsonResponse({ error: 'Missing events' }, 400)\n for (const row of events) {\n const seq = (row as { seq?: unknown }).seq\n const event = (row as { event?: unknown }).event\n if (typeof seq !== 'number' || !Number.isInteger(seq) || seq < 1 || typeof event !== 'string') {\n return jsonResponse({ error: 'Invalid turn-event row' }, 400)\n }\n }\n const status = await this.state.storage.get<string>(TURN_STREAM_STORAGE_KEYS.turnStatus)\n if (status === 'complete' || status === 'error') return jsonResponse({ appended: 0 })\n for (const row of events as Array<{ seq: number; event: string }>) {\n await this.state.storage.put(turnEventStorageKey(row.seq), row.event)\n }\n return jsonResponse({ appended: events.length })\n }\n\n private async handleTurnEventsRead(request: Request): Promise<Response> {\n const body = await jsonBody(request)\n const fromSeq = typeof body?.fromSeq === 'number' && Number.isFinite(body.fromSeq) ? Math.trunc(body.fromSeq) : 0\n const rows = await this.state.storage.list<string>({\n prefix: TURN_STREAM_STORAGE_KEYS.turnEventPrefix,\n start: turnEventStorageKey(fromSeq + 1),\n })\n const events: Array<{ seq: number; event: string }> = []\n for (const [key, event] of rows) {\n const seq = Number(key.slice(TURN_STREAM_STORAGE_KEYS.turnEventPrefix.length))\n if (Number.isFinite(seq) && seq > fromSeq) events.push({ seq, event })\n }\n return jsonResponse({ events })\n }\n\n private async handleTurnStatusSet(request: Request): Promise<Response> {\n const body = await jsonBody(request)\n const status = body?.status\n if (status !== 'running' && status !== 'complete' && status !== 'error') {\n return jsonResponse({ error: 'Invalid status' }, 400)\n }\n await this.state.storage.put(TURN_STREAM_STORAGE_KEYS.turnStatus, status)\n return jsonResponse({ ok: true })\n }\n\n private async handleTurnStatusGet(): Promise<Response> {\n const status = await this.state.storage.get<string>(TURN_STREAM_STORAGE_KEYS.turnStatus)\n return jsonResponse({ status: status ?? null })\n }\n\n // ── scope index (listRunning reconnect discovery) ─────────────────────────\n\n private async handleScopeStatusSet(request: Request): Promise<Response> {\n const body = await jsonBody(request)\n const turnId = stringValue(body?.turnId)\n const status = body?.status\n if (!turnId || (status !== 'running' && status !== 'complete' && status !== 'error')) {\n return jsonResponse({ error: 'Invalid scope-status request' }, 400)\n }\n const index =\n (await this.state.storage.get<Record<string, ScopeTurnEntry>>(TURN_STREAM_STORAGE_KEYS.turnScope)) ?? {}\n index[turnId] = { status, updatedAt: Date.now() }\n const entries = Object.entries(index)\n if (entries.length > MAX_SCOPE_TURNS) {\n // Prune oldest terminal entries first; a running turn is never dropped.\n const removable = entries\n .filter(([, entry]) => entry.status !== 'running')\n .sort((a, b) => a[1].updatedAt - b[1].updatedAt)\n for (const [id] of removable.slice(0, entries.length - MAX_SCOPE_TURNS)) {\n delete index[id]\n }\n }\n await this.state.storage.put(TURN_STREAM_STORAGE_KEYS.turnScope, index)\n return jsonResponse({ ok: true })\n }\n\n private async handleScopeRunningList(): Promise<Response> {\n const index =\n (await this.state.storage.get<Record<string, ScopeTurnEntry>>(TURN_STREAM_STORAGE_KEYS.turnScope)) ?? {}\n const cutoff = Date.now() - Math.max(\n 1,\n this.options.runningTurnLeaseMs ?? DEFAULT_RUNNING_TURN_LEASE_MS,\n )\n const running = Object.entries(index)\n .filter(([, entry]) => entry.status === 'running' && entry.updatedAt >= cutoff)\n .sort((a, b) => b[1].updatedAt - a[1].updatedAt)\n .map(([turnId]) => turnId)\n return jsonResponse({ running })\n }\n}\n","/**\n * Worker adapters for durable turn storage, single-flight locks, and workspace signals.\n * Namespace interfaces remain structural so Cloudflare bindings and the memory harness\n * run the same authenticated request path.\n * Products provide workspace authorization and their binding's TURN_STREAM_AUTH_SECRET.\n */\n\nimport { reconcileStaleTurnLock, type ReconcileStaleTurnLockOptions } from '../chat-routes/stale-turn-lock'\nimport type { TurnEventStore, TurnStatus } from '../stream/turn-buffer'\nimport {\n TURN_STREAM_PATHS,\n TURN_STREAM_TOKEN_HEADER,\n mintTurnStreamToken,\n scopeIndexChannelKey,\n turnLockChannelKey,\n turnStorageChannelKey,\n workspaceChannelKey,\n type DurableTurnLock,\n type TurnLockAcquireResult,\n type TurnLockReleaseInput,\n type TurnLockScope,\n type TurnStreamEvent,\n} from './core'\n\n// ── structural namespace ────────────────────────────────────────────────────\n\n/** Resolve a stub interface for handling fetch requests with optional initialization parameters */\nexport interface TurnStreamStubLike {\n fetch(input: Request | string, init?: RequestInit): Promise<Response>\n}\n\n/** The surface of `DurableObjectNamespace` the adapters use. */\nexport interface TurnStreamNamespaceLike {\n idFromName(name: string): unknown\n get(id: unknown): TurnStreamStubLike\n}\n\nconst INTERNAL_ORIGIN = 'https://turn-stream.internal'\n\n/** The HMAC secret shared between this worker and its `TurnStreamDO` binding.\n * Every adapter call mints a channel-bound capability token from it; the DO\n * refuses requests without one (issue #746). Products pass the same value\n * the DO resolves — `env.TURN_STREAM_AUTH_SECRET`. */\nexport interface TurnStreamAuth {\n secret: string\n}\n\nasync function postJson<T>(\n namespace: TurnStreamNamespaceLike,\n channelKey: string,\n path: string,\n body: unknown,\n auth: TurnStreamAuth,\n): Promise<{ status: number; body: T }> {\n const stub = namespace.get(namespace.idFromName(channelKey))\n const response = await stub.fetch(`${INTERNAL_ORIGIN}${path}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n [TURN_STREAM_TOKEN_HEADER]: await mintTurnStreamToken(channelKey, auth.secret),\n },\n body: JSON.stringify(body),\n })\n return { status: response.status, body: (await response.json()) as T }\n}\n\nasync function postJsonOk<T>(\n namespace: TurnStreamNamespaceLike,\n channelKey: string,\n path: string,\n body: unknown,\n auth: TurnStreamAuth,\n): Promise<T> {\n const result = await postJson<T>(namespace, channelKey, path, body, auth)\n if (result.status !== 200) {\n throw new Error(`turn-stream ${path} failed with status ${result.status}`)\n }\n return result.body\n}\n\n// ── TurnEventStore adapter (the real turnStore) ─────────────────────────────\n\n/**\n * Optional Durable Object backend for {@link TurnEventStore}.\n * Each turn has its own named instance; a per-scope index tracks running turns.\n * Products choose this adapter or the D1 backend independently of their lock\n * and workspace-signal transport.\n */\nexport function createDurableObjectTurnEventStore(\n namespace: TurnStreamNamespaceLike,\n auth: TurnStreamAuth,\n): TurnEventStore {\n return {\n async append(turnId, events) {\n if (!events.length) return\n await postJsonOk(namespace, turnStorageChannelKey(turnId), TURN_STREAM_PATHS.turnEventsAppend, { events }, auth)\n },\n async read(turnId, fromSeq) {\n const body = await postJsonOk<{ events: Array<{ seq: number; event: string }> }>(\n namespace,\n turnStorageChannelKey(turnId),\n TURN_STREAM_PATHS.turnEventsRead,\n { fromSeq },\n auth,\n )\n return body.events\n },\n async setStatus(turnId, status, scopeId, options) {\n // This store keeps no update time to preserve or compare; refuse rather than write unguarded.\n if (options?.preserveUpdatedAt || options?.onlyIfRunningBefore !== undefined) {\n throw new Error('The turn-stream Durable Object store does not support guarded or time-preserving status writes')\n }\n await postJsonOk(namespace, turnStorageChannelKey(turnId), TURN_STREAM_PATHS.turnStatusSet, { status }, auth)\n if (scopeId) {\n await postJsonOk(namespace, scopeIndexChannelKey(scopeId), TURN_STREAM_PATHS.scopeStatusSet, {\n turnId,\n status,\n }, auth)\n }\n },\n async getStatus(turnId) {\n const body = await postJsonOk<{ status: TurnStatus | null }>(\n namespace,\n turnStorageChannelKey(turnId),\n TURN_STREAM_PATHS.turnStatusGet,\n {},\n auth,\n )\n return body.status\n },\n async listRunning(scopeId) {\n const body = await postJsonOk<{ running: string[] }>(\n namespace,\n scopeIndexChannelKey(scopeId),\n TURN_STREAM_PATHS.scopeRunningList,\n {},\n auth,\n )\n return body.running\n },\n }\n}\n\n// ── lock primitives (usable outside the route seam too) ─────────────────────\n\n/** Define input parameters required to acquire a durable turn lock in a workspace thread context */\nexport interface AcquireDurableTurnLockInput {\n workspaceId: string\n threadId: string\n scope: TurnLockScope\n executionId: string\n turnId?: string\n /** Supply to reclaim/retry with a stable id; default mints a UUID. */\n lockId?: string\n}\n\n/** Acquire a durable turn lock in the specified namespace with given input parameters */\nexport async function acquireDurableTurnLock(\n namespace: TurnStreamNamespaceLike,\n input: AcquireDurableTurnLockInput,\n auth: TurnStreamAuth,\n): Promise<TurnLockAcquireResult> {\n const lockId = input.lockId ?? crypto.randomUUID()\n const key = turnLockChannelKey(input.workspaceId, input.threadId, input.scope)\n const result = await postJson<TurnLockAcquireResult>(namespace, key, TURN_STREAM_PATHS.lockAcquire, {\n ...input,\n lockId,\n }, auth)\n if (result.status !== 200 && result.status !== 409) {\n throw new Error(`turn-stream lock acquire failed with status ${result.status}`)\n }\n return result.body\n}\n\n/**\n * Read a channel's active turn lock without changing it.\n *\n * The first request to a Durable Object that has never been used creates it,\n * which measured 1.3-2.4 s for a thread's first turn in GTM production\n * (2026-10-10) against 0.05-0.33 s once it exists. A peek sent as soon as a\n * turn is admitted lets that creation overlap the turn's other pre-lock work.\n */\nexport async function peekDurableTurnLock(\n namespace: TurnStreamNamespaceLike,\n input: { workspaceId: string; threadId: string; scope: TurnLockScope },\n auth: TurnStreamAuth,\n): Promise<DurableTurnLock | null> {\n const key = turnLockChannelKey(input.workspaceId, input.threadId, input.scope)\n const result = await postJson<{ active?: DurableTurnLock | null }>(namespace, key, TURN_STREAM_PATHS.lockPeek, {}, auth)\n if (result.status !== 200) throw new Error(`turn-stream lock peek failed with status ${result.status}`)\n return result.body.active ?? null\n}\n\n/** Release a durable turn lock and indicate if the release was successful or deferred */\nexport async function releaseDurableTurnLock(\n namespace: TurnStreamNamespaceLike,\n input: TurnLockReleaseInput,\n auth: TurnStreamAuth,\n): Promise<{ released: boolean; deferred?: boolean }> {\n const key = turnLockChannelKey(input.workspaceId, input.threadId, input.scope)\n return postJsonOk(namespace, key, TURN_STREAM_PATHS.lockRelease, input, auth)\n}\n\n/** Define input parameters to release an interrupted durable turn lock in a workspace or thread */\nexport interface ReleaseInterruptedDurableTurnLockInput {\n workspaceId: string\n threadId: string\n /** Try only this scope; omit to try workspace then thread (a stop button\n * doesn't know which lane the wedged turn ran on). */\n scope?: TurnLockScope\n interruptedAt: number\n turnId?: string\n}\n\n/** Fenced out-of-band release — the DO refuses a successor lock (started\n * after `interruptedAt`) and a turnId mismatch. Returns whether any scope\n * released. */\nexport async function releaseInterruptedDurableTurnLock(\n namespace: TurnStreamNamespaceLike,\n input: ReleaseInterruptedDurableTurnLockInput,\n auth: TurnStreamAuth,\n): Promise<boolean> {\n const scopes: readonly TurnLockScope[] = input.scope ? [input.scope] : ['workspace', 'thread']\n for (const scope of scopes) {\n const key = turnLockChannelKey(input.workspaceId, input.threadId, scope)\n const result = await postJsonOk<{ released: boolean }>(namespace, key, TURN_STREAM_PATHS.lockReleaseInterrupted, {\n threadId: input.threadId,\n interruptedAt: input.interruptedAt,\n ...(input.turnId ? { turnId: input.turnId } : {}),\n }, auth)\n if (result.released) return true\n }\n return false\n}\n\n// ── stale-lock reconciliation against the DO ────────────────────────────────\n\n/** Define options to reconcile stale durable turn locks with context, namespace, workspace, and active lock details */\nexport interface ReconcileStaleDurableTurnLockOptions\n extends Pick<\n ReconcileStaleTurnLockOptions,\n 'probeSandbox' | 'probeSession' | 'graceMs' | 'terminalGraceMs' | 'context' | 'log' | 'now'\n > {\n namespace: TurnStreamNamespaceLike\n auth: TurnStreamAuth\n workspaceId: string\n threadId: string\n /** The lock the acquire attempt was refused on. */\n active: DurableTurnLock\n}\n\n/**\n * `/chat-routes`' `reconcileStaleTurnLock` policy wired to the DO: the\n * product supplies the probes (which box, what its session says), the policy\n * decides, and a release lands as a FENCED interrupted release —\n * `interruptedAt` is the policy's `fence.observedAt`, so a successor lock\n * acquired while the probes were in flight survives by construction.\n */\nexport async function reconcileStaleDurableTurnLock(\n options: ReconcileStaleDurableTurnLockOptions,\n): Promise<{ released: boolean; diagnostics: Record<string, unknown> }> {\n const { namespace, auth, workspaceId, threadId, active, ...policy } = options\n return reconcileStaleTurnLock({\n ...policy,\n lockStartedAt: active.startedAt,\n release: (fence) =>\n releaseInterruptedDurableTurnLock(namespace, {\n workspaceId,\n threadId: active.threadId,\n scope: active.scope,\n interruptedAt: fence.observedAt,\n ...(active.turnId ? { turnId: active.turnId } : {}),\n }, auth),\n })\n}\n\n// ── ChatTurnLock adapter (the turnLock seam) ────────────────────────────────\n\n/** The subset of `ChatTurnProduceArgs` the lock adapter reads — structural,\n * so this module needs no import from `/chat-routes/turn-routes` (which\n * would drag the `agent-runtime` peer into every `/turn-stream` consumer). */\nexport interface TurnLockSeamArgs<TContext> {\n identity: { tenantId: string; sessionId: string }\n executionId: string\n context: TContext\n body: { turnId?: string }\n}\n\n/** Verdict shape of the vertical's `turnLock.acquire`. */\nexport type TurnLockSeamResult =\n | { acquired: true; handle?: unknown }\n | { acquired: false; response: Response }\n\n/** Define options for creating a durable turn lock with customizable scope and identification methods */\nexport interface CreateDurableTurnLockOptions<TContext> {\n namespace: TurnStreamNamespaceLike\n /** Capability tokens for the DO; pass the worker's\n * `env.TURN_STREAM_AUTH_SECRET`. The DO validates every request. */\n auth: TurnStreamAuth\n /** Which lane serializes this turn: `'workspace'` (shared sandbox — one\n * turn per workspace) or `'thread'` (router lane — one turn per thread). */\n scopeOf(args: TurnLockSeamArgs<TContext>): TurnLockScope\n /** Override the execution id the lock records (e.g. a follow-up reclaiming\n * the execution it already dispatched). Default: the turn's own. */\n lockExecutionIdOf?(args: TurnLockSeamArgs<TContext>): string | undefined\n /** Client turn id recorded on the lock (fences interrupted releases to the\n * right turn). Default: the request body's `turnId`. */\n clientTurnIdOf?(args: TurnLockSeamArgs<TContext>): string | undefined\n /**\n * Attempt stale-lock recovery after a refused acquire. Return whether the\n * held lock was released (the adapter then retries the acquire once).\n * Products wire {@link reconcileStaleDurableTurnLock} with their sandbox +\n * session probes here. Omit → a refused acquire is final.\n */\n reconcile?(\n args: TurnLockSeamArgs<TContext>,\n active: DurableTurnLock,\n ): Promise<{ released: boolean; diagnostics?: Record<string, unknown> }>\n /** Build the refusal `Response`. Default: a 409 with the shared body shape\n * (`code`, `message`, lock identity + age, reconcile diagnostics). */\n onRefused?(active: DurableTurnLock, diagnostics: Record<string, unknown> | undefined): Response\n}\n\nfunction defaultRefusalResponse(\n active: DurableTurnLock,\n diagnostics: Record<string, unknown> | undefined,\n): Response {\n const workspaceConflict = active.scope === 'workspace'\n return Response.json(\n {\n error: {\n code: workspaceConflict ? 'workspace_turn_in_flight' : 'chat_turn_in_flight',\n message: workspaceConflict\n ? 'A chat turn is already running for this workspace. Wait for it to finish before starting another.'\n : 'A chat turn is already running for this thread. Reconnect to the active response or wait for it to finish.',\n threadId: active.threadId,\n executionId: active.executionId,\n ...(active.turnId ? { turnId: active.turnId } : {}),\n lockStartedAt: active.startedAt,\n lockAgeMs: Date.now() - active.startedAt,\n ...(diagnostics ? { diagnostics } : {}),\n },\n },\n { status: 409 },\n )\n}\n\n/**\n * The vertical's `turnLock` seam on the shared DO: dual-scope single-flight\n * acquire (with one reconcile-then-retry pass when the product supplies a\n * stale-lock reconciler) and cooperative release on settle. The returned\n * object satisfies `createChatTurnRoutes`' `ChatTurnLock<TContext>`\n * structurally.\n */\nexport function createDurableTurnLock<TContext>(options: CreateDurableTurnLockOptions<TContext>): {\n acquire(args: TurnLockSeamArgs<TContext>): Promise<TurnLockSeamResult>\n release(handle: unknown): Promise<void>\n warm(target: { tenantId: string; threadId: string }): Promise<void>\n} {\n return {\n // The thread channel is the one a thread's first turn creates; the\n // workspace channel is shared by every thread and its broadcasts keep it up.\n async warm(target) {\n await peekDurableTurnLock(options.namespace, { workspaceId: target.tenantId, threadId: target.threadId, scope: 'thread' }, options.auth)\n },\n async acquire(args) {\n const workspaceId = args.identity.tenantId\n const threadId = args.identity.sessionId\n const scope = options.scopeOf(args)\n const executionId = options.lockExecutionIdOf?.(args) ?? args.executionId\n const turnId = options.clientTurnIdOf ? options.clientTurnIdOf(args) : args.body.turnId\n const input: AcquireDurableTurnLockInput = {\n workspaceId,\n threadId,\n scope,\n executionId,\n ...(turnId ? { turnId } : {}),\n }\n\n let acquired = await acquireDurableTurnLock(options.namespace, input, options.auth)\n let diagnostics: Record<string, unknown> | undefined\n if (!acquired.acquired && options.reconcile) {\n const reconciled = await options.reconcile(args, acquired.active)\n diagnostics = reconciled.diagnostics\n if (reconciled.released) {\n acquired = await acquireDurableTurnLock(options.namespace, input, options.auth)\n }\n }\n if (!acquired.acquired) {\n return {\n acquired: false,\n response: (options.onRefused ?? defaultRefusalResponse)(acquired.active, diagnostics),\n }\n }\n const handle: TurnLockReleaseInput = {\n workspaceId,\n threadId,\n scope,\n executionId,\n lockId: acquired.lock.lockId,\n }\n return { acquired: true, handle }\n },\n async release(handle) {\n if (!handle) return\n await releaseDurableTurnLock(options.namespace, handle as TurnLockReleaseInput, options.auth)\n },\n }\n}\n\n// ── broadcast helpers (products wire these into onEvent) ────────────────────\n\n/** Coarse per-workspace marker that a thread's turn started / ended — drives\n * a sidebar \"agent responding\" indicator subscribed once per workspace. */\nexport async function broadcastWorkspaceActivity(\n namespace: TurnStreamNamespaceLike,\n workspaceId: string,\n threadId: string,\n phase: 'start' | 'end',\n auth: TurnStreamAuth,\n): Promise<void> {\n try {\n await postJson(namespace, workspaceChannelKey(workspaceId), TURN_STREAM_PATHS.broadcast, {\n type: 'thread.activity',\n timestamp: Date.now(),\n data: { threadId, phase, workspaceId, sessionId: workspaceId },\n } satisfies TurnStreamEvent, auth)\n } catch {\n // Best-effort: the indicator is non-critical UI.\n }\n}\n\n/** Per-workspace marker that a new thread was created, so an already-open\n * history list prepends it without a reload. */\nexport async function broadcastThreadCreated(\n namespace: TurnStreamNamespaceLike,\n workspaceId: string,\n thread: { threadId: string; title: string },\n auth: TurnStreamAuth,\n): Promise<void> {\n try {\n await postJson(namespace, workspaceChannelKey(workspaceId), TURN_STREAM_PATHS.broadcast, {\n type: 'thread.created',\n timestamp: Date.now(),\n data: { threadId: thread.threadId, title: thread.title, workspaceId, sessionId: workspaceId },\n } satisfies TurnStreamEvent, auth)\n } catch {\n // Best-effort: the loader reconciles on the next revalidation.\n }\n}\n\n// ── WebSocket upgrade forwarder (worker entry) ──────────────────────────────\n\n/** Represent success or failure of a TURN stream upgrade authorization with optional response data */\nexport type TurnStreamUpgradeAuthorization = { ok: true } | { ok: false; response: Response }\n\n/** Define options for creating a TURN stream upgrade handler including namespace, path, and authorization logic */\nexport interface CreateTurnStreamUpgradeHandlerOptions {\n namespace: TurnStreamNamespaceLike\n /** HMAC secret shared with the DO binding (`env.TURN_STREAM_AUTH_SECRET`).\n * After the product `authorize` passes, the handler mints a channel-bound\n * capability token and the DO verifies it — the browser never holds it. */\n authSecret: string\n /** The worker route serving the stream. Default `/api/session-stream`. */\n path?: string\n /** Viewer access check (session cookie → workspace membership). */\n authorize(\n request: Request,\n target: { workspaceId: string },\n ): Promise<TurnStreamUpgradeAuthorization>\n}\n\n/**\n * The worker-entry WebSocket forwarder — WORKSPACE channel only (the\n * per-thread variant went with the deprecated rebroadcast lane; a thread's\n * live turn replay is the sandbox session gateway's job). Call BEFORE the\n * app router (a router loader cannot return a 101); returns `null` for\n * requests that are not a WebSocket upgrade on the configured path.\n *\n * GET {path}?workspaceId=... → workspace channel (sidebar signals)\n *\n * After the 101, the client sends `{type:'sync'}` and receives the\n * current-activity + recent-threads snapshot, then live signals.\n *\n * Security: the product's `authorize` decides whether THIS caller may see the\n * workspace's signals; the channel-bound token this handler then mints is\n * what the DO itself verifies — two independent gates, and the browser never\n * learns the token (it rides the forwarded upgrade request only).\n */\nexport function createTurnStreamUpgradeHandler(\n options: CreateTurnStreamUpgradeHandlerOptions,\n): (request: Request) => Promise<Response | null> {\n const path = options.path ?? '/api/session-stream'\n if (!options.authSecret || options.authSecret.length < 32) {\n throw new Error('createTurnStreamUpgradeHandler requires authSecret (>= 32 chars) — the DO fails closed without tokens')\n }\n return async (request) => {\n const url = new URL(request.url)\n if (url.pathname !== path || request.headers.get('Upgrade')?.toLowerCase() !== 'websocket') return null\n\n if (url.searchParams.has('threadId')) {\n return new Response('Thread replay uses the sandbox session gateway or chat replay route', { status: 400 })\n }\n const workspaceId = url.searchParams.get('workspaceId')\n if (!workspaceId) return new Response('Missing workspaceId', { status: 400 })\n\n const auth = await options.authorize(request, { workspaceId })\n if (!auth.ok) return auth.response\n\n const key = workspaceChannelKey(workspaceId)\n const stub = options.namespace.get(options.namespace.idFromName(key))\n\n const forwardUrl = new URL(request.url)\n forwardUrl.searchParams.set('sessionId', workspaceId)\n forwardUrl.searchParams.set('scope', 'workspace')\n forwardUrl.searchParams.delete('token')\n const headers = new Headers(request.headers)\n headers.set(TURN_STREAM_TOKEN_HEADER, await mintTurnStreamToken(key, options.authSecret))\n return stub.fetch(new Request(new Request(forwardUrl, request), { headers }))\n }\n}\n","/**\n * In-process turn-stream harness: a fake namespace that routes `idFromName`\n * to REAL {@link TurnStreamDO} instances over an in-memory storage/socket\n * state. The adapters and the DO run their production code paths — only the\n * Cloudflare runtime (isolation, hibernation, real sockets) is simulated.\n *\n * The harness runs the DO in its PRODUCTION auth mode: instances carry their\n * channel name and a capability-token secret (a fixed default, overridable),\n * so tests exercise the real gate — an adapter call without `auth` fails\n * exactly like a misconfigured worker would. Test-side viewer sockets attach\n * through `channel().connect()` (inside the DO, post-upgrade), which is the\n * one hop that legitimately skips the HTTP gate.\n *\n * For vitest composition tests and keyless local dev (the same role\n * `createMemoryTurnEventStore` plays for the D1 store). Not for production:\n * state is per-process and evaporates on restart.\n */\n\nimport { TurnStreamDO, type TurnStreamDOOptions, type TurnStreamDOState, type TurnStreamSocket, type TurnStreamStorage } from './do'\nimport type { TurnStreamNamespaceLike, TurnStreamStubLike } from './adapters'\n\nfunction createMemoryStorage(): TurnStreamStorage {\n const map = new Map<string, unknown>()\n return {\n async get<T>(key: string) {\n return map.get(key) as T | undefined\n },\n async put(key, value) {\n map.set(key, value)\n },\n async delete(key) {\n return map.delete(key)\n },\n async list<T>({ prefix, start }: { prefix: string; start?: string }) {\n const keys = [...map.keys()]\n .filter((key) => key.startsWith(prefix) && (start === undefined || key >= start))\n .sort()\n return new Map(keys.map((key) => [key, map.get(key) as T]))\n },\n }\n}\n\n/** A test-side viewer socket: records frames sent by the DO and lets the\n * test drive the `sync` handshake. */\nexport interface MemoryTurnStreamSocket extends TurnStreamSocket {\n readonly frames: string[]\n readonly closed: boolean\n}\n\n/** Define an in-memory channel for streaming turn-based data with viewer socket connection support */\nexport interface MemoryTurnStreamChannel {\n /** Attach a viewer socket to this channel (bypasses the HTTP 101 — the\n * upgrade handshake is Cloudflare-runtime-only) and run its `sync`. */\n connect(input: { sessionId: string; scope: 'thread' | 'workspace'; afterSeq?: number }): Promise<MemoryTurnStreamSocket>\n readonly instance: TurnStreamDO\n}\n\n/** Provide an interface to manage channels and namespaces for memory-based turn stream testing */\nexport interface MemoryTurnStreamHarness {\n namespace: TurnStreamNamespaceLike\n /** The channel (creating its DO instance if needed) for a channel key —\n * e.g. `threadChannelKey(ws, thread)` — to connect test viewers. */\n channel(name: string): MemoryTurnStreamChannel\n}\n\ninterface MemoryInstance {\n instance: TurnStreamDO\n sockets: MemoryTurnStreamSocket[]\n}\n\n/**\n * Build the harness. `createInstance` lets a product test run its own\n * `TurnStreamDO` subclass through the same wiring.\n */\n/** Default harness secret — long enough to satisfy the DO's minimum, fixed so\n * tests are deterministic. Products testing their own subclass pass their\n * own via `options.authSecret`. */\nexport const MEMORY_TURN_STREAM_AUTH_SECRET = 'test-turn-stream-harness-secret-0123456789ab'\n\nexport function createMemoryTurnStreamHarness(\n createInstance: (state: TurnStreamDOState, authSecret: string) => TurnStreamDO = (state, secret) =>\n new TurnStreamDO(state, undefined, { authSecret: secret }),\n options: TurnStreamDOOptions & { authSecret?: string } = {},\n): MemoryTurnStreamHarness {\n const authSecret = options.authSecret ?? MEMORY_TURN_STREAM_AUTH_SECRET\n const createInst = (state: TurnStreamDOState) => createInstance(state, authSecret)\n const instances = new Map<string, MemoryInstance>()\n\n function ensure(name: string): MemoryInstance {\n let entry = instances.get(name)\n if (!entry) {\n const sockets: MemoryTurnStreamSocket[] = []\n const state: TurnStreamDOState = {\n storage: createMemoryStorage(),\n acceptWebSocket: (ws) => sockets.push(ws as MemoryTurnStreamSocket),\n getWebSockets: () => sockets.filter((ws) => !ws.closed),\n id: { name },\n }\n entry = { instance: createInst(state), sockets }\n instances.set(name, entry)\n }\n return entry\n }\n\n\n const namespace: TurnStreamNamespaceLike = {\n idFromName: (name) => name,\n get(id): TurnStreamStubLike {\n const entry = ensure(String(id))\n return {\n fetch: (input, init) =>\n entry.instance.fetch(typeof input === 'string' ? new Request(input, init) : input),\n }\n },\n }\n\n return {\n namespace,\n channel(name) {\n const entry = ensure(name)\n return {\n instance: entry.instance,\n async connect({ sessionId, scope, afterSeq = 0 }) {\n let attachment: unknown = null\n const frames: string[] = []\n let closed = false\n const socket: MemoryTurnStreamSocket = {\n get frames() {\n return frames\n },\n get closed() {\n return closed\n },\n send(data) {\n if (closed) throw new Error('socket closed')\n frames.push(data)\n },\n close() {\n closed = true\n },\n serializeAttachment(value) {\n attachment = value\n },\n deserializeAttachment() {\n return attachment\n },\n }\n socket.serializeAttachment({ sessionId, scope, synced: false })\n entry.sockets.push(socket)\n await entry.instance.webSocketMessage(socket, JSON.stringify({ type: 'sync', afterSeq }))\n return socket\n },\n }\n },\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/turn-stream/core.ts","../../src/turn-stream/do.ts","../../src/turn-stream/adapters.ts","../../src/turn-stream/memory.ts"],"sourcesContent":["/**\n * Channel tokens, lock records, workspace signals, and durable event keys.\n * Interactive turn replay belongs to the sandbox session gateway.\n * An optional turn-event backend stores durable rows and a running-turn index.\n * This core uses WebCrypto and plain data without a Cloudflare runtime import.\n */\n\nimport { base64UrlEncodeText, constantTimeEqual, hmacSha256Base64Url } from '../crypto/web-token'\n\n// ── events ──────────────────────────────────────────────────────────────────\n\n/** One event on a turn-stream channel. Workspace-signal events\n * (`thread.created`, `thread.activity`, product events) carry no `seq` —\n * ordering is delivery order on the DO's single-threaded event loop. */\nexport interface TurnStreamEvent {\n type: string\n data?: unknown\n timestamp: number\n seq?: never\n}\n\n// ── channel-bound capability tokens (issue #746) ─────────────────────────────\n//\n// The DO's endpoints are not publicly routed, but \"safe because nothing\n// forwards to it\" is convention, not enforcement: one accidental\n// `stub.fetch(publicRequest)` and every channel's lock, turn events, and\n// signals are cross-tenant writable. Every request the DO serves must now\n// carry a short-lived HMAC capability token minted for THIS DO's channel\n// name — a valid token for one channel is refused by every other, and a\n// token without the secret cannot exist. Worker code mints with the same\n// secret via the adapters (`authSecret`) and the WS upgrade forwarder;\n// nothing else needs to know the format.\n\n/** Default capability lifetime. Tokens are minted per worker request and\n * verified immediately; a WS token gates only the upgrade handshake — an\n * accepted socket lives until it closes. */\nexport const TURN_STREAM_TOKEN_TTL_MS = 60_000\n\n/** Request header the adapters attach (and the DO accepts). The worker also\n * attaches it when forwarding an authorized browser WebSocket handshake. */\nexport const TURN_STREAM_TOKEN_HEADER = 'x-turn-stream-token'\n\n/** `v1.<base64url-channel>.<exp-ms-36>.<base64url-mac>` — the expiry is inside the signed payload so\n * it cannot be extended by editing the token. */\nexport async function mintTurnStreamToken(\n channelName: string,\n secret: string,\n now: () => number = Date.now,\n ttlMs: number = TURN_STREAM_TOKEN_TTL_MS,\n): Promise<string> {\n if (!secret || secret.length < 32) throw new Error('turn-stream token secret must be at least 32 characters')\n const expiresAt = now() + ttlMs\n const payload = `v1.${base64UrlEncodeText(channelName)}.${expiresAt.toString(36)}`\n return `${payload}.${await hmacSha256Base64Url(payload, secret)}`\n}\n\n/** Verify a token for `channelName`: constant-time MAC compare with the\n * exact channel name embedded in the signed payload, plus freshness. A\n * token minted for any other channel — or a tampered expiry — is false. */\nexport async function verifyTurnStreamToken(\n channelName: string,\n token: string,\n secret: string,\n now: () => number = Date.now,\n): Promise<boolean> {\n if (!secret || secret.length < 32) return false\n const parts = token.split('.')\n if (parts.length !== 4 || parts[0] !== 'v1') return false\n const [, name, expires36, mac] = parts as [string, string, string, string]\n if (name !== base64UrlEncodeText(channelName)) return false\n if (!/^[0-9a-z]+$/.test(expires36)) return false\n const expiresAt = parseInt(expires36, 36)\n if (!Number.isSafeInteger(expiresAt) || expiresAt <= now()) return false\n const expected = await hmacSha256Base64Url(`v1.${name}.${expires36}`, secret)\n return constantTimeEqual(mac, expected)\n}\n\n// ── channel keys ────────────────────────────────────────────────────────────\n//\n// One DO instance per channel key. Four families:\n// thread `${workspaceId}:${threadId}` — thread viewer signals.\n// workspace `${workspaceId}` — sidebar activity + thread.created\n// + every turn lock: the workspace-scope lock and each\n// thread-scope lock, under separate storage keys.\n// turn `turn:${turnId}` — durable turn-event rows + status\n// (the TurnEventStore contract; replay survives DO eviction).\n// scope `scope:${scopeId}` — running-turn index for a thread,\n// backing `TurnEventStore.listRunning`.\n\n/** Define the scope level for acquiring a turn lock within thread or workspace contexts */\nexport type TurnLockScope = 'thread' | 'workspace'\n\n/** Channel for a thread's viewer signals. */\nexport function threadChannelKey(workspaceId: string, threadId: string): string {\n return `${workspaceId}:${threadId}`\n}\n\n/** Channel for workspace signals and the workspace-scoped lock. */\nexport function workspaceChannelKey(workspaceId: string): string {\n return workspaceId\n}\n\n/** The channel a lock lives on: the workspace channel, for both scopes.\n * Workspace-scope locks serialize every thread in the workspace (one shared\n * sandbox) and thread-scope locks serialize one thread (router lane); they\n * stay independent because each has its own storage key\n * ({@link turnLockStorageKey}). A thread lock on its own channel created a\n * new Durable Object on every thread's first turn, 0.8-1.9 s inside Creative's\n * admission on staging (2026-10-20) against an object that already exists. */\nexport function turnLockChannelKey(workspaceId: string, _threadId: string, _scope: TurnLockScope): string {\n return workspaceChannelKey(workspaceId)\n}\n\n/** The storage key a lock lives under inside its channel's instance: the\n * workspace-scope lock under `TURN_STREAM_STORAGE_KEYS.lock`, each thread's\n * lock under `TURN_STREAM_STORAGE_KEYS.threadLockPrefix` + its thread id. */\nexport function turnLockStorageKey(scope: TurnLockScope, threadId: string): string {\n return scope === 'workspace' ? TURN_STREAM_STORAGE_KEYS.lock : `${TURN_STREAM_STORAGE_KEYS.threadLockPrefix}${threadId}`\n}\n\n/** Channel for the optional Durable Object turn-event backend. */\nexport function turnStorageChannelKey(turnId: string): string {\n return `turn:${turnId}`\n}\n\n/** Running-turn index for the optional Durable Object turn-event backend. */\nexport function scopeIndexChannelKey(scopeId: string): string {\n return `scope:${scopeId}`\n}\n\n/** Recent thread-created markers kept for late-connecting sidebars. */\nexport const MAX_RECENT_CREATED = 50\n\n/** A responding marker older than this is treated as stale, so a dropped\n * `end` broadcast can't leave a permanently-stuck \"responding\" dot. */\nexport const ACTIVITY_TTL_MS = 15 * 60 * 1000\n\n/** Remove responding markers older than ttlMs; return their thread IDs. */\nexport function pruneStaleThreads(active: Map<string, number>, now: number, ttlMs: number): string[] {\n const removed: string[] = []\n for (const [threadId, startedAt] of active) {\n if (now - startedAt > ttlMs) {\n active.delete(threadId)\n removed.push(threadId)\n }\n }\n return removed\n}\n\n// ── chat-turn lock record + fences ──────────────────────────────────────────\n\n/** Fallback lifetime for an unreleased lock. Use stale-lock reconciliation to\n * recover earlier when execution state proves that its holder stopped. */\nexport const TURN_LOCK_TTL_MS = 30 * 60 * 1000\n\n/** The stored single-flight lock. Field-compatible with the reference\n * consumer's `ChatTurnLock` so adoption is a swap, not a migration. */\nexport interface DurableTurnLock {\n workspaceId: string\n threadId: string\n scope: TurnLockScope\n executionId: string\n lockId: string\n startedAt: number\n expiresAt: number\n turnId?: string\n /** The turn released this lock, but a product-owned post-turn task (e.g.\n * file persistence reading the box) is still running, so the release is\n * parked on the lock until the task settles. Written only through the DO's\n * defer seam — the base package never sets it on its own. */\n releasePending?: boolean\n}\n\n/** Define input parameters required to acquire a turn-based lock in a workspace thread */\nexport interface TurnLockAcquireInput {\n workspaceId: string\n threadId: string\n scope: TurnLockScope\n executionId: string\n lockId: string\n turnId?: string\n}\n\n/** Resolve the result of attempting to acquire a turn lock indicating success or active lock status */\nexport type TurnLockAcquireResult =\n | { acquired: true; lock: DurableTurnLock }\n | { acquired: false; active: DurableTurnLock }\n\n/** Define input parameters required to release a turn lock in a specific workspace thread */\nexport interface TurnLockReleaseInput {\n workspaceId: string\n threadId: string\n scope: TurnLockScope\n executionId: string\n lockId: string\n}\n\n/** Fenced out-of-band release (stop button, stale-lock reconciliation). The\n * fences make it refuse a SUCCESSOR lock: `interruptedAt` must not precede\n * the lock's own start, and when either side names a turn, both must name\n * the same one. */\nexport interface TurnLockInterruptedReleaseInput {\n workspaceId: string\n threadId: string\n /** Try only this scope; omit to try workspace then thread. */\n scope?: TurnLockScope\n interruptedAt: number\n turnId?: string\n}\n\n/** `stored` is what the DO read from storage; expired locks are dead. */\nexport function activeTurnLock(stored: DurableTurnLock | undefined, now: number): DurableTurnLock | null {\n if (!stored) return null\n // Locks written before the scope field existed default to thread scope.\n const lock = stored.scope ? stored : { ...stored, scope: 'thread' as const }\n return lock.expiresAt > now ? lock : null\n}\n\n/** Create a durable turn lock object with timing and scope based on input parameters */\nexport function createTurnLock(input: TurnLockAcquireInput, now: number, ttlMs = TURN_LOCK_TTL_MS): DurableTurnLock {\n return {\n workspaceId: input.workspaceId,\n threadId: input.threadId,\n scope: input.scope,\n executionId: input.executionId,\n lockId: input.lockId,\n startedAt: now,\n expiresAt: now + ttlMs,\n ...(input.turnId ? { turnId: input.turnId } : {}),\n }\n}\n\n/** A cooperative release matches the execution and, when supplied, lock ID. */\nexport function turnLockMatchesRelease(\n active: DurableTurnLock,\n input: { executionId: string; lockId?: string },\n): boolean {\n if (active.executionId !== input.executionId) return false\n if (input.lockId && active.lockId !== input.lockId) return false\n return true\n}\n\n/**\n * The interrupted/stale release fence. `interruptedAt` is the instant the\n * releasing evidence was observed (the stop click, the stale-lock probe) —\n * a lock STARTED after that instant is a successor the evidence says nothing\n * about, so it survives. When the lock recorded a client turnId, the release\n * must name the same turn; a lock without one refuses a turn-specific\n * release (it cannot prove it is that turn).\n */\nexport function interruptedReleaseApplies(\n active: DurableTurnLock,\n input: { threadId: string; interruptedAt: number; turnId?: string },\n): boolean {\n if (active.threadId !== input.threadId) return false\n if (active.startedAt > input.interruptedAt) return false\n if (active.turnId) {\n if (active.turnId !== input.turnId) return false\n } else if (input.turnId) {\n return false\n }\n return true\n}\n\n// ── wire contract (adapter ↔ DO endpoint paths) ─────────────────────────────\n//\n// Kept byte-identical to the reference consumer's DO where an endpoint\n// existed there, so a product deletes its fork by re-pointing a binding, not\n// by re-speaking a protocol.\n\n/** Provide constant paths for managing chat turn streams and locks */\nexport const TURN_STREAM_PATHS = {\n broadcast: '/broadcast',\n lockAcquire: '/chat-turn-lock/acquire',\n /** Read the active lock without changing it; also brings the object up before a turn needs it. */\n lockPeek: '/chat-turn-lock/peek',\n lockRelease: '/chat-turn-lock/release',\n lockReleaseInterrupted: '/chat-turn-lock/release-interrupted',\n turnEventsAppend: '/turn-events/append',\n turnEventsRead: '/turn-events/read',\n turnStatusSet: '/turn-status/set',\n turnStatusGet: '/turn-status/get',\n scopeStatusSet: '/turn-scope/set',\n scopeRunningList: '/turn-scope/running',\n} as const\n\n/** Storage keys inside a DO instance. Exported for subclass coexistence —\n * a product extending the DO must not collide with these. */\nexport const TURN_STREAM_STORAGE_KEYS = {\n lock: 'chatTurnLock',\n threadLockPrefix: 'chatTurnLock:thread:',\n activeThreads: 'activeThreads',\n turnStatus: 'turnStatus',\n turnScope: 'turnScopeIndex',\n turnEventPrefix: 'turnEvent:',\n} as const\n\n/** Zero-padded seq so DO storage `list({ prefix })` returns rows in replay\n * order without a sort. 10 digits holds any realistic turn. */\nexport function turnEventStorageKey(seq: number): string {\n return `${TURN_STREAM_STORAGE_KEYS.turnEventPrefix}${String(seq).padStart(10, '0')}`\n}\n","/**\n * Durable Object transport for thread/workspace locks, workspace signals,\n * detached turn-event rows, and running-turn discovery.\n * Cloudflare's named object identity binds every request's capability token.\n * Worker and DO code share TURN_STREAM_AUTH_SECRET; absent configuration fails closed.\n *\n * Sockets use hibernation attachments; products may add authenticated endpoints,\n * defer lock release during persistence, and contribute workspace sync events.\n * Product storage keys must avoid TURN_STREAM_STORAGE_KEYS.\n */\n\nimport { DEFAULT_RUNNING_TURN_LEASE_MS } from '../stream/turn-buffer'\n\nimport {\n ACTIVITY_TTL_MS,\n MAX_RECENT_CREATED,\n TURN_LOCK_TTL_MS,\n TURN_STREAM_PATHS,\n TURN_STREAM_STORAGE_KEYS,\n TURN_STREAM_TOKEN_HEADER,\n activeTurnLock,\n createTurnLock,\n interruptedReleaseApplies,\n pruneStaleThreads,\n turnEventStorageKey,\n turnLockMatchesRelease,\n turnLockStorageKey,\n verifyTurnStreamToken,\n type DurableTurnLock,\n type TurnLockAcquireResult,\n type TurnLockScope,\n type TurnStreamEvent,\n} from './core'\n\n// ── structural Cloudflare surface ───────────────────────────────────────────\n\n/** The socket surface the DO touches. Cloudflare's hibernatable `WebSocket`\n * satisfies it. */\nexport interface TurnStreamSocket {\n send(data: string): void\n close(code?: number, reason?: string): void\n serializeAttachment(value: unknown): void\n deserializeAttachment(): unknown\n}\n\n/** The storage surface the DO touches (Cloudflare `DurableObjectStorage`\n * satisfies it structurally). `list` must return keys in ascending order —\n * the turn-event rows rely on it for replay order. */\nexport interface TurnStreamStorage {\n get<T = unknown>(key: string): Promise<T | undefined>\n put<T = unknown>(key: string, value: T): Promise<void>\n delete(key: string): Promise<boolean | void>\n list<T = unknown>(options: { prefix: string; start?: string }): Promise<Map<string, T>>\n}\n\n/** The `DurableObjectState` surface the DO uses. */\nexport interface TurnStreamDOState {\n storage: TurnStreamStorage\n acceptWebSocket(ws: TurnStreamSocket): void\n getWebSockets(): TurnStreamSocket[]\n /** The instance's named identity — Cloudflare's `DurableObjectState.id`\n * satisfies this structurally (`state.id.name` for `idFromName` bindings).\n * Capability tokens are bound to this name, so it must be present for the\n * DO to serve any request. */\n id?: { name?: string | null }\n}\n\ninterface SocketMeta {\n sessionId: string\n scope: 'thread' | 'workspace'\n /** Live fan-out is withheld until the client has run its initial `sync`\n * (see {@link TurnStreamDO.webSocketMessage}). This makes replay and live\n * delivery mutually exclusive per socket, so frames can neither interleave\n * out of order nor drop in the connect race. */\n synced: boolean\n}\n\n// ── request/response plumbing ───────────────────────────────────────────────\n\nfunction jsonResponse(body: unknown, status = 200): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'Content-Type': 'application/json' },\n })\n}\n\nfunction stringValue(value: unknown): string | null {\n return typeof value === 'string' && value.trim() ? value.trim() : null\n}\n\nasync function jsonBody(request: Request): Promise<Record<string, unknown> | null> {\n const body = await request.json().catch(() => null)\n return body && typeof body === 'object' && !Array.isArray(body) ? (body as Record<string, unknown>) : null\n}\n\n/** The storage key a lock request names: a thread-scope request its thread's\n * lock, anything else (including a body without a scope) the workspace-scope\n * lock. */\nfunction lockStorageKeyOf(body: Record<string, unknown> | null): string {\n const threadId = stringValue(body?.threadId)\n return body?.scope === 'thread' && threadId ? turnLockStorageKey('thread', threadId) : TURN_STREAM_STORAGE_KEYS.lock\n}\n\n/** Per-scope running-turn index entry (one record per buffered turn). */\ninterface ScopeTurnEntry {\n status: 'running' | 'complete' | 'error'\n updatedAt: number\n}\n\n/** Cap on remembered turns per scope; oldest terminal entries are pruned\n * first, so a busy thread's index stays bounded without ever dropping a\n * still-running turn. */\nconst MAX_SCOPE_TURNS = 100\n\n// ── the DO ──────────────────────────────────────────────────────────────────\n\n/** Define options to override default TTL and event limits for TURN stream DO operations */\nexport interface TurnStreamDOOptions {\n /** Override {@link TURN_LOCK_TTL_MS}. */\n lockTtlMs?: number\n /** Override {@link ACTIVITY_TTL_MS}. */\n activityTtlMs?: number\n /** How long an unrenewed running turn remains discoverable. */\n runningTurnLeaseMs?: number\n /**\n * HMAC secret for the channel-bound capability tokens every request must\n * carry (issue #746). When omitted, the DO reads `TURN_STREAM_AUTH_SECRET`\n * from its `env`. Fail-closed: absent or shorter than 32 chars, every\n * request answers 500 with the fix in the message — there is deliberately\n * no unauthenticated mode.\n */\n authSecret?: string\n}\n\n/** Durable turn-event storage, workspace signals, and the single-flight turn\n * lock, on one channel-bound Durable Object. Every request — including\n * subclass product routes — passes the capability-token gate in\n * {@link TurnStreamDO.fetch} (issue #746). */\nexport class TurnStreamDO {\n protected readonly state: TurnStreamDOState\n protected readonly env: unknown\n protected readonly options: TurnStreamDOOptions\n\n // Workspace channel: recent thread.created markers (in-memory, best-effort)\n // + durable responding set (threadId → startedAt) that survives eviction.\n private recentCreated: TurnStreamEvent[] = []\n private activeThreads: Map<string, number> | null = null\n\n constructor(state: TurnStreamDOState, env?: unknown, options: TurnStreamDOOptions = {}) {\n this.state = state\n this.env = env\n this.options = options\n }\n\n /** The token-verification secret: explicit option first, then the\n * `TURN_STREAM_AUTH_SECRET` env binding. Protected so a product subclass\n * with its own secret store overrides ONE method instead of copying the\n * resolution. */\n protected resolveAuthSecret(): string | undefined {\n const explicit = this.options.authSecret\n if (explicit) return explicit\n const fromEnv = (this.env as { TURN_STREAM_AUTH_SECRET?: unknown } | undefined)?.TURN_STREAM_AUTH_SECRET\n return typeof fromEnv === 'string' && fromEnv ? fromEnv : undefined\n }\n\n /**\n * The single authentication gate for EVERYTHING this DO serves — base\n * endpoints, the WebSocket upgrade, and subclass product routes (they are\n * dispatched from {@link fetch} after this check). A request must present a\n * capability token minted for THIS instance's channel name: cross-channel\n * replay is impossible, an unauthenticated forward gets 401, and a missing\n * secret fails closed with a 500 that names the fix (issue #746).\n */\n private async authorizeRequest(request: Request): Promise<Response | null> {\n const name = this.state.id?.name\n if (typeof name !== 'string' || !name) {\n return Response.json(\n { error: 'turn-stream DO has no channel name; bind with idFromName so capability tokens can be bound to it' },\n { status: 500 },\n )\n }\n const secret = this.resolveAuthSecret()\n if (!secret || secret.length < 32) {\n return Response.json(\n { error: 'turn-stream DO requires authSecret (option or TURN_STREAM_AUTH_SECRET, >= 32 chars); refusing unauthenticated mode' },\n { status: 500 },\n )\n }\n const token = request.headers.get(TURN_STREAM_TOKEN_HEADER) ?? ''\n if (await verifyTurnStreamToken(name, token, secret)) return null\n return Response.json({ error: 'invalid or missing turn-stream capability token' }, { status: 401 })\n }\n\n async fetch(request: Request): Promise<Response> {\n const url = new URL(request.url)\n\n const unauthorized = await this.authorizeRequest(request)\n if (unauthorized) return unauthorized\n\n if (request.headers.get('Upgrade')?.toLowerCase() === 'websocket') {\n return this.handleWebSocketUpgrade(request, url)\n }\n\n if (request.method === 'POST') {\n switch (url.pathname) {\n case TURN_STREAM_PATHS.broadcast:\n return this.handleBroadcast(request)\n case TURN_STREAM_PATHS.lockAcquire:\n return this.handleLockAcquire(request)\n case TURN_STREAM_PATHS.lockPeek:\n return jsonResponse({ active: await this.loadActiveLock(Date.now(), lockStorageKeyOf(await jsonBody(request))) })\n case TURN_STREAM_PATHS.lockRelease:\n return this.handleLockRelease(request)\n case TURN_STREAM_PATHS.lockReleaseInterrupted:\n return this.handleLockReleaseInterrupted(request)\n case TURN_STREAM_PATHS.turnEventsAppend:\n return this.handleTurnEventsAppend(request)\n case TURN_STREAM_PATHS.turnEventsRead:\n return this.handleTurnEventsRead(request)\n case TURN_STREAM_PATHS.turnStatusSet:\n return this.handleTurnStatusSet(request)\n case TURN_STREAM_PATHS.turnStatusGet:\n return this.handleTurnStatusGet()\n case TURN_STREAM_PATHS.scopeStatusSet:\n return this.handleScopeStatusSet(request)\n case TURN_STREAM_PATHS.scopeRunningList:\n return this.handleScopeRunningList()\n }\n }\n\n const product = await this.handleProductRequest(request, url)\n if (product) return product\n return request.method === 'POST'\n ? jsonResponse({ error: `Unknown turn-stream endpoint ${url.pathname}` }, 404)\n : new Response('Method not allowed', { status: 405 })\n }\n\n // ── product extension seam ────────────────────────────────────────────────\n\n /** Called for any request no base endpoint claimed (before the 404), so a\n * subclass adds product endpoints without touching base routing. Return\n * `null` to decline. */\n protected async handleProductRequest(_request: Request, _url: URL): Promise<Response | null> {\n return null\n }\n\n /** Consulted before any lock release (cooperative or interrupted). Return `true` while a product-owned\n * post-turn task for `executionId` must keep the scope serialized (e.g.\n * file persistence still reading the box) — the release is then parked as\n * `releasePending` on the lock and completed via\n * {@link completeDeferredLockRelease}. Base: never defer. */\n protected async shouldDeferLockRelease(_executionId: string): Promise<boolean> {\n return false\n }\n\n /** Complete a release parked by {@link shouldDeferLockRelease}. A subclass\n * calls this when its deferred condition settles. */\n protected async completeDeferredLockRelease(executionId: string): Promise<boolean> {\n const threadLocks = await this.state.storage.list<DurableTurnLock>({ prefix: TURN_STREAM_STORAGE_KEYS.threadLockPrefix })\n for (const key of [TURN_STREAM_STORAGE_KEYS.lock, ...threadLocks.keys()]) {\n const active = await this.loadActiveLock(Date.now(), key)\n if (!active?.releasePending || active.executionId !== executionId) continue\n await this.state.storage.delete(key)\n return true\n }\n return false\n }\n\n /** Extra product state replayed to a socket during its `sync`, after the\n * base replay for its scope (e.g. an in-flight persistence status card).\n * Base: none. */\n protected async productSyncEvents(_scope: 'thread' | 'workspace', _meta: { sessionId: string }): Promise<TurnStreamEvent[]> {\n return []\n }\n\n // ── WebSocket channel ─────────────────────────────────────────────────────\n\n private handleWebSocketUpgrade(_request: Request, url: URL): Response {\n const sessionId = url.searchParams.get('sessionId')\n if (!sessionId) return new Response('Missing sessionId', { status: 400 })\n const scope = url.searchParams.get('scope') === 'workspace' ? 'workspace' : 'thread'\n\n const PairCtor = (globalThis as { WebSocketPair?: new () => Record<number, unknown> }).WebSocketPair\n if (!PairCtor) {\n return new Response('WebSocket upgrades require the Cloudflare runtime', { status: 501 })\n }\n const pair = new PairCtor()\n const client = pair[0]\n const server = pair[1] as TurnStreamSocket\n\n // Accept for hibernation and record routing metadata. No replay is pushed\n // here: sending before the client finishes the 101 handshake is racy\n // (frames can drop). The client issues a `sync` once open; replay happens\n // there.\n this.state.acceptWebSocket(server)\n server.serializeAttachment({ sessionId, scope, synced: false } satisfies SocketMeta)\n\n return new Response(null, { status: 101, webSocket: client } as ResponseInit)\n }\n\n /**\n * First (and only) client message after open: `{ type: 'sync', afterSeq }`.\n * Replays the current state for the socket's scope, then marks it `synced`\n * so live broadcasts start flowing. Because the DO is single-threaded, the\n * replay snapshot and the synced flip are atomic w.r.t. broadcasts — every\n * event reaches the socket exactly once, in order, via replay XOR live\n * fan-out.\n */\n async webSocketMessage(ws: TurnStreamSocket, message: string | ArrayBuffer): Promise<void> {\n const meta = ws.deserializeAttachment() as SocketMeta | null\n if (!meta || meta.synced) return\n try {\n const parsed = JSON.parse(typeof message === 'string' ? message : '') as { type?: string }\n if (parsed.type !== 'sync') return\n } catch {\n return\n }\n\n {\n // Replay the current responding state and recent thread-created markers.\n const active = await this.loadActiveThreads()\n const removed = pruneStaleThreads(active, Date.now(), this.options.activityTtlMs ?? ACTIVITY_TTL_MS)\n if (removed.length > 0) await this.persistActiveThreads(active)\n for (const [threadId, startedAt] of active) {\n this.trySend(ws, {\n type: 'thread.activity',\n data: { threadId, phase: 'start', sessionId: meta.sessionId },\n timestamp: startedAt,\n })\n }\n for (const event of this.recentCreated) this.trySend(ws, event)\n }\n for (const event of await this.productSyncEvents(meta.scope, { sessionId: meta.sessionId })) {\n this.trySend(ws, event)\n }\n\n ws.serializeAttachment({ ...meta, synced: true } satisfies SocketMeta)\n }\n\n webSocketClose(ws: TurnStreamSocket, code: number, reason: string): void {\n try {\n ws.close(code, reason)\n } catch {\n // Already closed.\n }\n }\n\n webSocketError(): void {\n // Socket dropped; getWebSockets() will no longer include it.\n }\n\n private trySend(ws: TurnStreamSocket, event: TurnStreamEvent): void {\n try {\n ws.send(JSON.stringify(event))\n } catch {\n // Client may not be ready yet, or is closing.\n }\n }\n\n // ── broadcast (live fanout + activity) ─────────────────────────────────────\n\n private async handleBroadcast(request: Request): Promise<Response> {\n const incoming = (await request.json()) as TurnStreamEvent\n const data = (incoming.data ?? {}) as Record<string, unknown>\n const sessionId = typeof data.sessionId === 'string' ? data.sessionId : undefined\n\n if (incoming.type === 'thread.activity') {\n const threadId = typeof data.threadId === 'string' ? data.threadId : undefined\n if (threadId) {\n const active = await this.loadActiveThreads()\n if (data.phase === 'end') active.delete(threadId)\n else active.set(threadId, Date.now())\n await this.persistActiveThreads(active)\n }\n } else if (incoming.type === 'thread.created') {\n this.recentCreated.push(incoming)\n if (this.recentCreated.length > MAX_RECENT_CREATED) {\n this.recentCreated = this.recentCreated.slice(-MAX_RECENT_CREATED)\n }\n }\n // (The per-turn rebroadcast lane that used to live here — segment\n // buffering + terminal-event lock auto-release — was REMOVED with the\n // deprecated segment store. Lock release is owned by the cooperative\n // release and the stale-lock reconciler, which are the paths that\n // actually free a wedged lane.)\n\n // Fan out to live sockets for this session. Enumerate accepted sockets so\n // fan-out is correct after WebSocket hibernation (no in-memory socket map).\n // Only `synced` sockets receive live frames; an un-synced socket is still\n // mid-handshake and will pick this event up in its `sync` replay snapshot.\n const message = JSON.stringify(incoming)\n for (const ws of this.state.getWebSockets()) {\n const meta = ws.deserializeAttachment() as SocketMeta | null\n if (!meta?.synced) continue\n if (!sessionId || meta.sessionId === sessionId) {\n try {\n ws.send(message)\n } catch {\n // Dead/closing socket — getWebSockets() will stop returning it.\n }\n }\n }\n\n return new Response('OK', { status: 200 })\n }\n\n private async loadActiveThreads(): Promise<Map<string, number>> {\n if (this.activeThreads === null) {\n const stored = await this.state.storage.get<Record<string, number>>(TURN_STREAM_STORAGE_KEYS.activeThreads)\n this.activeThreads = new Map(Object.entries(stored ?? {}))\n }\n return this.activeThreads\n }\n\n private async persistActiveThreads(map: Map<string, number>): Promise<void> {\n await this.state.storage.put(TURN_STREAM_STORAGE_KEYS.activeThreads, Object.fromEntries(map))\n }\n\n // ── chat-turn lock ────────────────────────────────────────────────────────\n\n /** The live lock under `key` (default: the workspace-scope lock); an\n * expired one is deleted and reads as none. */\n protected async loadActiveLock(now = Date.now(), key: string = TURN_STREAM_STORAGE_KEYS.lock): Promise<DurableTurnLock | null> {\n const stored = await this.state.storage.get<DurableTurnLock>(key)\n const lock = activeTurnLock(stored, now)\n if (stored && !lock) await this.state.storage.delete(key)\n return lock\n }\n\n private async releaseActiveLock(key: string, input: { executionId: string; lockId?: string }): Promise<boolean> {\n const active = await this.loadActiveLock(Date.now(), key)\n if (!active || !turnLockMatchesRelease(active, input)) return false\n await this.state.storage.delete(key)\n return true\n }\n\n /** Park a release on the lock itself; {@link completeDeferredLockRelease}\n * finishes it once the product's deferred condition settles. */\n private async deferLockRelease(key: string, input: { executionId: string; lockId?: string }): Promise<boolean> {\n const active = await this.loadActiveLock(Date.now(), key)\n if (!active || !turnLockMatchesRelease(active, input)) return false\n if (active.releasePending) return true\n await this.state.storage.put(key, { ...active, releasePending: true })\n return true\n }\n\n /** Delete expired thread locks, so a thread whose release never arrived\n * leaves no row behind once its lock's TTL passes. */\n private async pruneExpiredThreadLocks(now: number): Promise<void> {\n const threadLocks = await this.state.storage.list<DurableTurnLock>({ prefix: TURN_STREAM_STORAGE_KEYS.threadLockPrefix })\n for (const [key, stored] of threadLocks) {\n if (!activeTurnLock(stored, now)) await this.state.storage.delete(key)\n }\n }\n\n private async handleLockAcquire(request: Request): Promise<Response> {\n const body = await jsonBody(request)\n const workspaceId = stringValue(body?.workspaceId)\n const threadId = stringValue(body?.threadId)\n const executionId = stringValue(body?.executionId)\n const lockId = stringValue(body?.lockId)\n const scope: TurnLockScope | null =\n body?.scope === 'workspace' ? 'workspace' : body?.scope === 'thread' ? 'thread' : null\n const turnId = stringValue(body?.turnId) ?? undefined\n if (!workspaceId || !threadId || !executionId || !lockId || !scope) {\n return jsonResponse({ error: 'Missing workspaceId, threadId, executionId, lockId, or scope' }, 400)\n }\n\n const key = turnLockStorageKey(scope, threadId)\n const now = Date.now()\n const active = await this.loadActiveLock(now, key)\n if (active) {\n return jsonResponse({ acquired: false, active } satisfies TurnLockAcquireResult, 409)\n }\n if (scope === 'thread') await this.pruneExpiredThreadLocks(now)\n\n const lock = createTurnLock(\n { workspaceId, threadId, scope, executionId, lockId, ...(turnId ? { turnId } : {}) },\n now,\n this.options.lockTtlMs ?? TURN_LOCK_TTL_MS,\n )\n await this.state.storage.put(key, lock)\n return jsonResponse({ acquired: true, lock } satisfies TurnLockAcquireResult)\n }\n\n private async handleLockRelease(request: Request): Promise<Response> {\n const body = await jsonBody(request)\n const executionId = stringValue(body?.executionId)\n const lockId = stringValue(body?.lockId)\n if (!executionId || !lockId) {\n return jsonResponse({ error: 'Missing executionId or lockId' }, 400)\n }\n\n const key = lockStorageKeyOf(body)\n if (await this.shouldDeferLockRelease(executionId)) {\n const deferred = await this.deferLockRelease(key, { executionId, lockId })\n return jsonResponse({ released: false, deferred })\n }\n const released = await this.releaseActiveLock(key, { executionId, lockId })\n return jsonResponse({ released })\n }\n\n private async handleLockReleaseInterrupted(request: Request): Promise<Response> {\n const body = await jsonBody(request)\n const threadId = stringValue(body?.threadId)\n const interruptedAt =\n typeof body?.interruptedAt === 'number' && Number.isFinite(body.interruptedAt) ? body.interruptedAt : undefined\n if (!threadId || interruptedAt === undefined) {\n return jsonResponse({ error: 'Missing threadId or interruptedAt' }, 400)\n }\n const turnId = stringValue(body?.turnId) ?? undefined\n const key = lockStorageKeyOf(body)\n\n const active = await this.loadActiveLock(Date.now(), key)\n if (!active) return jsonResponse({ released: false })\n // A deferred product task holds the scope even against an out-of-band\n // release — the box is still being read.\n if (await this.shouldDeferLockRelease(active.executionId)) {\n return jsonResponse({ released: false })\n }\n if (!interruptedReleaseApplies(active, { threadId, interruptedAt, ...(turnId ? { turnId } : {}) })) {\n return jsonResponse({ released: false })\n }\n await this.state.storage.delete(key)\n return jsonResponse({ released: true })\n }\n\n // ── durable turn-event storage (TurnEventStore backing) ───────────────────\n\n private async handleTurnEventsAppend(request: Request): Promise<Response> {\n const body = await jsonBody(request)\n const events = Array.isArray(body?.events) ? body.events : null\n if (!events) return jsonResponse({ error: 'Missing events' }, 400)\n for (const row of events) {\n const seq = (row as { seq?: unknown }).seq\n const event = (row as { event?: unknown }).event\n if (typeof seq !== 'number' || !Number.isInteger(seq) || seq < 1 || typeof event !== 'string') {\n return jsonResponse({ error: 'Invalid turn-event row' }, 400)\n }\n }\n const status = await this.state.storage.get<string>(TURN_STREAM_STORAGE_KEYS.turnStatus)\n if (status === 'complete' || status === 'error') return jsonResponse({ appended: 0 })\n for (const row of events as Array<{ seq: number; event: string }>) {\n await this.state.storage.put(turnEventStorageKey(row.seq), row.event)\n }\n return jsonResponse({ appended: events.length })\n }\n\n private async handleTurnEventsRead(request: Request): Promise<Response> {\n const body = await jsonBody(request)\n const fromSeq = typeof body?.fromSeq === 'number' && Number.isFinite(body.fromSeq) ? Math.trunc(body.fromSeq) : 0\n const rows = await this.state.storage.list<string>({\n prefix: TURN_STREAM_STORAGE_KEYS.turnEventPrefix,\n start: turnEventStorageKey(fromSeq + 1),\n })\n const events: Array<{ seq: number; event: string }> = []\n for (const [key, event] of rows) {\n const seq = Number(key.slice(TURN_STREAM_STORAGE_KEYS.turnEventPrefix.length))\n if (Number.isFinite(seq) && seq > fromSeq) events.push({ seq, event })\n }\n return jsonResponse({ events })\n }\n\n private async handleTurnStatusSet(request: Request): Promise<Response> {\n const body = await jsonBody(request)\n const status = body?.status\n if (status !== 'running' && status !== 'complete' && status !== 'error') {\n return jsonResponse({ error: 'Invalid status' }, 400)\n }\n await this.state.storage.put(TURN_STREAM_STORAGE_KEYS.turnStatus, status)\n return jsonResponse({ ok: true })\n }\n\n private async handleTurnStatusGet(): Promise<Response> {\n const status = await this.state.storage.get<string>(TURN_STREAM_STORAGE_KEYS.turnStatus)\n return jsonResponse({ status: status ?? null })\n }\n\n // ── scope index (listRunning reconnect discovery) ─────────────────────────\n\n private async handleScopeStatusSet(request: Request): Promise<Response> {\n const body = await jsonBody(request)\n const turnId = stringValue(body?.turnId)\n const status = body?.status\n if (!turnId || (status !== 'running' && status !== 'complete' && status !== 'error')) {\n return jsonResponse({ error: 'Invalid scope-status request' }, 400)\n }\n const index =\n (await this.state.storage.get<Record<string, ScopeTurnEntry>>(TURN_STREAM_STORAGE_KEYS.turnScope)) ?? {}\n index[turnId] = { status, updatedAt: Date.now() }\n const entries = Object.entries(index)\n if (entries.length > MAX_SCOPE_TURNS) {\n // Prune oldest terminal entries first; a running turn is never dropped.\n const removable = entries\n .filter(([, entry]) => entry.status !== 'running')\n .sort((a, b) => a[1].updatedAt - b[1].updatedAt)\n for (const [id] of removable.slice(0, entries.length - MAX_SCOPE_TURNS)) {\n delete index[id]\n }\n }\n await this.state.storage.put(TURN_STREAM_STORAGE_KEYS.turnScope, index)\n return jsonResponse({ ok: true })\n }\n\n private async handleScopeRunningList(): Promise<Response> {\n const index =\n (await this.state.storage.get<Record<string, ScopeTurnEntry>>(TURN_STREAM_STORAGE_KEYS.turnScope)) ?? {}\n const cutoff = Date.now() - Math.max(\n 1,\n this.options.runningTurnLeaseMs ?? DEFAULT_RUNNING_TURN_LEASE_MS,\n )\n const running = Object.entries(index)\n .filter(([, entry]) => entry.status === 'running' && entry.updatedAt >= cutoff)\n .sort((a, b) => b[1].updatedAt - a[1].updatedAt)\n .map(([turnId]) => turnId)\n return jsonResponse({ running })\n }\n}\n","/**\n * Worker adapters for durable turn storage, single-flight locks, and workspace signals.\n * Namespace interfaces remain structural so Cloudflare bindings and the memory harness\n * run the same authenticated request path.\n * Products provide workspace authorization and their binding's TURN_STREAM_AUTH_SECRET.\n */\n\nimport { reconcileStaleTurnLock, type ReconcileStaleTurnLockOptions } from '../chat-routes/stale-turn-lock'\nimport type { TurnEventStore, TurnStatus } from '../stream/turn-buffer'\nimport {\n TURN_STREAM_PATHS,\n TURN_STREAM_TOKEN_HEADER,\n mintTurnStreamToken,\n scopeIndexChannelKey,\n turnLockChannelKey,\n turnStorageChannelKey,\n workspaceChannelKey,\n type DurableTurnLock,\n type TurnLockAcquireResult,\n type TurnLockReleaseInput,\n type TurnLockScope,\n type TurnStreamEvent,\n} from './core'\n\n// ── structural namespace ────────────────────────────────────────────────────\n\n/** Resolve a stub interface for handling fetch requests with optional initialization parameters */\nexport interface TurnStreamStubLike {\n fetch(input: Request | string, init?: RequestInit): Promise<Response>\n}\n\n/** The surface of `DurableObjectNamespace` the adapters use. */\nexport interface TurnStreamNamespaceLike {\n idFromName(name: string): unknown\n get(id: unknown): TurnStreamStubLike\n}\n\nconst INTERNAL_ORIGIN = 'https://turn-stream.internal'\n\n/** The HMAC secret shared between this worker and its `TurnStreamDO` binding.\n * Every adapter call mints a channel-bound capability token from it; the DO\n * refuses requests without one (issue #746). Products pass the same value\n * the DO resolves — `env.TURN_STREAM_AUTH_SECRET`. */\nexport interface TurnStreamAuth {\n secret: string\n}\n\nasync function postJson<T>(\n namespace: TurnStreamNamespaceLike,\n channelKey: string,\n path: string,\n body: unknown,\n auth: TurnStreamAuth,\n): Promise<{ status: number; body: T }> {\n const stub = namespace.get(namespace.idFromName(channelKey))\n const response = await stub.fetch(`${INTERNAL_ORIGIN}${path}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n [TURN_STREAM_TOKEN_HEADER]: await mintTurnStreamToken(channelKey, auth.secret),\n },\n body: JSON.stringify(body),\n })\n return { status: response.status, body: (await response.json()) as T }\n}\n\nasync function postJsonOk<T>(\n namespace: TurnStreamNamespaceLike,\n channelKey: string,\n path: string,\n body: unknown,\n auth: TurnStreamAuth,\n): Promise<T> {\n const result = await postJson<T>(namespace, channelKey, path, body, auth)\n if (result.status !== 200) {\n throw new Error(`turn-stream ${path} failed with status ${result.status}`)\n }\n return result.body\n}\n\n// ── TurnEventStore adapter (the real turnStore) ─────────────────────────────\n\n/**\n * Optional Durable Object backend for {@link TurnEventStore}.\n * Each turn has its own named instance; a per-scope index tracks running turns.\n * Products choose this adapter or the D1 backend independently of their lock\n * and workspace-signal transport.\n */\nexport function createDurableObjectTurnEventStore(\n namespace: TurnStreamNamespaceLike,\n auth: TurnStreamAuth,\n): TurnEventStore {\n return {\n async append(turnId, events) {\n if (!events.length) return\n await postJsonOk(namespace, turnStorageChannelKey(turnId), TURN_STREAM_PATHS.turnEventsAppend, { events }, auth)\n },\n async read(turnId, fromSeq) {\n const body = await postJsonOk<{ events: Array<{ seq: number; event: string }> }>(\n namespace,\n turnStorageChannelKey(turnId),\n TURN_STREAM_PATHS.turnEventsRead,\n { fromSeq },\n auth,\n )\n return body.events\n },\n async setStatus(turnId, status, scopeId, options) {\n // This store keeps no update time to preserve or compare; refuse rather than write unguarded.\n if (options?.preserveUpdatedAt || options?.onlyIfRunningBefore !== undefined) {\n throw new Error('The turn-stream Durable Object store does not support guarded or time-preserving status writes')\n }\n await postJsonOk(namespace, turnStorageChannelKey(turnId), TURN_STREAM_PATHS.turnStatusSet, { status }, auth)\n if (scopeId) {\n await postJsonOk(namespace, scopeIndexChannelKey(scopeId), TURN_STREAM_PATHS.scopeStatusSet, {\n turnId,\n status,\n }, auth)\n }\n },\n async getStatus(turnId) {\n const body = await postJsonOk<{ status: TurnStatus | null }>(\n namespace,\n turnStorageChannelKey(turnId),\n TURN_STREAM_PATHS.turnStatusGet,\n {},\n auth,\n )\n return body.status\n },\n async listRunning(scopeId) {\n const body = await postJsonOk<{ running: string[] }>(\n namespace,\n scopeIndexChannelKey(scopeId),\n TURN_STREAM_PATHS.scopeRunningList,\n {},\n auth,\n )\n return body.running\n },\n }\n}\n\n// ── lock primitives (usable outside the route seam too) ─────────────────────\n\n/** Define input parameters required to acquire a durable turn lock in a workspace thread context */\nexport interface AcquireDurableTurnLockInput {\n workspaceId: string\n threadId: string\n scope: TurnLockScope\n executionId: string\n turnId?: string\n /** Supply to reclaim/retry with a stable id; default mints a UUID. */\n lockId?: string\n}\n\n/** Acquire a durable turn lock in the specified namespace with given input parameters */\nexport async function acquireDurableTurnLock(\n namespace: TurnStreamNamespaceLike,\n input: AcquireDurableTurnLockInput,\n auth: TurnStreamAuth,\n): Promise<TurnLockAcquireResult> {\n const lockId = input.lockId ?? crypto.randomUUID()\n const key = turnLockChannelKey(input.workspaceId, input.threadId, input.scope)\n const result = await postJson<TurnLockAcquireResult>(namespace, key, TURN_STREAM_PATHS.lockAcquire, {\n ...input,\n lockId,\n }, auth)\n if (result.status !== 200 && result.status !== 409) {\n throw new Error(`turn-stream lock acquire failed with status ${result.status}`)\n }\n return result.body\n}\n\n/**\n * Read a channel's active turn lock without changing it.\n *\n * The first request to a Durable Object that has never been used creates it,\n * which measured 1.3-2.4 s for a thread's first turn in GTM production\n * (2026-10-10) against 0.05-0.33 s once it exists; every lock now lives on the\n * workspace channel, which exists after the workspace's first turn. A peek\n * sent as soon as a turn is admitted wakes that object if it was evicted.\n */\nexport async function peekDurableTurnLock(\n namespace: TurnStreamNamespaceLike,\n input: { workspaceId: string; threadId: string; scope: TurnLockScope },\n auth: TurnStreamAuth,\n): Promise<DurableTurnLock | null> {\n const key = turnLockChannelKey(input.workspaceId, input.threadId, input.scope)\n const result = await postJson<{ active?: DurableTurnLock | null }>(namespace, key, TURN_STREAM_PATHS.lockPeek, {\n scope: input.scope,\n threadId: input.threadId,\n }, auth)\n if (result.status !== 200) throw new Error(`turn-stream lock peek failed with status ${result.status}`)\n return result.body.active ?? null\n}\n\n/** Release a durable turn lock and indicate if the release was successful or deferred */\nexport async function releaseDurableTurnLock(\n namespace: TurnStreamNamespaceLike,\n input: TurnLockReleaseInput,\n auth: TurnStreamAuth,\n): Promise<{ released: boolean; deferred?: boolean }> {\n const key = turnLockChannelKey(input.workspaceId, input.threadId, input.scope)\n return postJsonOk(namespace, key, TURN_STREAM_PATHS.lockRelease, input, auth)\n}\n\n/** Define input parameters to release an interrupted durable turn lock in a workspace or thread */\nexport interface ReleaseInterruptedDurableTurnLockInput {\n workspaceId: string\n threadId: string\n /** Try only this scope; omit to try workspace then thread (a stop button\n * doesn't know which lane the wedged turn ran on). */\n scope?: TurnLockScope\n interruptedAt: number\n turnId?: string\n}\n\n/** Fenced out-of-band release — the DO refuses a successor lock (started\n * after `interruptedAt`) and a turnId mismatch. Returns whether any scope\n * released. */\nexport async function releaseInterruptedDurableTurnLock(\n namespace: TurnStreamNamespaceLike,\n input: ReleaseInterruptedDurableTurnLockInput,\n auth: TurnStreamAuth,\n): Promise<boolean> {\n const scopes: readonly TurnLockScope[] = input.scope ? [input.scope] : ['workspace', 'thread']\n for (const scope of scopes) {\n const key = turnLockChannelKey(input.workspaceId, input.threadId, scope)\n const result = await postJsonOk<{ released: boolean }>(namespace, key, TURN_STREAM_PATHS.lockReleaseInterrupted, {\n scope,\n threadId: input.threadId,\n interruptedAt: input.interruptedAt,\n ...(input.turnId ? { turnId: input.turnId } : {}),\n }, auth)\n if (result.released) return true\n }\n return false\n}\n\n// ── stale-lock reconciliation against the DO ────────────────────────────────\n\n/** Define options to reconcile stale durable turn locks with context, namespace, workspace, and active lock details */\nexport interface ReconcileStaleDurableTurnLockOptions\n extends Pick<\n ReconcileStaleTurnLockOptions,\n 'probeSandbox' | 'probeSession' | 'graceMs' | 'terminalGraceMs' | 'context' | 'log' | 'now'\n > {\n namespace: TurnStreamNamespaceLike\n auth: TurnStreamAuth\n workspaceId: string\n threadId: string\n /** The lock the acquire attempt was refused on. */\n active: DurableTurnLock\n}\n\n/**\n * `/chat-routes`' `reconcileStaleTurnLock` policy wired to the DO: the\n * product supplies the probes (which box, what its session says), the policy\n * decides, and a release lands as a FENCED interrupted release —\n * `interruptedAt` is the policy's `fence.observedAt`, so a successor lock\n * acquired while the probes were in flight survives by construction.\n */\nexport async function reconcileStaleDurableTurnLock(\n options: ReconcileStaleDurableTurnLockOptions,\n): Promise<{ released: boolean; diagnostics: Record<string, unknown> }> {\n const { namespace, auth, workspaceId, threadId, active, ...policy } = options\n return reconcileStaleTurnLock({\n ...policy,\n lockStartedAt: active.startedAt,\n release: (fence) =>\n releaseInterruptedDurableTurnLock(namespace, {\n workspaceId,\n threadId: active.threadId,\n scope: active.scope,\n interruptedAt: fence.observedAt,\n ...(active.turnId ? { turnId: active.turnId } : {}),\n }, auth),\n })\n}\n\n// ── ChatTurnLock adapter (the turnLock seam) ────────────────────────────────\n\n/** The subset of `ChatTurnProduceArgs` the lock adapter reads — structural,\n * so this module needs no import from `/chat-routes/turn-routes` (which\n * would drag the `agent-runtime` peer into every `/turn-stream` consumer). */\nexport interface TurnLockSeamArgs<TContext> {\n identity: { tenantId: string; sessionId: string }\n executionId: string\n context: TContext\n body: { turnId?: string }\n}\n\n/** Verdict shape of the vertical's `turnLock.acquire`. */\nexport type TurnLockSeamResult =\n | { acquired: true; handle?: unknown }\n | { acquired: false; response: Response }\n\n/** Define options for creating a durable turn lock with customizable scope and identification methods */\nexport interface CreateDurableTurnLockOptions<TContext> {\n namespace: TurnStreamNamespaceLike\n /** Capability tokens for the DO; pass the worker's\n * `env.TURN_STREAM_AUTH_SECRET`. The DO validates every request. */\n auth: TurnStreamAuth\n /** Which lane serializes this turn: `'workspace'` (shared sandbox — one\n * turn per workspace) or `'thread'` (router lane — one turn per thread). */\n scopeOf(args: TurnLockSeamArgs<TContext>): TurnLockScope\n /** Override the execution id the lock records (e.g. a follow-up reclaiming\n * the execution it already dispatched). Default: the turn's own. */\n lockExecutionIdOf?(args: TurnLockSeamArgs<TContext>): string | undefined\n /** Client turn id recorded on the lock (fences interrupted releases to the\n * right turn). Default: the request body's `turnId`. */\n clientTurnIdOf?(args: TurnLockSeamArgs<TContext>): string | undefined\n /**\n * Attempt stale-lock recovery after a refused acquire. Return whether the\n * held lock was released (the adapter then retries the acquire once).\n * Products wire {@link reconcileStaleDurableTurnLock} with their sandbox +\n * session probes here. Omit → a refused acquire is final.\n */\n reconcile?(\n args: TurnLockSeamArgs<TContext>,\n active: DurableTurnLock,\n ): Promise<{ released: boolean; diagnostics?: Record<string, unknown> }>\n /** Build the refusal `Response`. Default: a 409 with the shared body shape\n * (`code`, `message`, lock identity + age, reconcile diagnostics). */\n onRefused?(active: DurableTurnLock, diagnostics: Record<string, unknown> | undefined): Response\n}\n\nfunction defaultRefusalResponse(\n active: DurableTurnLock,\n diagnostics: Record<string, unknown> | undefined,\n): Response {\n const workspaceConflict = active.scope === 'workspace'\n return Response.json(\n {\n error: {\n code: workspaceConflict ? 'workspace_turn_in_flight' : 'chat_turn_in_flight',\n message: workspaceConflict\n ? 'A chat turn is already running for this workspace. Wait for it to finish before starting another.'\n : 'A chat turn is already running for this thread. Reconnect to the active response or wait for it to finish.',\n threadId: active.threadId,\n executionId: active.executionId,\n ...(active.turnId ? { turnId: active.turnId } : {}),\n lockStartedAt: active.startedAt,\n lockAgeMs: Date.now() - active.startedAt,\n ...(diagnostics ? { diagnostics } : {}),\n },\n },\n { status: 409 },\n )\n}\n\n/**\n * The vertical's `turnLock` seam on the shared DO: dual-scope single-flight\n * acquire (with one reconcile-then-retry pass when the product supplies a\n * stale-lock reconciler) and cooperative release on settle. The returned\n * object satisfies `createChatTurnRoutes`' `ChatTurnLock<TContext>`\n * structurally.\n */\nexport function createDurableTurnLock<TContext>(options: CreateDurableTurnLockOptions<TContext>): {\n acquire(args: TurnLockSeamArgs<TContext>): Promise<TurnLockSeamResult>\n release(handle: unknown): Promise<void>\n warm(target: { tenantId: string; threadId: string }): Promise<void>\n} {\n return {\n // Every lock lives on the workspace channel, so this wakes that object\n // (evicted after idle) before the turn's acquire reaches it.\n async warm(target) {\n await peekDurableTurnLock(options.namespace, { workspaceId: target.tenantId, threadId: target.threadId, scope: 'thread' }, options.auth)\n },\n async acquire(args) {\n const workspaceId = args.identity.tenantId\n const threadId = args.identity.sessionId\n const scope = options.scopeOf(args)\n const executionId = options.lockExecutionIdOf?.(args) ?? args.executionId\n const turnId = options.clientTurnIdOf ? options.clientTurnIdOf(args) : args.body.turnId\n const input: AcquireDurableTurnLockInput = {\n workspaceId,\n threadId,\n scope,\n executionId,\n ...(turnId ? { turnId } : {}),\n }\n\n let acquired = await acquireDurableTurnLock(options.namespace, input, options.auth)\n let diagnostics: Record<string, unknown> | undefined\n if (!acquired.acquired && options.reconcile) {\n const reconciled = await options.reconcile(args, acquired.active)\n diagnostics = reconciled.diagnostics\n if (reconciled.released) {\n acquired = await acquireDurableTurnLock(options.namespace, input, options.auth)\n }\n }\n if (!acquired.acquired) {\n return {\n acquired: false,\n response: (options.onRefused ?? defaultRefusalResponse)(acquired.active, diagnostics),\n }\n }\n const handle: TurnLockReleaseInput = {\n workspaceId,\n threadId,\n scope,\n executionId,\n lockId: acquired.lock.lockId,\n }\n return { acquired: true, handle }\n },\n async release(handle) {\n if (!handle) return\n await releaseDurableTurnLock(options.namespace, handle as TurnLockReleaseInput, options.auth)\n },\n }\n}\n\n// ── broadcast helpers (products wire these into onEvent) ────────────────────\n\n/** Coarse per-workspace marker that a thread's turn started / ended — drives\n * a sidebar \"agent responding\" indicator subscribed once per workspace. */\nexport async function broadcastWorkspaceActivity(\n namespace: TurnStreamNamespaceLike,\n workspaceId: string,\n threadId: string,\n phase: 'start' | 'end',\n auth: TurnStreamAuth,\n): Promise<void> {\n try {\n await postJson(namespace, workspaceChannelKey(workspaceId), TURN_STREAM_PATHS.broadcast, {\n type: 'thread.activity',\n timestamp: Date.now(),\n data: { threadId, phase, workspaceId, sessionId: workspaceId },\n } satisfies TurnStreamEvent, auth)\n } catch {\n // Best-effort: the indicator is non-critical UI.\n }\n}\n\n/** Per-workspace marker that a new thread was created, so an already-open\n * history list prepends it without a reload. */\nexport async function broadcastThreadCreated(\n namespace: TurnStreamNamespaceLike,\n workspaceId: string,\n thread: { threadId: string; title: string },\n auth: TurnStreamAuth,\n): Promise<void> {\n try {\n await postJson(namespace, workspaceChannelKey(workspaceId), TURN_STREAM_PATHS.broadcast, {\n type: 'thread.created',\n timestamp: Date.now(),\n data: { threadId: thread.threadId, title: thread.title, workspaceId, sessionId: workspaceId },\n } satisfies TurnStreamEvent, auth)\n } catch {\n // Best-effort: the loader reconciles on the next revalidation.\n }\n}\n\n// ── WebSocket upgrade forwarder (worker entry) ──────────────────────────────\n\n/** Represent success or failure of a TURN stream upgrade authorization with optional response data */\nexport type TurnStreamUpgradeAuthorization = { ok: true } | { ok: false; response: Response }\n\n/** Define options for creating a TURN stream upgrade handler including namespace, path, and authorization logic */\nexport interface CreateTurnStreamUpgradeHandlerOptions {\n namespace: TurnStreamNamespaceLike\n /** HMAC secret shared with the DO binding (`env.TURN_STREAM_AUTH_SECRET`).\n * After the product `authorize` passes, the handler mints a channel-bound\n * capability token and the DO verifies it — the browser never holds it. */\n authSecret: string\n /** The worker route serving the stream. Default `/api/session-stream`. */\n path?: string\n /** Viewer access check (session cookie → workspace membership). */\n authorize(\n request: Request,\n target: { workspaceId: string },\n ): Promise<TurnStreamUpgradeAuthorization>\n}\n\n/**\n * The worker-entry WebSocket forwarder — WORKSPACE channel only (the\n * per-thread variant went with the deprecated rebroadcast lane; a thread's\n * live turn replay is the sandbox session gateway's job). Call BEFORE the\n * app router (a router loader cannot return a 101); returns `null` for\n * requests that are not a WebSocket upgrade on the configured path.\n *\n * GET {path}?workspaceId=... → workspace channel (sidebar signals)\n *\n * After the 101, the client sends `{type:'sync'}` and receives the\n * current-activity + recent-threads snapshot, then live signals.\n *\n * Security: the product's `authorize` decides whether THIS caller may see the\n * workspace's signals; the channel-bound token this handler then mints is\n * what the DO itself verifies — two independent gates, and the browser never\n * learns the token (it rides the forwarded upgrade request only).\n */\nexport function createTurnStreamUpgradeHandler(\n options: CreateTurnStreamUpgradeHandlerOptions,\n): (request: Request) => Promise<Response | null> {\n const path = options.path ?? '/api/session-stream'\n if (!options.authSecret || options.authSecret.length < 32) {\n throw new Error('createTurnStreamUpgradeHandler requires authSecret (>= 32 chars) — the DO fails closed without tokens')\n }\n return async (request) => {\n const url = new URL(request.url)\n if (url.pathname !== path || request.headers.get('Upgrade')?.toLowerCase() !== 'websocket') return null\n\n if (url.searchParams.has('threadId')) {\n return new Response('Thread replay uses the sandbox session gateway or chat replay route', { status: 400 })\n }\n const workspaceId = url.searchParams.get('workspaceId')\n if (!workspaceId) return new Response('Missing workspaceId', { status: 400 })\n\n const auth = await options.authorize(request, { workspaceId })\n if (!auth.ok) return auth.response\n\n const key = workspaceChannelKey(workspaceId)\n const stub = options.namespace.get(options.namespace.idFromName(key))\n\n const forwardUrl = new URL(request.url)\n forwardUrl.searchParams.set('sessionId', workspaceId)\n forwardUrl.searchParams.set('scope', 'workspace')\n forwardUrl.searchParams.delete('token')\n const headers = new Headers(request.headers)\n headers.set(TURN_STREAM_TOKEN_HEADER, await mintTurnStreamToken(key, options.authSecret))\n return stub.fetch(new Request(new Request(forwardUrl, request), { headers }))\n }\n}\n","/**\n * In-process turn-stream harness: a fake namespace that routes `idFromName`\n * to REAL {@link TurnStreamDO} instances over an in-memory storage/socket\n * state. The adapters and the DO run their production code paths — only the\n * Cloudflare runtime (isolation, hibernation, real sockets) is simulated.\n *\n * The harness runs the DO in its PRODUCTION auth mode: instances carry their\n * channel name and a capability-token secret (a fixed default, overridable),\n * so tests exercise the real gate — an adapter call without `auth` fails\n * exactly like a misconfigured worker would. Test-side viewer sockets attach\n * through `channel().connect()` (inside the DO, post-upgrade), which is the\n * one hop that legitimately skips the HTTP gate.\n *\n * For vitest composition tests and keyless local dev (the same role\n * `createMemoryTurnEventStore` plays for the D1 store). Not for production:\n * state is per-process and evaporates on restart.\n */\n\nimport { TurnStreamDO, type TurnStreamDOOptions, type TurnStreamDOState, type TurnStreamSocket, type TurnStreamStorage } from './do'\nimport type { TurnStreamNamespaceLike, TurnStreamStubLike } from './adapters'\n\nfunction createMemoryStorage(): TurnStreamStorage {\n const map = new Map<string, unknown>()\n return {\n async get<T>(key: string) {\n return map.get(key) as T | undefined\n },\n async put(key, value) {\n map.set(key, value)\n },\n async delete(key) {\n return map.delete(key)\n },\n async list<T>({ prefix, start }: { prefix: string; start?: string }) {\n const keys = [...map.keys()]\n .filter((key) => key.startsWith(prefix) && (start === undefined || key >= start))\n .sort()\n return new Map(keys.map((key) => [key, map.get(key) as T]))\n },\n }\n}\n\n/** A test-side viewer socket: records frames sent by the DO and lets the\n * test drive the `sync` handshake. */\nexport interface MemoryTurnStreamSocket extends TurnStreamSocket {\n readonly frames: string[]\n readonly closed: boolean\n}\n\n/** Define an in-memory channel for streaming turn-based data with viewer socket connection support */\nexport interface MemoryTurnStreamChannel {\n /** Attach a viewer socket to this channel (bypasses the HTTP 101 — the\n * upgrade handshake is Cloudflare-runtime-only) and run its `sync`. */\n connect(input: { sessionId: string; scope: 'thread' | 'workspace'; afterSeq?: number }): Promise<MemoryTurnStreamSocket>\n readonly instance: TurnStreamDO\n}\n\n/** Provide an interface to manage channels and namespaces for memory-based turn stream testing */\nexport interface MemoryTurnStreamHarness {\n namespace: TurnStreamNamespaceLike\n /** The channel (creating its DO instance if needed) for a channel key —\n * e.g. `threadChannelKey(ws, thread)` — to connect test viewers. */\n channel(name: string): MemoryTurnStreamChannel\n}\n\ninterface MemoryInstance {\n instance: TurnStreamDO\n sockets: MemoryTurnStreamSocket[]\n}\n\n/**\n * Build the harness. `createInstance` lets a product test run its own\n * `TurnStreamDO` subclass through the same wiring.\n */\n/** Default harness secret — long enough to satisfy the DO's minimum, fixed so\n * tests are deterministic. Products testing their own subclass pass their\n * own via `options.authSecret`. */\nexport const MEMORY_TURN_STREAM_AUTH_SECRET = 'test-turn-stream-harness-secret-0123456789ab'\n\nexport function createMemoryTurnStreamHarness(\n createInstance: (state: TurnStreamDOState, authSecret: string) => TurnStreamDO = (state, secret) =>\n new TurnStreamDO(state, undefined, { authSecret: secret }),\n options: TurnStreamDOOptions & { authSecret?: string } = {},\n): MemoryTurnStreamHarness {\n const authSecret = options.authSecret ?? MEMORY_TURN_STREAM_AUTH_SECRET\n const createInst = (state: TurnStreamDOState) => createInstance(state, authSecret)\n const instances = new Map<string, MemoryInstance>()\n\n function ensure(name: string): MemoryInstance {\n let entry = instances.get(name)\n if (!entry) {\n const sockets: MemoryTurnStreamSocket[] = []\n const state: TurnStreamDOState = {\n storage: createMemoryStorage(),\n acceptWebSocket: (ws) => sockets.push(ws as MemoryTurnStreamSocket),\n getWebSockets: () => sockets.filter((ws) => !ws.closed),\n id: { name },\n }\n entry = { instance: createInst(state), sockets }\n instances.set(name, entry)\n }\n return entry\n }\n\n\n const namespace: TurnStreamNamespaceLike = {\n idFromName: (name) => name,\n get(id): TurnStreamStubLike {\n const entry = ensure(String(id))\n return {\n fetch: (input, init) =>\n entry.instance.fetch(typeof input === 'string' ? new Request(input, init) : input),\n }\n },\n }\n\n return {\n namespace,\n channel(name) {\n const entry = ensure(name)\n return {\n instance: entry.instance,\n async connect({ sessionId, scope, afterSeq = 0 }) {\n let attachment: unknown = null\n const frames: string[] = []\n let closed = false\n const socket: MemoryTurnStreamSocket = {\n get frames() {\n return frames\n },\n get closed() {\n return closed\n },\n send(data) {\n if (closed) throw new Error('socket closed')\n frames.push(data)\n },\n close() {\n closed = true\n },\n serializeAttachment(value) {\n attachment = value\n },\n deserializeAttachment() {\n return attachment\n },\n }\n socket.serializeAttachment({ sessionId, scope, synced: false })\n entry.sockets.push(socket)\n await entry.instance.webSocketMessage(socket, JSON.stringify({ type: 'sync', afterSeq }))\n return socket\n },\n }\n },\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-app",
3
- "version": "0.60.210",
3
+ "version": "0.60.212",
4
4
  "packageManager": "pnpm@11.24.0",
5
5
  "description": "Build agent applications with typed chat, tools, sandboxes, integrations, billing, and evaluation.",
6
6
  "keywords": [
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/copy-quality/index.ts"],"sourcesContent":["/**\n * `@tangle-network/agent-app/copy-quality` — deterministic checks for copy an\n * agent writes for a reader: ads, posts, subject lines, emails, landing pages.\n *\n * No model call. A scan returns findings an agent can fix before a person sees\n * the draft, and the same findings feed a product's outcome checks, so the\n * in-run feedback and the scored episode never disagree.\n *\n * Tiers follow the \"AI tells\" list in coreyhaines31/marketingskills (MIT,\n * revision f719a8079c694e3267d47b6b60a62aa926055f2c,\n * skills/copywriting/references/ai-tells.md):\n * - ban: a tell on one sighting.\n * - cap: fine once; a tell when two land in one paragraph.\n * - weak: reported, never failing on its own.\n * Weighting follows blader/humanizer (MIT, revision\n * 225a6f39ac85f76ee48dbad772ea4abe4ed6c9d8): staged contrasts, negation lists\n * and staged reveals count on one sighting; a dash or a curly quote in long\n * copy is weak alone. The density score mirrors the EQ-Bench Slop Score mix:\n * 60% tell-word rate, 25% contrast constructions, 15% repeated trigrams.\n *\n * Code blocks, inline code and URLs are removed before scanning: an SDK\n * snippet in a post is the product, not prose.\n */\n\nimport { canonicalizeValue, valuesInText } from '../work-product/claim-support'\n\nexport type CopySurface = 'short' | 'long'\n\nexport type CopyTellTier = 'ban' | 'cap' | 'weak'\n\nexport type CopyTellCategory =\n | 'banned-word'\n | 'cap-words'\n | 'contrast-reveal'\n | 'negation-list'\n | 'question-reveal'\n | 'colon-reveal'\n | 'opener-closer'\n | 'marketing-phrase'\n | 'puffery'\n | 'weasel-source'\n | 'chummy-opener'\n | 'hype-punctuation'\n | 'decoration'\n | 'fragment-drumbeat'\n | 'dash'\n | 'chat-wrapper'\n\nexport interface CopyTellFinding {\n tier: CopyTellTier\n category: CopyTellCategory\n /** The text that triggered the finding. */\n match: string\n /** Zero-based paragraph index, or -1 for a whole-text finding. */\n paragraph: number\n /** What to do instead, written for the agent that will fix it. */\n fix: string\n}\n\nexport interface CopyScanOptions {\n /**\n * `short` = headline, subject line, ad, social post, SMS, hero, CTA: no\n * dashes at all. `long` = article, email body, page: two dashes allowed.\n * Default `long`.\n */\n surface?: CopySurface\n /** Product-specific banned words or phrases (case-insensitive, whole word). */\n extraBanned?: readonly string[]\n}\n\nexport interface CopyScanResult {\n /** No ban-tier and no cap-tier findings. Weak findings never fail alone. */\n pass: boolean\n findings: CopyTellFinding[]\n counts: Record<CopyTellTier, number>\n /** 0 (clean) to 100 (dense with tells). Informational; `pass` is the gate. */\n slopScore: number\n words: number\n}\n\n/**\n * Ban-tier vocabulary: ai-tells.md \"Vocabulary → Ban\". Base forms; the scanner\n * also matches inflections (\"leveraging\", \"synergies\") and a hyphen written as\n * a space (\"game changer\"), so prompts can list each word once.\n */\nexport const BAN_WORDS: readonly string[] = [\n 'delve', 'tapestry', 'testament to', 'realm', 'embark', 'beacon', 'multifaceted',\n 'paradigm', 'synergy', 'myriad', 'plethora', 'meticulous', 'intricate', 'utilize',\n 'leverage', 'supercharge', 'turbocharge', 'game-changer', 'game-changing',\n 'ever-evolving landscape', 'plays a crucial role',\n]\n\n/** Cap-tier vocabulary: ai-tells.md \"Vocabulary → Cap\" plus empty intensifiers. Base forms. */\nexport const CAP_WORDS: readonly string[] = [\n 'seamless', 'robust', 'streamline', 'empower', 'elevate', 'unlock', 'unleash',\n 'harness', 'foster', 'enhance', 'optimize', 'cutting-edge', 'innovative',\n 'revolutionary', 'transformative', 'holistic', 'comprehensive', 'pivotal', 'crucial',\n 'vital', 'powerful', 'next-level', 'world-class', 'best-in-class', 'landscape',\n 'navigate', 'ecosystem', 'journey',\n 'truly', 'genuinely', 'incredibly', 'deeply', 'fundamentally', 'significantly',\n 'simply', 'actually', 'literally', 'quietly',\n]\n\ninterface PhraseRule {\n readonly pattern: RegExp\n readonly category: CopyTellCategory\n readonly fix: string\n}\n\nconst rule = (category: CopyTellCategory, fix: string) => (pattern: RegExp): PhraseRule => ({ pattern, category, fix })\n\nconst CONTRAST_FIX = 'State the real point directly with its reason; do not stage a denial first.'\n\nconst PHRASE_RULES: readonly PhraseRule[] = [\n ...[\n /\\bin today'?s (?:fast-paced |digital |modern )?(?:world|landscape|market|age)\\b/i,\n /\\bwhether you'?re an? [^,.]{1,40},? or an? /i,\n /(?:^|[.!?]\\s+)imagine\\b/i,\n /\\bwhen it comes to\\b/i,\n /\\bhere'?s the thing\\b|\\bthe truth is\\b|\\blet me be clear\\b|\\bhere'?s what nobody tells you\\b/i,\n /\\bin this (?:guide|post|article),? we'?ll\\b|\\blet'?s dive in(?:to)?\\b/i,\n /\\bin conclusion\\b|\\bat the end of the day\\b/i,\n /(?:^|[.!?]\\s+)(?:it'?s worth noting|moreover|additionally|importantly|ultimately)\\b/i,\n ].map(rule('opener-closer', 'Delete the wind-up and start with the point.')),\n ...[\n /\\bsay (?:goodbye|hello) to\\b/i,\n /\\b(?:unlock|unleash) the (?:full )?power of\\b/i,\n /\\b\\w+, (?:reimagined|redefined)\\b/i,\n /\\bto the next level\\b/i,\n /\\btransform the way you\\b/i,\n /\\bthe \\w+ you deserve\\b/i,\n /\\bthe future of [\\w ]{1,30} is here\\b/i,\n /\\bbuilt for teams (?:who|that) move fast\\b/i,\n /\\beverything you need to\\b|\\ball-in-one\\b/i,\n /\\beffortless(?:ly)?\\b|\\bin just a few clicks\\b/i,\n /\\bjoin (?:thousands|millions|hundreds) of\\b/i,\n /\\bchanges everything\\b/i,\n /\\bexperience the power of\\b/i,\n ].map(rule('marketing-phrase', 'Replace with the concrete outcome, number or workflow.')),\n rule('puffery', 'State the fact; let the reader judge it.')(/\\b(?:marks|represents) a pivotal moment\\b|\\bredefines the category\\b/i),\n rule('weasel-source', 'Name the source or the customer, or cut the claim.')(/\\bexperts agree\\b|\\bstudies show\\b|\\btrusted by (?:industry )?leaders\\b/i),\n rule('chummy-opener', 'Cut the chummy opener.')(/^(?:great question|let'?s be honest|we get it)\\b/im),\n rule('contrast-reveal', CONTRAST_FIX)(\n /\\bit'?s not (?:just )?[^.,;!?]{1,60}[,;.]\\s*it'?s\\b|\\b(?:this|that) isn'?t [^.;!?]{1,60}\\.\\s*(?:this|that|it) is\\b|\\bnot just [^.;!?]{1,60},? but\\b|\\bnot because [^.;!?]{1,60}\\.\\s*because\\b|\\bless [a-z]+, more [a-z]+\\b|\\bthe question isn'?t\\b|\\?\\s*think again\\b/i,\n ),\n rule('contrast-reveal', CONTRAST_FIX)(/(?:^|[.!?]\\s+)Not [^.;!?]{1,40}\\.\\s+[A-Z][^.;!?]{1,60}\\./),\n rule('negation-list', 'Say what does happen. One plain \"No card required\" beside the CTA is fine.')(\n /\\bno [a-z-]+(?: [a-z-]+)?[,.]\\s*no [a-z-]+/i,\n ),\n rule('question-reveal', 'Delete the question and keep the answer.')(\n /\\bthe (?:result|catch|best part|secret|kicker|twist|answer|difference|outcome|upshot)\\?\\s+\\S/i,\n ),\n rule('colon-reveal', 'Write it as a normal sentence.')(\n /\\b(?:the best part|here'?s the (?:secret|thing|kicker|catch)|one word|the result|the truth|the secret)\\s*:\\s/i,\n ),\n rule('hype-punctuation', 'Remove the stacked punctuation.')(/[!?]{2,}/),\n rule('decoration', 'Remove decorative unicode letters and emoji bullets.')(\n /[\\u{1D400}-\\u{1D7FF}]|^\\s*(?:[-*•]\\s*)?[\\u{1F300}-\\u{1FAFF}\\u{2600}-\\u{27BF}]/mu,\n ),\n]\n\nconst CHAT_WRAPPERS: readonly RegExp[] = [\n /^\\s*(?:sure|certainly|absolutely|of course|great)[!,.]/i,\n /^\\s*here(?:'s| is| are) (?:a|an|the|your|my) (?:draft|version|post|email|ad|headline|copy|subject|options?|revised)/i,\n /\\bi hope (?:this|that) helps\\b/i,\n /\\blet me know if you(?:'d like| want| need)\\b/i,\n /\\b(?:want|would you like) me to\\b/i,\n /\\bfeel free to (?:adjust|tweak|edit)\\b/i,\n]\n\n// An em dash anywhere; an en dash, double hyphen or single hyphen used as a\n// dash between words. A range like 10–20 has no spaces and is not a dash.\nconst DASH = /—|(?<=\\S)[ \\t]+(?:–|--?)[ \\t]+(?=\\S)/g\nconst CURLY = /[‘’“”]/\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/** Inflections of one word: \"leverage\" → leverages, leveraged, leveraging. */\nfunction inflected(word: string): string {\n const stem = escapeRegExp(word)\n if (/e$/.test(word)) return `${stem.slice(0, -1)}(?:e|es|ed|ing|ely)`\n if (/[^aeiou]y$/.test(word)) return `${stem.slice(0, -1)}(?:y|ies|ied|ying|ily)`\n return `${stem}(?:s|es|ed|ing|ly)?`\n}\n\n/**\n * A whole-word matcher for a term. A single word (hyphenated or not) also\n * matches its inflections; a phrase matches as written. Hyphens and spaces in\n * a term match either, and a space matches any run of whitespace.\n */\nfunction termRegex(term: string): RegExp {\n const body = /\\s/.test(term.trim())\n ? escapeRegExp(term.trim())\n : inflected(term.trim())\n return new RegExp(`(?<![\\\\w-])${body.replace(/-|\\s+/g, '[-\\\\s]+')}(?![\\\\w-])`, 'gi')\n}\n\nconst BAN_REGEXES = BAN_WORDS.map((word) => ({ word, regex: termRegex(word) }))\nconst CAP_REGEXES = CAP_WORDS.map((word) => ({ word, regex: termRegex(word) }))\n\n/** The prose a reader reads: code, URLs and quote styles normalized away. */\nexport function proseOf(text: string): string {\n return text\n .replace(/```[\\s\\S]*?```/g, '\\n\\n')\n .replace(/`[^`\\n]*`/g, 'code')\n .replace(/https?:\\/\\/\\S+/g, 'link')\n .replace(/[‘’]/g, \"'\")\n .replace(/[“”]/g, '\"')\n}\n\nfunction paragraphsOf(prose: string): string[] {\n return prose.split(/\\n\\s*\\n/).map((p) => p.trim()).filter(Boolean)\n}\n\nfunction wordCount(text: string): number {\n return (text.match(/[A-Za-z0-9][\\w'-]*/g) ?? []).length\n}\n\nfunction repeatedTrigramRate(prose: string): number {\n const words = prose.toLowerCase().match(/[a-z0-9][a-z0-9'-]*/g) ?? []\n if (words.length < 12) return 0\n const seen = new Map<string, number>()\n for (let i = 0; i + 2 < words.length; i += 1) {\n const key = `${words[i]} ${words[i + 1]} ${words[i + 2]}`\n seen.set(key, (seen.get(key) ?? 0) + 1)\n }\n let repeats = 0\n for (const count of seen.values()) if (count > 1) repeats += count - 1\n return repeats / (words.length - 2)\n}\n\n/** Scan copy for AI tells. Deterministic and cheap; run it on every draft. */\nexport function scanCopy(text: string, options: CopyScanOptions = {}): CopyScanResult {\n const surface = options.surface ?? 'long'\n const prose = proseOf(text)\n const paragraphs = paragraphsOf(prose)\n // An extra already in the Ban tier (or listed twice) must not count twice.\n const extras = [...new Set((options.extraBanned ?? []).map((word) => word.toLowerCase()))]\n .filter((word) => !BAN_WORDS.includes(word))\n const banned = [...BAN_REGEXES, ...extras.map((word) => ({ word, regex: termRegex(word) }))]\n // A product that bans a cap-tier word outright gets one finding for it, not two.\n const capped = CAP_REGEXES.filter(({ word }) => !extras.includes(word))\n const findings: CopyTellFinding[] = []\n let contrastParagraphs = 0\n let tellWords = 0\n\n paragraphs.forEach((paragraph, index) => {\n for (const { word, regex } of banned) {\n for (const match of paragraph.matchAll(regex)) {\n tellWords += 1\n findings.push({ tier: 'ban', category: 'banned-word', match: match[0], paragraph: index, fix: `Replace \"${word}\" with the fact it stands in for.` })\n }\n }\n const capMatches = capped.flatMap(({ regex }) => [...paragraph.matchAll(regex)].map((match) => match[0]))\n tellWords += capMatches.length\n if (capMatches.length >= 2) {\n findings.push({ tier: 'cap', category: 'cap-words', match: capMatches.join(', '), paragraph: index, fix: 'Two cap-tier words in one paragraph: rewrite it from the facts.' })\n }\n let contrast = false\n for (const { pattern, category, fix } of PHRASE_RULES) {\n const match = paragraph.match(pattern)\n if (!match) continue\n if (category === 'contrast-reveal') {\n if (contrast) continue\n contrast = true\n }\n findings.push({ tier: 'ban', category, match: match[0].trim(), paragraph: index, fix })\n }\n if (contrast) contrastParagraphs += 1\n let run = 0\n for (const sentence of paragraph.split(/(?<=[.!?])\\s+/)) {\n const count = wordCount(sentence)\n run = count > 0 && count <= 3 ? run + 1 : 0\n if (run === 3) {\n findings.push({ tier: 'cap', category: 'fragment-drumbeat', match: sentence, paragraph: index, fix: 'Merge the fragments into one sentence that carries a fact.' })\n break\n }\n }\n })\n\n const dashes = prose.match(DASH)?.length ?? 0\n if (dashes > 0) {\n const tier: CopyTellTier = surface === 'short' ? 'ban' : dashes > 2 ? 'cap' : 'weak'\n const fix = surface === 'short'\n ? 'Short copy uses no dashes between words: use a period, comma or colon.'\n : 'Long copy allows two dashes per page at most; prefer a period or comma.'\n findings.push({ tier, category: 'dash', match: `${dashes} dash${dashes === 1 ? '' : 'es'}`, paragraph: -1, fix })\n }\n if (CURLY.test(text)) {\n findings.push({ tier: 'weak', category: 'decoration', match: 'curly quotes', paragraph: -1, fix: 'Weak alone: use the quote style the target format uses.' })\n }\n const wrapper = chatWrapperIn(text)\n if (wrapper !== null) {\n findings.push({ tier: 'ban', category: 'chat-wrapper', match: wrapper, paragraph: -1, fix: 'Return only the deliverable, with no chat around it.' })\n }\n\n const counts: Record<CopyTellTier, number> = { ban: 0, cap: 0, weak: 0 }\n for (const finding of findings) counts[finding.tier] += 1\n const words = wordCount(prose)\n // Saturates at 40 tell words per 1,000, a contrast in every paragraph, and 10% repeated trigrams.\n const wordRate = Math.min(1, (tellWords * 1000) / Math.max(1, words) / 40)\n const contrastRate = paragraphs.length === 0 ? 0 : contrastParagraphs / paragraphs.length\n const trigramRate = Math.min(1, repeatedTrigramRate(prose) * 10)\n const slopScore = Math.round(100 * (0.6 * wordRate + 0.25 * contrastRate + 0.15 * trigramRate))\n return { pass: counts.ban === 0 && counts.cap === 0, findings, counts, slopScore, words }\n}\n\n/** The chat wrapper around a deliverable (\"Here's a draft…\", \"Hope this helps\"), or null. */\nexport function chatWrapperIn(text: string): string | null {\n const prose = proseOf(text)\n for (const pattern of CHAT_WRAPPERS) {\n const match = prose.match(pattern)\n if (match) return match[0].trim()\n }\n return null\n}\n\n/**\n * Banned words and phrases for prompts that list them: base forms, lowercased,\n * deduplicated, product extras first. The scanner matches their inflections.\n */\nexport function bannedCopyVocabulary(extra: readonly string[] = []): string[] {\n return [...new Set([...extra, ...BAN_WORDS].map((word) => word.toLowerCase()))]\n}\n\nexport interface MetricClaim {\n /** The figure as written, e.g. \"$0.02/hr\", \"40%\", \"10x\", \"300ms\". */\n text: string\n /** Canonical value used to match it against sources (claim-support rules). */\n value: string\n}\n\n// A figure a reader takes as a measured fact: money, a percentage, a multiple,\n// a duration or size with its unit, a scaled count, or a grouped number.\n// Bare small integers (\"3 steps\") and years are not metrics.\nconst METRIC =\n /[$€£¥₹]\\s?\\d[\\d,]*(?:\\.\\d+)?(?:\\s?[kKmMbB]\\b)?|\\b\\d[\\d,]*(?:\\.\\d+)?\\s?(?:%|x\\b|×|ms\\b|s\\b|sec\\b|secs\\b|seconds?\\b|mins?\\b|minutes?\\b|hrs?\\b|hours?\\b|days?\\b|[kKmMbB]\\+?(?=\\s|$|[.,;])|[KMGT]B\\b|rps\\b|qps\\b|tokens?\\b)|\\b\\d{1,3}(?:,\\d{3})+(?:\\.\\d+)?\\b/g\n\n/** Every figure in `text` a reader would take as a measured fact. */\nexport function metricClaims(text: string): MetricClaim[] {\n const claims: MetricClaim[] = []\n for (const match of proseOf(text).matchAll(METRIC)) {\n const numeric = match[0].match(/[$€£¥₹]?\\s?\\d[\\d,]*(?:\\.\\d+)?/)?.[0]\n const value = numeric ? canonicalizeValue(numeric) : null\n if (value !== null) claims.push({ text: match[0].trim(), value })\n }\n return claims\n}\n\n/**\n * Metric claims in `copy` whose value appears in none of `sources`: figures\n * the writer cannot point to. Values match with claim-support's\n * canonicalization, so \"$1,200\" in copy matches \"1200.00\" in a source.\n */\nexport function unsourcedMetrics(copy: string, sources: readonly string[]): MetricClaim[] {\n const known = new Set(sources.flatMap((source) => valuesInText(source)))\n return metricClaims(copy).filter((claim) => !known.has(claim.value))\n}\n"],"mappings":"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","names":[]}