@tangle-network/agent-app 0.43.46 → 0.43.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Recovery policy for a `ChatTurnLock` whose holder died.
3
+ *
4
+ * `createChatTurnRoutes` takes the lock as a seam (`acquire`/`release`) and a
5
+ * lock is a single-flight guard: while it is held, a second turn on the same
6
+ * scope is refused. Products give it a TTL measured in tens of minutes, so a
7
+ * turn that dies without releasing wedges chat for that whole window. Every
8
+ * app on the seam inherits that wedge, which is why the way OUT of it is
9
+ * policy this package owns rather than something each app rediscovers.
10
+ *
11
+ * The policy takes PROBES, not clients: it imports no sandbox SDK, opens no
12
+ * connection, and knows nothing about how a product finds its box or talks to
13
+ * a sidecar. That is what makes the rules testable and what keeps the concrete
14
+ * probes — which box key, which session id, which sidecar endpoint — in the
15
+ * product.
16
+ *
17
+ * The rules, in precedence order:
18
+ *
19
+ * 1. The session probe answered and the execution is TERMINAL ⇒ release, once
20
+ * the lock is past a short grace period. The authority on "is this turn
21
+ * still running" is whatever is actually running it; a terminal verdict is
22
+ * proof the lock outlived its turn — but only if the verdict is about THIS
23
+ * turn, which is what the grace buys (see
24
+ * {@link DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS}).
25
+ * 2. The session probe answered and the execution is LIVE ⇒ hold, always.
26
+ * Nothing below may override this. The lock is doing exactly its job.
27
+ * 3. The probes could not reach that authority at all — the sandbox could not
28
+ * be listed, is gone, is not running, or its session probe failed ⇒ fall
29
+ * back on the physical argument: an execution runs INSIDE the box, so a box
30
+ * that is not there is running nothing, and the lock is releasable. Without
31
+ * this fallback the recovery would depend on the very subsystem whose
32
+ * failure produced the stale lock.
33
+ *
34
+ * Rule 3 is gated on a grace period because it is an inference, not an
35
+ * observation — see {@link DEFAULT_STALE_TURN_LOCK_GRACE_MS}.
36
+ */
37
+ /** Where the box is, as far as the caller can see. `state` on `not-running`
38
+ * is the platform's own status string, carried through for the log. */
39
+ type StaleTurnLockSandboxProbeResult = {
40
+ status: 'running';
41
+ } | {
42
+ status: 'absent';
43
+ } | {
44
+ status: 'not-running';
45
+ state?: string;
46
+ };
47
+ /** What the thing running the turn says about it. `terminal: false` means an
48
+ * execution is LIVE — the strongest signal in the policy. `diagnostics` rides
49
+ * through to the result and the logs unread. */
50
+ type StaleTurnLockSessionProbeResult = {
51
+ reachable: true;
52
+ terminal: boolean;
53
+ diagnostics?: Record<string, unknown>;
54
+ } | {
55
+ reachable: false;
56
+ reason?: string;
57
+ };
58
+ /**
59
+ * Minimum age a lock must reach before the "sandbox unreachable ⇒ nothing can
60
+ * be running" fallback may force-release it.
61
+ *
62
+ * The lock is acquired BEFORE the box is ensured, so during a cold workspace's
63
+ * first turn there is a real window in which the lock is held and no box exists
64
+ * yet — indistinguishable, from a peek, from a box that vanished. The grace
65
+ * period has to outlast that window (create + bootstrap + whatever the product
66
+ * hydrates) or a concurrent request steals the lock from a turn that is merely
67
+ * still provisioning. Five minutes clears observed cold starts with room to
68
+ * spare while cutting the worst case from a TTL-length wedge down to five
69
+ * minutes. Raising it makes recovery slower; lowering it risks stealing a lock
70
+ * mid-provision.
71
+ */
72
+ declare const DEFAULT_STALE_TURN_LOCK_GRACE_MS: number;
73
+ /**
74
+ * Minimum age a lock must reach before a TERMINAL session verdict may release
75
+ * it.
76
+ *
77
+ * The session probe is keyed on the THREAD, not on the execution the lock
78
+ * holds: a sidecar that has nothing running reports `terminal` with
79
+ * `activeExecutionId: null`, so there is no id to match the lock against. The
80
+ * lock, meanwhile, is acquired BEFORE the box is ensured and before the
81
+ * execution registers with the sidecar. Between those two moments a second
82
+ * request that reconciles the lock asks the sidecar about a turn it has not
83
+ * heard of yet and gets back the PREVIOUS turn's terminal state — proof about
84
+ * the wrong execution. Releasing on that verdict hands the second request a
85
+ * lock the first one is still using, which is two concurrent turns on a scope
86
+ * whose single-flight guard just voted for itself.
87
+ *
88
+ * One minute covers the acquire → box-ensure → sidecar-registration window on
89
+ * a warm box (the cold-box case is Rule 3's, and has its own, much longer
90
+ * grace). Deliberately NOT
91
+ * {@link DEFAULT_STALE_TURN_LOCK_GRACE_MS}: this branch has a positive
92
+ * observation behind it, so it should recover fast, and stretching it to five
93
+ * minutes would leave a genuinely dead turn wedged for the whole window that
94
+ * the session probe exists to shortcut. Raising it delays recovery from a
95
+ * crashed turn; lowering it narrows the registration window it protects.
96
+ */
97
+ declare const DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS: number;
98
+ interface ReconcileStaleTurnLockOptions {
99
+ /** When the held lock was acquired (epoch ms). The grace period is measured
100
+ * from here, so it must be the LOCK's start, not the turn's. */
101
+ lockStartedAt: number;
102
+ /** Is the box there and running? Never provisions — a peek, not an ensure.
103
+ * A throw is treated as unreachable, same as `absent`. */
104
+ probeSandbox(): Promise<StaleTurnLockSandboxProbeResult>;
105
+ /** Ask the running box whether the execution is still live. Only called when
106
+ * `probeSandbox` reported `running`. A throw is treated as unreachable. */
107
+ probeSession(): Promise<StaleTurnLockSessionProbeResult>;
108
+ /** Release the lock, fenced by the instant the releasing evidence was
109
+ * observed. `fence.observedAt` is snapshotted BEFORE the probe that
110
+ * justified the release, so a store that can compare it against the held
111
+ * lock's start refuses to delete a SUCCESSOR lock acquired while the probe
112
+ * was in flight. A store that cannot make that comparison may ignore the
113
+ * fence, but must not substitute its own `Date.now()` — that timestamp is
114
+ * by construction newer than any successor and makes the check vacuous.
115
+ *
116
+ * Returns whether the release actually landed — `false` when the lock was
117
+ * already gone (someone else got there first), which is reported, never
118
+ * treated as a release. */
119
+ release(fence: {
120
+ observedAt: number;
121
+ }): boolean | Promise<boolean>;
122
+ /** Override {@link DEFAULT_STALE_TURN_LOCK_GRACE_MS} (Rule 3's fallback). */
123
+ graceMs?: number;
124
+ /** Override {@link DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS} (Rule 1's release). */
125
+ terminalGraceMs?: number;
126
+ /** Identity fields merged into every log line (workspace, thread, execution
127
+ * id — whatever makes the entry findable in the product's logs). */
128
+ context?: Record<string, unknown>;
129
+ /** Defaults to `console.warn`. Both the withheld and the force-released
130
+ * branches log; a force-release is never silent. */
131
+ log?(message: string, meta: Record<string, unknown>): void;
132
+ /** Injectable clock, for tests. */
133
+ now?(): number;
134
+ }
135
+ interface ReconcileStaleTurnLockResult {
136
+ released: boolean;
137
+ /** Why the policy decided what it did — the probe's own diagnostics on the
138
+ * reachable path, the unreachable reason and lock age on the fallback. */
139
+ diagnostics: Record<string, unknown>;
140
+ }
141
+ /**
142
+ * Decide whether a held lock is stale and, if so, release it.
143
+ *
144
+ * Never provisions and never mutates anything but the lock: a reconciliation
145
+ * attempt on a cold workspace leaves it cold.
146
+ */
147
+ declare function reconcileStaleTurnLock(options: ReconcileStaleTurnLockOptions): Promise<ReconcileStaleTurnLockResult>;
148
+
149
+ export { DEFAULT_STALE_TURN_LOCK_GRACE_MS as D, type ReconcileStaleTurnLockOptions as R, type StaleTurnLockSandboxProbeResult as S, DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS as a, type ReconcileStaleTurnLockResult as b, type StaleTurnLockSessionProbeResult as c, reconcileStaleTurnLock as r };
@@ -1,4 +1,5 @@
1
1
  import { b as ChatInteractionStatus } from '../contract-KfqJh_au.js';
2
+ export { B as BufferedTurnEvent, a as BufferedTurnOptions, b as BufferedTurnTap, D as D1LikeForTurns, P as PumpBufferedTurnOptions, R as ReplayTurnEventsOptions, T as TURN_EVENTS_MIGRATION_SQL, c as TURN_STATUS_SCOPE_MIGRATION_SQL, d as TurnEventStore, e as TurnStatus, f as coalesceChatStreamEvents, g as coalesceDeltas, h as createBufferedTurnTap, i as createD1TurnEventStore, j as createMemoryTurnEventStore, p as pumpBufferedTurn, r as replayTurnEvents } from '../turn-buffer-C9mEgoop.js';
2
3
  import '@tangle-network/agent-interface';
3
4
 
4
5
  type JsonRecord = Record<string, unknown>;
@@ -67,149 +68,4 @@ declare function resolveChatTurn(input: {
67
68
  turnId?: string;
68
69
  }): ResolvedChatTurn;
69
70
 
70
- /**
71
- * Resumable chat turns — the router-path answer to "streams resume on
72
- * disconnect" (issue #27). A turn's loop events are teed into a store as they
73
- * stream; the turn keeps running under `ctx.waitUntil` when the client drops;
74
- * a reconnecting client replays the buffered tail by sequence number and
75
- * keeps following until the turn completes.
76
- *
77
- * POST /chat/stream → pumpBufferedTurn(...) + live NDJSON
78
- * GET /chat/stream/:turnId → replayTurnEvents({ fromSeq }) → NDJSON
79
- *
80
- * Storage is a structural seam ({@link TurnEventStore}); a D1 implementation
81
- * ships here because that's what Cloudflare products have (KV is unsuitable:
82
- * eventually consistent cross-isolate). Per-token deltas would mean hundreds
83
- * of rows per turn, so consecutive text/reasoning deltas are coalesced within
84
- * a flush window before they are persisted — replay yields slightly chunkier
85
- * deltas with identical concatenation.
86
- */
87
- type TurnStatus = 'running' | 'complete' | 'error';
88
- interface BufferedTurnEvent {
89
- seq: number;
90
- /** The serialized event line (JSON string, no trailing newline). */
91
- event: string;
92
- }
93
- interface TurnEventStore {
94
- append(turnId: string, events: BufferedTurnEvent[]): Promise<void>;
95
- read(turnId: string, fromSeq: number): Promise<BufferedTurnEvent[]>;
96
- /** Record turn lifecycle. `scopeId` (a thread/session id) is optional and lets
97
- * {@link TurnEventStore.listRunning} rediscover this turn after a client reload
98
- * loses the turnId; stores that don't track scope ignore it. */
99
- setStatus(turnId: string, status: TurnStatus, scopeId?: string): Promise<void>;
100
- getStatus(turnId: string): Promise<TurnStatus | null>;
101
- /** Running turnIds for a scope, newest first — so a reloaded client (clientRunId
102
- * lost) can find and resume the in-flight turn. Optional: a store records it
103
- * only if `setStatus` was given a `scopeId`. */
104
- listRunning?(scopeId: string): Promise<string[]>;
105
- }
106
- /** Merge consecutive text/reasoning deltas of the same type into one event.
107
- * Concatenation-preserving: replaying the coalesced stream produces the same
108
- * accumulated text as the original. */
109
- declare function coalesceDeltas(events: unknown[]): unknown[];
110
- /**
111
- * Coalesce consecutive `message.part.updated` deltas for the SAME part into one
112
- * event. agent-runtime products stream `ChatStreamEvent` NDJSON
113
- * (`{type:'message.part.updated', data:{part, delta}}`); pumped through the
114
- * buffer with the default tool-loop coalescer, every per-token delta persists as
115
- * its own row because that coalescer never recognizes the shape. Pass this as
116
- * {@link PumpBufferedTurnOptions.coalesce} instead.
117
- *
118
- * Concatenation-preserving for BOTH consumer styles: the merged event keeps the
119
- * LATEST event's `data.part` (already the cumulative accumulation) and sets
120
- * `data.delta` to the concatenation of the merged deltas, so a client that
121
- * appends `delta` and one that reads the cumulative `part` both reconstruct the
122
- * identical final text.
123
- */
124
- declare function coalesceChatStreamEvents(events: unknown[]): unknown[];
125
- interface BufferedTurnOptions {
126
- store: TurnEventStore;
127
- turnId: string;
128
- /** Deliver one serialized line to the live client. Throwing here (client
129
- * disconnected) does NOT stop buffering — events keep persisting. */
130
- write?: (line: string) => Promise<void> | void;
131
- /** Flush buffered events to the store at most this often. Default 400ms. */
132
- flushIntervalMs?: number;
133
- /** Per-flush coalescer. Default {@link coalesceDeltas} (tool-loop text/reasoning
134
- * deltas). agent-runtime products streaming `ChatStreamEvent` pass
135
- * {@link coalesceChatStreamEvents} so per-token deltas don't each persist as a
136
- * row. Must be concatenation-preserving. */
137
- coalesce?: (events: unknown[]) => unknown[];
138
- /** Optional scope (thread/session id) recorded with the turn status, so
139
- * {@link TurnEventStore.listRunning} can find this turn after a reload. */
140
- scopeId?: string;
141
- }
142
- /** A push-driven buffer for a turn whose producer the caller does NOT own. */
143
- interface BufferedTurnTap {
144
- /** Buffer one event: persist (coalesced, on the flush window) + best-effort
145
- * live-deliver. Wire to a push source's per-event hook (e.g. agent-runtime
146
- * `handleChatTurn`'s `hooks.onEvent`). Marks the turn 'running' on first call. */
147
- onEvent(raw: unknown): Promise<void>;
148
- /** Settle the turn: final flush + set status. Call after the producer resolves
149
- * ('complete') or rejects ('error'). 'error' flushes what was produced first. */
150
- done(status?: Extract<TurnStatus, 'complete' | 'error'>): Promise<void>;
151
- }
152
- /**
153
- * The buffering core. Sequence-numbers every event, delivers it to `write`
154
- * (best-effort — a disconnected client never stops buffering), and flushes to
155
- * the store in coalesced batches. Drives both transports:
156
- *
157
- * • {@link pumpBufferedTurn} — when you OWN an `AsyncIterable` producer.
158
- * • this tap (`onEvent`/`done`) — when the producer owns iteration and only
159
- * hands you a push callback (agent-runtime `handleChatTurn`'s `hooks.onEvent`
160
- * + the finished body). Durability stays here in the shell; the engine needs
161
- * no `TurnEventStore` seam.
162
- */
163
- declare function createBufferedTurnTap(opts: BufferedTurnOptions): BufferedTurnTap;
164
- interface PumpBufferedTurnOptions extends BufferedTurnOptions {
165
- source: AsyncIterable<unknown>;
166
- }
167
- /**
168
- * Drive a turn to completion regardless of the live client, when you OWN the
169
- * producer as an `AsyncIterable`. A thin driver over {@link createBufferedTurnTap}.
170
- * Returns a promise that resolves when the turn finishes — hand it to
171
- * `ctx.waitUntil` so a disconnect can't kill the turn. Never rejects on
172
- * client-write failure; a source error marks the turn 'error' (after flushing
173
- * what was produced) and rethrows.
174
- */
175
- declare function pumpBufferedTurn(opts: PumpBufferedTurnOptions): Promise<void>;
176
- interface ReplayTurnEventsOptions {
177
- store: TurnEventStore;
178
- turnId: string;
179
- /** Replay strictly after this sequence number (0 = from the beginning). */
180
- fromSeq?: number;
181
- /** Poll cadence while the turn is still running. Default 500ms. */
182
- pollMs?: number;
183
- /** Give up following a 'running' turn after this long. Default 120s. */
184
- timeoutMs?: number;
185
- }
186
- /**
187
- * Yield buffered events after `fromSeq`, then keep polling while the turn is
188
- * still 'running' until it completes, errors, or times out. Terminates with a
189
- * final `{seq: -1, event: '{"type":"turn_status",...}'}` marker so clients
190
- * know why the replay ended.
191
- */
192
- declare function replayTurnEvents(opts: ReplayTurnEventsOptions): AsyncGenerator<BufferedTurnEvent>;
193
- /** Minimal structural D1 contract (Cloudflare `D1Database` satisfies it). */
194
- interface D1LikeForTurns {
195
- prepare(sql: string): {
196
- bind(...values: unknown[]): {
197
- run(): Promise<unknown>;
198
- all<T = Record<string, unknown>>(): Promise<{
199
- results: T[];
200
- }>;
201
- first<T = Record<string, unknown>>(): Promise<T | null>;
202
- };
203
- };
204
- }
205
- /** Schema for the D1 store — append to the product's migrations. */
206
- declare const TURN_EVENTS_MIGRATION_SQL = "\nCREATE TABLE IF NOT EXISTS turn_events (\n turnId TEXT NOT NULL,\n seq INTEGER NOT NULL,\n event TEXT NOT NULL,\n PRIMARY KEY (turnId, seq)\n);\nCREATE TABLE IF NOT EXISTS turn_status (\n turnId TEXT PRIMARY KEY,\n status TEXT NOT NULL,\n scopeId TEXT,\n updatedAt TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS idx_turn_status_scope ON turn_status (scopeId, status);\n";
207
- /** For deployments whose `turn_status` table predates `scopeId`/`listRunning` —
208
- * run once to add the column (the CREATE above already includes it for new
209
- * deployments). SQLite ignores a duplicate-add error if already applied. */
210
- declare const TURN_STATUS_SCOPE_MIGRATION_SQL = "ALTER TABLE turn_status ADD COLUMN scopeId TEXT;";
211
- declare function createD1TurnEventStore(db: D1LikeForTurns): TurnEventStore;
212
- /** In-memory store for tests and keyless local dev. */
213
- declare function createMemoryTurnEventStore(): TurnEventStore;
214
-
215
- export { type BufferedTurnEvent, type BufferedTurnOptions, type BufferedTurnTap, type D1LikeForTurns, type JsonRecord, MISSING_TOOL_TERMINAL_ERROR, MISSING_TOOL_TERMINAL_REASON, type PersistedChatMessageForTurn, type PumpBufferedTurnOptions, type ReplayTurnEventsOptions, type ResolvedChatTurn, type StreamEvent, TURN_EVENTS_MIGRATION_SQL, TURN_STATUS_SCOPE_MIGRATION_SQL, type TurnEventStore, type TurnStatus, asRecord, asString, attachmentPartKey, buildUserTextParts, coalesceChatStreamEvents, coalesceDeltas, collapseRedundantTextParts, createBufferedTurnTap, createD1TurnEventStore, createMemoryTurnEventStore, encodeEvent, finalizeAssistantParts, finalizePendingInteractionParts, getPartKey, mergePersistedPart, messageHasTurnId, normalizeClientTurnId, normalizePersistedPart, normalizeTime, normalizeToolEvent, pumpBufferedTurn, replayTurnEvents, resolveChatTurn, resolveToolId, resolveToolName, terminalizeDanglingAssistantToolUpdates, terminalizeDanglingToolPart, terminalizeDanglingToolParts };
71
+ export { type JsonRecord, MISSING_TOOL_TERMINAL_ERROR, MISSING_TOOL_TERMINAL_REASON, type PersistedChatMessageForTurn, type ResolvedChatTurn, type StreamEvent, asRecord, asString, attachmentPartKey, buildUserTextParts, collapseRedundantTextParts, encodeEvent, finalizeAssistantParts, finalizePendingInteractionParts, getPartKey, mergePersistedPart, messageHasTurnId, normalizeClientTurnId, normalizePersistedPart, normalizeTime, normalizeToolEvent, resolveChatTurn, resolveToolId, resolveToolName, terminalizeDanglingAssistantToolUpdates, terminalizeDanglingToolPart, terminalizeDanglingToolParts };
@@ -1,3 +1,12 @@
1
+ import {
2
+ INVITATION_EXPIRY_DAYS,
3
+ generateInvitationToken,
4
+ getInvitationExpiresAt,
5
+ inviteUrlForToken,
6
+ normalizeInvitationEmail,
7
+ parseInvitationPermission,
8
+ renderInvitationEmail
9
+ } from "../chunk-WEBBJBDH.js";
1
10
  import {
2
11
  generateInviteToken,
3
12
  isInviteTokenShape,
@@ -18,15 +27,6 @@ import {
18
27
  workspaceRoleToCollaborationAccess,
19
28
  workspaceRoleToSandboxRole
20
29
  } from "../chunk-63CE7FEZ.js";
21
- import {
22
- INVITATION_EXPIRY_DAYS,
23
- generateInvitationToken,
24
- getInvitationExpiresAt,
25
- inviteUrlForToken,
26
- normalizeInvitationEmail,
27
- parseInvitationPermission,
28
- renderInvitationEmail
29
- } from "../chunk-WEBBJBDH.js";
30
30
  export {
31
31
  ASSIGNABLE_WORKSPACE_ROLES,
32
32
  INVITATION_EXPIRY_DAYS,
@@ -1,10 +1,3 @@
1
- import {
2
- SeatLimitError
3
- } from "../chunk-SWUVTGMR.js";
4
- import "../chunk-3SVAA3MA.js";
5
- import {
6
- hasWorkspaceRole
7
- } from "../chunk-63CE7FEZ.js";
8
1
  import {
9
2
  generateInvitationToken,
10
3
  getInvitationExpiresAt,
@@ -12,6 +5,13 @@ import {
12
5
  normalizeInvitationEmail,
13
6
  parseInvitationPermission
14
7
  } from "../chunk-WEBBJBDH.js";
8
+ import {
9
+ SeatLimitError
10
+ } from "../chunk-SWUVTGMR.js";
11
+ import "../chunk-3SVAA3MA.js";
12
+ import {
13
+ hasWorkspaceRole
14
+ } from "../chunk-63CE7FEZ.js";
15
15
 
16
16
  // src/teams/invitations-api.ts
17
17
  import { and, eq, lte, sql } from "drizzle-orm";
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Resumable chat turns — the router-path answer to "streams resume on
3
+ * disconnect" (issue #27). A turn's loop events are teed into a store as they
4
+ * stream; the turn keeps running under `ctx.waitUntil` when the client drops;
5
+ * a reconnecting client replays the buffered tail by sequence number and
6
+ * keeps following until the turn completes.
7
+ *
8
+ * POST /chat/stream → pumpBufferedTurn(...) + live NDJSON
9
+ * GET /chat/stream/:turnId → replayTurnEvents({ fromSeq }) → NDJSON
10
+ *
11
+ * Storage is a structural seam ({@link TurnEventStore}); a D1 implementation
12
+ * ships here because that's what Cloudflare products have (KV is unsuitable:
13
+ * eventually consistent cross-isolate). Per-token deltas would mean hundreds
14
+ * of rows per turn, so consecutive text/reasoning deltas are coalesced within
15
+ * a flush window before they are persisted — replay yields slightly chunkier
16
+ * deltas with identical concatenation.
17
+ */
18
+ type TurnStatus = 'running' | 'complete' | 'error';
19
+ interface BufferedTurnEvent {
20
+ seq: number;
21
+ /** The serialized event line (JSON string, no trailing newline). */
22
+ event: string;
23
+ }
24
+ interface TurnEventStore {
25
+ append(turnId: string, events: BufferedTurnEvent[]): Promise<void>;
26
+ read(turnId: string, fromSeq: number): Promise<BufferedTurnEvent[]>;
27
+ /** Record turn lifecycle. `scopeId` (a thread/session id) is optional and lets
28
+ * {@link TurnEventStore.listRunning} rediscover this turn after a client reload
29
+ * loses the turnId; stores that don't track scope ignore it. */
30
+ setStatus(turnId: string, status: TurnStatus, scopeId?: string): Promise<void>;
31
+ getStatus(turnId: string): Promise<TurnStatus | null>;
32
+ /** Running turnIds for a scope, newest first — so a reloaded client (clientRunId
33
+ * lost) can find and resume the in-flight turn. Optional: a store records it
34
+ * only if `setStatus` was given a `scopeId`. */
35
+ listRunning?(scopeId: string): Promise<string[]>;
36
+ }
37
+ /** Merge consecutive text/reasoning deltas of the same type into one event.
38
+ * Concatenation-preserving: replaying the coalesced stream produces the same
39
+ * accumulated text as the original. */
40
+ declare function coalesceDeltas(events: unknown[]): unknown[];
41
+ /**
42
+ * Coalesce consecutive `message.part.updated` deltas for the SAME part into one
43
+ * event. agent-runtime products stream `ChatStreamEvent` NDJSON
44
+ * (`{type:'message.part.updated', data:{part, delta}}`); pumped through the
45
+ * buffer with the default tool-loop coalescer, every per-token delta persists as
46
+ * its own row because that coalescer never recognizes the shape. Pass this as
47
+ * {@link PumpBufferedTurnOptions.coalesce} instead.
48
+ *
49
+ * Concatenation-preserving for BOTH consumer styles: the merged event keeps the
50
+ * LATEST event's `data.part` (already the cumulative accumulation) and sets
51
+ * `data.delta` to the concatenation of the merged deltas, so a client that
52
+ * appends `delta` and one that reads the cumulative `part` both reconstruct the
53
+ * identical final text.
54
+ */
55
+ declare function coalesceChatStreamEvents(events: unknown[]): unknown[];
56
+ interface BufferedTurnOptions {
57
+ store: TurnEventStore;
58
+ turnId: string;
59
+ /** Deliver one serialized line to the live client. Throwing here (client
60
+ * disconnected) does NOT stop buffering — events keep persisting. */
61
+ write?: (line: string) => Promise<void> | void;
62
+ /** Flush buffered events to the store at most this often. Default 400ms. */
63
+ flushIntervalMs?: number;
64
+ /** Per-flush coalescer. Default {@link coalesceDeltas} (tool-loop text/reasoning
65
+ * deltas). agent-runtime products streaming `ChatStreamEvent` pass
66
+ * {@link coalesceChatStreamEvents} so per-token deltas don't each persist as a
67
+ * row. Must be concatenation-preserving. */
68
+ coalesce?: (events: unknown[]) => unknown[];
69
+ /** Optional scope (thread/session id) recorded with the turn status, so
70
+ * {@link TurnEventStore.listRunning} can find this turn after a reload. */
71
+ scopeId?: string;
72
+ }
73
+ /** A push-driven buffer for a turn whose producer the caller does NOT own. */
74
+ interface BufferedTurnTap {
75
+ /** Buffer one event: persist (coalesced, on the flush window) + best-effort
76
+ * live-deliver. Wire to a push source's per-event hook (e.g. agent-runtime
77
+ * `handleChatTurn`'s `hooks.onEvent`). Marks the turn 'running' on first call. */
78
+ onEvent(raw: unknown): Promise<void>;
79
+ /** Settle the turn: final flush + set status. Call after the producer resolves
80
+ * ('complete') or rejects ('error'). 'error' flushes what was produced first. */
81
+ done(status?: Extract<TurnStatus, 'complete' | 'error'>): Promise<void>;
82
+ }
83
+ /**
84
+ * The buffering core. Sequence-numbers every event, delivers it to `write`
85
+ * (best-effort — a disconnected client never stops buffering), and flushes to
86
+ * the store in coalesced batches. Drives both transports:
87
+ *
88
+ * • {@link pumpBufferedTurn} — when you OWN an `AsyncIterable` producer.
89
+ * • this tap (`onEvent`/`done`) — when the producer owns iteration and only
90
+ * hands you a push callback (agent-runtime `handleChatTurn`'s `hooks.onEvent`
91
+ * + the finished body). Durability stays here in the shell; the engine needs
92
+ * no `TurnEventStore` seam.
93
+ */
94
+ declare function createBufferedTurnTap(opts: BufferedTurnOptions): BufferedTurnTap;
95
+ interface PumpBufferedTurnOptions extends BufferedTurnOptions {
96
+ source: AsyncIterable<unknown>;
97
+ }
98
+ /**
99
+ * Drive a turn to completion regardless of the live client, when you OWN the
100
+ * producer as an `AsyncIterable`. A thin driver over {@link createBufferedTurnTap}.
101
+ * Returns a promise that resolves when the turn finishes — hand it to
102
+ * `ctx.waitUntil` so a disconnect can't kill the turn. Never rejects on
103
+ * client-write failure; a source error marks the turn 'error' (after flushing
104
+ * what was produced) and rethrows.
105
+ */
106
+ declare function pumpBufferedTurn(opts: PumpBufferedTurnOptions): Promise<void>;
107
+ interface ReplayTurnEventsOptions {
108
+ store: TurnEventStore;
109
+ turnId: string;
110
+ /** Replay strictly after this sequence number (0 = from the beginning). */
111
+ fromSeq?: number;
112
+ /** Poll cadence while the turn is still running. Default 500ms. */
113
+ pollMs?: number;
114
+ /** Give up following a 'running' turn after this long. Default 120s. */
115
+ timeoutMs?: number;
116
+ }
117
+ /**
118
+ * Yield buffered events after `fromSeq`, then keep polling while the turn is
119
+ * still 'running' until it completes, errors, or times out. Terminates with a
120
+ * final `{seq: -1, event: '{"type":"turn_status",...}'}` marker so clients
121
+ * know why the replay ended.
122
+ */
123
+ declare function replayTurnEvents(opts: ReplayTurnEventsOptions): AsyncGenerator<BufferedTurnEvent>;
124
+ /** Minimal structural D1 contract (Cloudflare `D1Database` satisfies it). */
125
+ interface D1LikeForTurns {
126
+ prepare(sql: string): {
127
+ bind(...values: unknown[]): {
128
+ run(): Promise<unknown>;
129
+ all<T = Record<string, unknown>>(): Promise<{
130
+ results: T[];
131
+ }>;
132
+ first<T = Record<string, unknown>>(): Promise<T | null>;
133
+ };
134
+ };
135
+ }
136
+ /** Schema for the D1 store — append to the product's migrations. */
137
+ declare const TURN_EVENTS_MIGRATION_SQL = "\nCREATE TABLE IF NOT EXISTS turn_events (\n turnId TEXT NOT NULL,\n seq INTEGER NOT NULL,\n event TEXT NOT NULL,\n PRIMARY KEY (turnId, seq)\n);\nCREATE TABLE IF NOT EXISTS turn_status (\n turnId TEXT PRIMARY KEY,\n status TEXT NOT NULL,\n scopeId TEXT,\n updatedAt TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS idx_turn_status_scope ON turn_status (scopeId, status);\n";
138
+ /** For deployments whose `turn_status` table predates `scopeId`/`listRunning` —
139
+ * run once to add the column (the CREATE above already includes it for new
140
+ * deployments). SQLite ignores a duplicate-add error if already applied. */
141
+ declare const TURN_STATUS_SCOPE_MIGRATION_SQL = "ALTER TABLE turn_status ADD COLUMN scopeId TEXT;";
142
+ declare function createD1TurnEventStore(db: D1LikeForTurns): TurnEventStore;
143
+ /** In-memory store for tests and keyless local dev. */
144
+ declare function createMemoryTurnEventStore(): TurnEventStore;
145
+
146
+ export { type BufferedTurnEvent as B, type D1LikeForTurns as D, type PumpBufferedTurnOptions as P, type ReplayTurnEventsOptions as R, TURN_EVENTS_MIGRATION_SQL as T, type BufferedTurnOptions as a, type BufferedTurnTap as b, TURN_STATUS_SCOPE_MIGRATION_SQL as c, type TurnEventStore as d, type TurnStatus as e, coalesceChatStreamEvents as f, coalesceDeltas as g, createBufferedTurnTap as h, createD1TurnEventStore as i, createMemoryTurnEventStore as j, pumpBufferedTurn as p, replayTurnEvents as r };