@workerdeck/server 0.23.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.d.mts +63 -844
- package/build/index.mjs +187 -1022
- package/build/index.mjs.map +1 -1
- package/package.json +13 -13
package/build/index.d.mts
CHANGED
|
@@ -2,215 +2,81 @@ import { IncomingMessage, Server, ServerResponse } from "node:http";
|
|
|
2
2
|
import { AttachmentInput, BridgeAnswer, BrowserBridgeExecutor, ClaudeAuthProbe, EngineAdapter, EngineSessionOptions, HostToolDefinition, LanguageModel, McpConnection, ParkedExecution, Runner, RunnerSnapshot, SessionRunnerConfig, ToolExecutionCall, ToolExecutionResult, ToolExecutor, ToolSet } from "@workerdeck/core";
|
|
3
3
|
import { JobQueue, QueueAdapter } from "@workerdeck/queue";
|
|
4
4
|
import { CreateSessionRequest, JobEvent, MessageAttachment, ProfileEngine, ProfileInfo, ProfileUsage, ProviderConfig, SdkSessionSummary, ServerFrame, SessionCapability, SessionInfo, SessionNotification, SessionWebhookConfig } from "@workerdeck/protocol";
|
|
5
|
-
|
|
6
5
|
//#region src/services/bridge.d.ts
|
|
7
6
|
type BridgeHubOptions = {
|
|
8
|
-
|
|
9
|
-
/** Called when a bridged execution reaches a terminal result — the host feeds
|
|
10
|
-
* it back into the runner's loop. */
|
|
7
|
+
timeoutMs?: number;
|
|
11
8
|
onResult?: (sessionId: string, executionId: string, result: ToolExecutionResult) => void;
|
|
12
9
|
};
|
|
13
|
-
/**
|
|
14
|
-
* Routes tool executions between a session and the browser tabs attached to it.
|
|
15
|
-
*
|
|
16
|
-
* A session may have several clients attached (dashboard plus embedded panel);
|
|
17
|
-
* the bridge asks the **first attached** one, which is the closest thing to "the
|
|
18
|
-
* client driving this session". If none is attached, dispatch fails fast rather
|
|
19
|
-
* than hanging — an autonomous job simply never bridges, it uses the server
|
|
20
|
-
* executor instead.
|
|
21
|
-
*/
|
|
22
10
|
declare class BridgeHub {
|
|
23
11
|
#private;
|
|
24
12
|
constructor(options?: BridgeHubOptions);
|
|
25
|
-
/** The executor to hand a runner for this session. Created on first use and
|
|
26
|
-
* reused, so results routed back always reach the same pending table. */
|
|
27
13
|
executorFor(sessionId: string): BrowserBridgeExecutor;
|
|
28
|
-
/** How many clients are watching this session. Parking consults it: a session
|
|
29
|
-
* someone is watching stays live. */
|
|
30
14
|
attachedCount(sessionId: string): number;
|
|
31
|
-
/** Register an attached client. Returns a detach function. */
|
|
32
15
|
attach(sessionId: string, send: (frame: ServerFrame) => void): () => void;
|
|
33
|
-
/**
|
|
34
|
-
* Deliver a client's answer to a bridged call. Returns false when the id is
|
|
35
|
-
* unknown or already settled — late and duplicate answers are ignored.
|
|
36
|
-
*/
|
|
37
16
|
resolve(sessionId: string, executionId: string, answer: BridgeAnswer): boolean;
|
|
38
|
-
/** Drop a session's bridge, failing anything still in flight. */
|
|
39
17
|
remove(sessionId: string): void;
|
|
40
18
|
}
|
|
41
19
|
//#endregion
|
|
42
20
|
//#region src/services/notifications.d.ts
|
|
43
21
|
type SessionNotificationOptions = {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
* Unfiltered: `webhook.events` narrows POST deliveries, not this. */
|
|
48
|
-
onNotification?: (notification: SessionNotification) => void; /** Delivery attempts per notification (exponential backoff). Default 3. */
|
|
49
|
-
attempts?: number; /** Initial backoff between attempts. Default 500ms. */
|
|
22
|
+
webhook?: SessionWebhookConfig;
|
|
23
|
+
onNotification?: (notification: SessionNotification) => void;
|
|
24
|
+
attempts?: number;
|
|
50
25
|
retryDelayMs?: number;
|
|
51
|
-
/** Gateway wiring, not a host option: the serve-time `SessionInfo` decoration
|
|
52
|
-
* (project identity today), so a webhook or push consumer reads the same
|
|
53
|
-
* record every REST caller does. The assembly supplies it; identity when
|
|
54
|
-
* absent. */
|
|
55
26
|
decorateInfo?: (info: SessionInfo) => SessionInfo;
|
|
56
27
|
};
|
|
57
|
-
/**
|
|
58
|
-
* Turns session events into the handful of notifications a human away from the
|
|
59
|
-
* screen cares about, and delivers them to a webhook and/or a local observer.
|
|
60
|
-
*
|
|
61
|
-
* This is the *primitive*, deliberately transport-agnostic: the server stays
|
|
62
|
-
* credential-free and knows nothing about APNs, Slack or email. Turning a
|
|
63
|
-
* notification into a push is a forwarder's job (the turnkey CLI's), and one that
|
|
64
|
-
* needs credentials, so it does not live here.
|
|
65
|
-
*
|
|
66
|
-
* Delivery is best-effort and ordered per session, mirroring the job queue's
|
|
67
|
-
* webhook behaviour — a consumer that missed one can always attach to the session
|
|
68
|
-
* WS with `afterSeq` and see the truth.
|
|
69
|
-
*/
|
|
70
28
|
declare class SessionNotifier {
|
|
71
29
|
#private;
|
|
72
30
|
constructor(options: SessionNotificationOptions);
|
|
73
|
-
/** True when nothing is listening — lets the caller skip subscribing at all. */
|
|
74
31
|
get idle(): boolean;
|
|
75
|
-
/**
|
|
76
|
-
* Subscribe to a runner for its lifetime.
|
|
77
|
-
*
|
|
78
|
-
* `afterSeq` defaults to whatever the runner has already emitted, which is what
|
|
79
|
-
* makes this safe on a *rehydrated* session: `subscribe` replays the log from
|
|
80
|
-
* `afterSeq`, so subscribing at 0 to a session rebuilt from a park would
|
|
81
|
-
* re-announce every permission request it ever made.
|
|
82
|
-
*/
|
|
83
32
|
watch(runner: Runner, afterSeq?: number): void;
|
|
84
33
|
}
|
|
85
34
|
//#endregion
|
|
86
35
|
//#region src/services/registry.d.ts
|
|
87
36
|
type SessionRegistryOptions = {
|
|
88
|
-
/**
|
|
89
|
-
* Called once per runner as it enters the table, before it starts — the one
|
|
90
|
-
* seam every path goes through (create, prepare, adopt, and the rebuild of a
|
|
91
|
-
* parked session), which is what a watcher that must not miss a session needs.
|
|
92
|
-
*/
|
|
93
37
|
onRegister?: (runner: Runner) => void;
|
|
94
38
|
};
|
|
95
|
-
/** In-memory session table. Terminal sessions stay listed until removed or the process exits. */
|
|
96
39
|
declare class SessionRegistry {
|
|
97
40
|
#private;
|
|
98
41
|
constructor(options?: SessionRegistryOptions);
|
|
99
42
|
create(config: SessionRunnerConfig): Runner;
|
|
100
|
-
/** Build and list a Claude-engine runner without starting it, so watchers can
|
|
101
|
-
* subscribe first. Call `start()` once they have. */
|
|
102
43
|
prepare(config: SessionRunnerConfig): Runner;
|
|
103
|
-
/** Register an already-built runner (a non-Claude engine) and start it. */
|
|
104
44
|
adopt(runner: Runner): Runner;
|
|
105
|
-
/** List a runner without starting it — for a rehydrated session, whose watchers
|
|
106
|
-
* must be subscribed before it comes back up. */
|
|
107
45
|
register(runner: Runner): Runner;
|
|
108
46
|
get(id: string): Runner | undefined;
|
|
109
47
|
list(): SessionInfo[];
|
|
110
48
|
remove(id: string): boolean;
|
|
111
|
-
/** Drop a runner WITHOUT closing it: the session isn't ending, it parked and
|
|
112
|
-
* lives on in its snapshot. Closing here would tell every client it was over. */
|
|
113
49
|
evict(id: string): boolean;
|
|
114
50
|
closeAll(): void;
|
|
115
51
|
}
|
|
116
52
|
//#endregion
|
|
117
53
|
//#region src/services/session-store.d.ts
|
|
118
|
-
/**
|
|
119
|
-
* A session captured whole: its wire-visible info, the config to rebuild the
|
|
120
|
-
* runner, the engine's snapshot, and what it is waiting for.
|
|
121
|
-
*
|
|
122
|
-
* Two things are stored in this one shape, and the discriminator is `kind`:
|
|
123
|
-
*
|
|
124
|
-
* - **`parked`** — the live runner is torn down and the session is waiting on
|
|
125
|
-
* deferred executions. The record *is* the session, so waking consumes it.
|
|
126
|
-
* - **`live`** — the runner is up and this is a copy taken after a turn, so the
|
|
127
|
-
* session survives a restart (`persistLive`). It is a way back that the
|
|
128
|
-
* session still needs the *next* time the process dies, so waking **refreshes
|
|
129
|
-
* it in place** and only `session_closed` removes it.
|
|
130
|
-
*
|
|
131
|
-
* That difference is the whole reason these are two kinds rather than one, and
|
|
132
|
-
* it is a correctness difference, not bookkeeping: consuming a live record on
|
|
133
|
-
* wake opens a window from the attach to the next turn in which the session
|
|
134
|
-
* exists nowhere durable. A user who opens a session, reads it and types nothing
|
|
135
|
-
* would lose it to a redeploy — silently, which is the failure class worth
|
|
136
|
-
* spending a discriminator on.
|
|
137
|
-
*
|
|
138
|
-
* They share the shape so a store, and an older server, need no new code: every
|
|
139
|
-
* other branch (rebuild from `config` + `snapshot`, serve `snapshot.vfs`, arm
|
|
140
|
-
* `executions`, subscribe past `snapshot.seq`) is already right for both.
|
|
141
|
-
*/
|
|
142
54
|
type ParkedSessionRecord = {
|
|
143
|
-
|
|
55
|
+
kind?: 'parked' | 'live';
|
|
144
56
|
id: string;
|
|
145
|
-
/** Session info as of the write: `parked` for a park, `idle` for a live copy —
|
|
146
|
-
* never `running`, which would come back as a spinner over no process. */
|
|
147
57
|
info: SessionInfo;
|
|
148
|
-
profile?: string;
|
|
58
|
+
profile?: string;
|
|
149
59
|
config: SessionRunnerConfig;
|
|
150
60
|
snapshot: RunnerSnapshot;
|
|
151
|
-
|
|
152
|
-
* nothing for `hydrate` to arm a watchdog for. */
|
|
153
|
-
executions: ParkedExecution[]; /** When it was written. Named for the park that came first. */
|
|
61
|
+
executions: ParkedExecution[];
|
|
154
62
|
parkedAt: number;
|
|
155
63
|
};
|
|
156
|
-
/**
|
|
157
|
-
* A live session, remembered so it can be brought back after a gateway restart.
|
|
158
|
-
*
|
|
159
|
-
* The counterpart to a park, and deliberately not the same mechanism. A park
|
|
160
|
-
* preserves *mid-task* state, which means a `RunnerSnapshot` — and only the
|
|
161
|
-
* provider engine can produce one, because the claude and codex engines run
|
|
162
|
-
* behind a binary that owns its own process state. What those two have instead
|
|
163
|
-
* is a session store of their own: the transcript is already on disk under an
|
|
164
|
-
* engine session id, and `CreateSessionRequest.resume` is how you get it back.
|
|
165
|
-
*
|
|
166
|
-
* So this record holds no transcript at all. It holds the id (every client keys
|
|
167
|
-
* its watermarks and routes on it), the engine session id to resume from, and
|
|
168
|
-
* the config to rebuild with — and rehydration is an ordinary create with
|
|
169
|
-
* `resume` set, done **lazily on first attach**, because eagerly respawning
|
|
170
|
-
* every session at boot is a fork bomb wearing a feature's clothes.
|
|
171
|
-
*
|
|
172
|
-
* Written only for engines whose capability record says `resume`, and only once
|
|
173
|
-
* an `sdkSessionId` exists: a record that would come back with an empty
|
|
174
|
-
* transcript is worse than no record.
|
|
175
|
-
*/
|
|
176
64
|
type DormantSessionRecord = {
|
|
177
65
|
kind: 'dormant';
|
|
178
66
|
id: string;
|
|
179
|
-
/** Session info as of the last save, with `status: 'idle'` — whatever it was
|
|
180
|
-
* doing, it is not doing it now. */
|
|
181
67
|
info: SessionInfo;
|
|
182
68
|
profile?: string;
|
|
183
|
-
|
|
184
|
-
* The config the session was built from, minus the ephemeral keys. Fed back
|
|
185
|
-
* through the server's `buildRunnerConfig` on wake rather than used as-is, so
|
|
186
|
-
* the profile's env pin and the host hook's injections are **re-derived**
|
|
187
|
-
* instead of persisted (see {@link EPHEMERAL_CONFIG_KEYS}).
|
|
188
|
-
*/
|
|
189
|
-
config: SessionRunnerConfig; /** Where the transcript actually lives. Without one there is nothing to resume. */
|
|
69
|
+
config: SessionRunnerConfig;
|
|
190
70
|
sdkSessionId: string;
|
|
191
71
|
savedAt: number;
|
|
192
72
|
};
|
|
193
|
-
/** What a {@link SessionStore} holds: a session waiting on deferred work, or one
|
|
194
|
-
* waiting to be asked for again. */
|
|
195
73
|
type StoredSessionRecord = ParkedSessionRecord | DormantSessionRecord;
|
|
196
|
-
/**
|
|
197
|
-
* Where parked sessions live. Two implementations ship: {@link MemorySessionStore}
|
|
198
|
-
* (a park survives a disconnect, not a restart) and {@link createFileSessionStore}
|
|
199
|
-
* (it survives both, on one host); a redis/sqlite/table store implements the same
|
|
200
|
-
* four operations.
|
|
201
|
-
*
|
|
202
|
-
* Two things to know before writing one: the record holds the session's whole
|
|
203
|
-
* transcript and tool I/O, and `config` may carry host-injected values (env, hooks,
|
|
204
|
-
* injected functions) that a JSON round-trip silently drops or, worse, persists.
|
|
205
|
-
* {@link toDurableRecord} is the filter the bundled file store applies — reuse it.
|
|
206
|
-
*/
|
|
207
74
|
interface SessionStore {
|
|
208
75
|
save(record: StoredSessionRecord): Promise<void>;
|
|
209
76
|
get(id: string): Promise<StoredSessionRecord | null>;
|
|
210
77
|
list(): Promise<StoredSessionRecord[]>;
|
|
211
78
|
delete(id: string): Promise<boolean>;
|
|
212
79
|
}
|
|
213
|
-
/** Single-process, no persistence: parks survive a client disconnect, not a restart. */
|
|
214
80
|
declare class MemorySessionStore implements SessionStore {
|
|
215
81
|
#private;
|
|
216
82
|
save(record: StoredSessionRecord): Promise<void>;
|
|
@@ -218,209 +84,60 @@ declare class MemorySessionStore implements SessionStore {
|
|
|
218
84
|
list(): Promise<StoredSessionRecord[]>;
|
|
219
85
|
delete(id: string): Promise<boolean>;
|
|
220
86
|
}
|
|
221
|
-
/** The record as it may be persisted: same session, config narrowed to what is
|
|
222
|
-
* safe and meaningful to keep (see {@link EPHEMERAL_CONFIG_KEYS}). */
|
|
223
87
|
declare function toDurableRecord<T extends StoredSessionRecord>(record: T): T;
|
|
224
88
|
type FileSessionStoreOptions = {
|
|
225
|
-
/** Directory holding one JSON file per parked session.
|
|
226
|
-
* Default `<cwd>/.workerdeck/parked`. */
|
|
227
89
|
dir?: string;
|
|
228
|
-
/** A record that could not be read or written. Losing one is losing a session's
|
|
229
|
-
* way back, so this is worth logging — the store itself stays quiet and skips it. */
|
|
230
90
|
onError?: (error: unknown, context: {
|
|
231
91
|
path: string;
|
|
232
92
|
op: 'save' | 'read' | 'delete';
|
|
233
93
|
}) => void;
|
|
234
94
|
};
|
|
235
|
-
/**
|
|
236
|
-
* Durable single-host store: one JSON file per parked session under `dir`, written
|
|
237
|
-
* through a temp file and a rename so a crash mid-write cannot truncate a session.
|
|
238
|
-
* `hydrate()` at `listen()` picks them up, re-indexes their executions, and re-arms
|
|
239
|
-
* the watchdogs, so a restart no longer loses parked work.
|
|
240
|
-
*
|
|
241
|
-
* Know what is on that disk: **the record holds the session's entire transcript** —
|
|
242
|
-
* prompts, model output, and tool I/O — in plaintext. Put it somewhere with the same
|
|
243
|
-
* protection as the SDK's own transcripts (`~/.claude/projects`), not in a directory
|
|
244
|
-
* that gets served, synced, or backed up somewhere looser.
|
|
245
|
-
*
|
|
246
|
-
* Single-process by design, exactly like the bundled queue adapter and profile
|
|
247
|
-
* store: two servers sharing one directory would both hydrate the same records and
|
|
248
|
-
* race to rebuild them. That is what the seam is for.
|
|
249
|
-
*
|
|
250
|
-
* Nothing here reaps: a record leaves only when its session wakes or is deleted.
|
|
251
|
-
* An execution dispatched without a deadline (a `DeferredExecutor` with no
|
|
252
|
-
* `timeoutMs`) has no watchdog to end the wait, so its record — and its transcript
|
|
253
|
-
* — stays until `DELETE /sessions/:id`. Give deferred calls a deadline, or sweep.
|
|
254
|
-
*/
|
|
255
95
|
declare function createFileSessionStore(options?: FileSessionStoreOptions): SessionStore;
|
|
256
96
|
//#endregion
|
|
257
97
|
//#region src/services/parking.d.ts
|
|
98
|
+
type ParkErrorContext = {
|
|
99
|
+
sessionId: string;
|
|
100
|
+
phase: 'park' | 'remember' | 'resume';
|
|
101
|
+
};
|
|
258
102
|
type SessionParkOptions = {
|
|
259
103
|
registry: SessionRegistry;
|
|
260
104
|
store: SessionStore;
|
|
261
|
-
/**
|
|
262
|
-
* Rebuild a stored session's runner. For a park the snapshot rides in on
|
|
263
|
-
* `config.restore`, so the engine adopts the id, event log, and history; for a
|
|
264
|
-
* dormant record there is no snapshot and the engine is asked to resume its
|
|
265
|
-
* own session under the stored id. Either way the runner it returns must carry
|
|
266
|
-
* `record.id` — {@link SessionParkManager} refuses one that does not.
|
|
267
|
-
*/
|
|
268
105
|
rebuild: (record: StoredSessionRecord) => Promise<Runner>;
|
|
269
|
-
/** How many clients are attached to this session. A watched session stays live:
|
|
270
|
-
* parking would pull the runner out from under the socket. */
|
|
271
106
|
attachedCount: (sessionId: string) => number;
|
|
272
|
-
/** Wait this long after the last client detaches before parking, so a reconnect
|
|
273
|
-
* (a wifi blip, a page reload) doesn't cost a teardown. Default 2000. */
|
|
274
107
|
parkDelayMs?: number;
|
|
275
|
-
/**
|
|
276
|
-
* Keep a live session's snapshot written through to the store, so it survives
|
|
277
|
-
* a gateway restart. **Off by default**: a library must not start writing a
|
|
278
|
-
* session's whole transcript to disk because someone upgraded.
|
|
279
|
-
*
|
|
280
|
-
* This is the restart story for the engine that cannot go dormant. Dormancy
|
|
281
|
-
* works by remembering an *engine* session id to resume from, which the
|
|
282
|
-
* provider engine does not have — there is no store behind it, the history
|
|
283
|
-
* lives in the runner. What it has instead is `snapshot()`, so the record
|
|
284
|
-
* carries the state itself and rehydration is the ordinary `restore` path.
|
|
285
|
-
* Between the two, every engine survives a restart.
|
|
286
|
-
*
|
|
287
|
-
* Written at the end of a turn, never on a shutdown hook — a `kill -9`, an OOM
|
|
288
|
-
* or a pulled power cable run no hook, and that is precisely the case this
|
|
289
|
-
* exists for. Turn-end is also a natural rate limit: one write per turn, not
|
|
290
|
-
* one per token.
|
|
291
|
-
*/
|
|
292
108
|
persistLive?: boolean;
|
|
293
|
-
/** Grace given at {@link SessionParkManager.hydrate} to an execution whose
|
|
294
|
-
* deadline passed while the server was down. Its result could not have been
|
|
295
|
-
* delivered during the outage, so failing it the instant the process is back
|
|
296
|
-
* would throw away an answer that is very likely seconds behind. Extends a
|
|
297
|
-
* deadline, never shortens one. Default 60000. */
|
|
298
109
|
expiredGraceMs?: number;
|
|
299
|
-
/** Veto + accounting hook, called before the teardown: the job queue frees the
|
|
300
|
-
* run's concurrency slot here, and refuses (false) when the run is finalizing. */
|
|
301
110
|
onParking?: (sessionId: string, executionId: string) => boolean;
|
|
302
|
-
/** The session is live again under a NEW runner object — anything holding the
|
|
303
|
-
* old reference must rebind. */
|
|
304
111
|
onResumed?: (sessionId: string, runner: Runner) => void;
|
|
305
|
-
|
|
306
|
-
* is intact, the host's storage or engine assembly isn't. */
|
|
307
|
-
onError?: (error: unknown, context: {
|
|
308
|
-
sessionId: string;
|
|
309
|
-
phase: 'park' | 'remember' | 'resume';
|
|
310
|
-
}) => void;
|
|
112
|
+
onError?: (error: unknown, context: ParkErrorContext) => void;
|
|
311
113
|
};
|
|
312
|
-
/**
|
|
313
|
-
* Two ways a session outlives its runner, behind one door.
|
|
314
|
-
*
|
|
315
|
-
* **Parking** is deferred execution's other half: a session waiting on work no
|
|
316
|
-
* process in this server is doing.
|
|
317
|
-
*
|
|
318
|
-
* **Dormancy** is the restart story for the engines that cannot park. Every live
|
|
319
|
-
* claude or codex session leaves a small record naming its engine session id, so
|
|
320
|
-
* a gateway that comes back up lists them and resumes one the first time someone
|
|
321
|
-
* attaches. Both kinds live in the same store and come back through the same
|
|
322
|
-
* `ensureLive`, which is why there is one class here and not two.
|
|
323
|
-
*
|
|
324
|
-
* The runner announces the moment with `status_changed: 'parked'` — emitted only
|
|
325
|
-
* once every dispatch of the batch has been handed over, so the snapshot can never
|
|
326
|
-
* miss a call that was still being dispatched. From there this class snapshots,
|
|
327
|
-
* evicts, and persists; delivering a result rebuilds the runner under the same id
|
|
328
|
-
* and hands the result to it. The session's identity, event log, and seq numbering
|
|
329
|
-
* survive intact, so a client reattaching with `afterSeq` sees one unbroken stream.
|
|
330
|
-
*/
|
|
331
114
|
declare class SessionParkManager {
|
|
332
115
|
#private;
|
|
333
116
|
constructor(options: SessionParkOptions);
|
|
334
|
-
/** Record the config a session was created with. Only sessions the host
|
|
335
|
-
* remembers can be parked — there is no way to rebuild the others. */
|
|
336
117
|
remember(sessionId: string, config: SessionRunnerConfig): void;
|
|
337
|
-
/**
|
|
338
|
-
* Re-save a live session's dormant record because something outside the event
|
|
339
|
-
* stream changed it.
|
|
340
|
-
*
|
|
341
|
-
* `#rememberDormant` is otherwise driven by `status_changed` and `system_init`
|
|
342
|
-
* alone, and a rename (`PATCH /sessions/:id`) fires neither — so without this
|
|
343
|
-
* a renamed session that is never touched again keeps its old title on disk
|
|
344
|
-
* and comes back under it. Safe to call for anything: every gate in
|
|
345
|
-
* `#rememberDormant` still applies, so a session that cannot be resumed, has
|
|
346
|
-
* no engine session yet, or is no longer the registry's writes nothing.
|
|
347
|
-
*/
|
|
348
118
|
touch(runner: Runner): void;
|
|
349
|
-
/**
|
|
350
|
-
* Adopt the store's contents (a durable store after a restart): re-index the
|
|
351
|
-
* executions and re-arm their watchdogs, no deadline sooner than the grace
|
|
352
|
-
* window — nothing could have been delivered while the process was down.
|
|
353
|
-
*
|
|
354
|
-
* Dormant records need nothing here, which is the point of them. They list
|
|
355
|
-
* from the store (`listInfo`) and come back on first attach (`ensureLive`), so
|
|
356
|
-
* a boot with fifty remembered sessions spawns nothing at all.
|
|
357
|
-
*/
|
|
358
119
|
hydrate(): Promise<void>;
|
|
359
|
-
/**
|
|
360
|
-
* Follow a session's lifecycle: index its deferred executions, park it when the
|
|
361
|
-
* engine says the turn has come to rest on them, and clean up when it ends.
|
|
362
|
-
* `afterSeq` skips a rehydrated runner's replayed history (re-arming a watchdog
|
|
363
|
-
* from an event whose deadline already passed would fail the execution instantly).
|
|
364
|
-
*/
|
|
365
120
|
watch(runner: Runner, afterSeq?: number): () => void;
|
|
366
|
-
/** A client detached: park the session if that was the last one watching. */
|
|
367
121
|
onDetach(sessionId: string): void;
|
|
368
|
-
/** Which session this execution belongs to — still waiting, or already settled. */
|
|
369
122
|
sessionFor(executionId: string): string | undefined;
|
|
370
|
-
/** The stored session's record, for the read paths (GET, list, attach). */
|
|
371
123
|
get(id: string): Promise<StoredSessionRecord | null>;
|
|
372
|
-
/** Every stored session's info, to merge into `GET {basePath}/sessions`. */
|
|
373
124
|
listInfo(): Promise<SessionInfo[]>;
|
|
374
|
-
/** The live runner for a session, rehydrating a parked one on demand. Undefined
|
|
375
|
-
* when the session is neither live nor parked. */
|
|
376
125
|
ensureLive(id: string): Promise<Runner | undefined>;
|
|
377
|
-
/**
|
|
378
|
-
* Deliver a deferred execution's result. Rehydrates the session if needed and
|
|
379
|
-
* folds the result into its agent loop.
|
|
380
|
-
*
|
|
381
|
-
* Undefined = no session is waiting on that id. `applied: false` = it was already
|
|
382
|
-
* settled: a duplicate delivery, or one racing the watchdog. Both are expected,
|
|
383
|
-
* neither is an error.
|
|
384
|
-
*/
|
|
385
126
|
submitResult(executionId: string, result: ToolExecutionResult): Promise<{
|
|
386
127
|
applied: boolean;
|
|
387
128
|
sessionId: string;
|
|
388
129
|
} | undefined>;
|
|
389
|
-
/** Drop a parked session for good: the run is over (closed, canceled, killed). */
|
|
390
130
|
discard(sessionId: string): Promise<void>;
|
|
391
131
|
close(): void;
|
|
392
132
|
}
|
|
393
133
|
//#endregion
|
|
394
134
|
//#region src/services/profile-store.d.ts
|
|
395
|
-
/**
|
|
396
|
-
* Where dashboard-managed profiles live. The seam exists for the same reason
|
|
397
|
-
* `QueueAdapter` does: a single-host deployment wants the bundled file store and
|
|
398
|
-
* no configuration, while an operator with a database wants their own.
|
|
399
|
-
*
|
|
400
|
-
* Profiles declared in `createWorkerServer({ profiles })` never enter a store —
|
|
401
|
-
* they are code, and stay immutable. The store holds only what the management
|
|
402
|
-
* routes created, and the two sets are unioned by name.
|
|
403
|
-
*
|
|
404
|
-
* A store holds NO credentials: `ProviderConfig.apiKeyEnv` is a variable name and
|
|
405
|
-
* a Claude profile's `configDir` is a path. Both are resolved by the server's own
|
|
406
|
-
* environment at session time, which is what keeps a stored profile safe to write
|
|
407
|
-
* to disk and safe to serve from `GET /profiles`.
|
|
408
|
-
*/
|
|
409
135
|
type ProfileStore = {
|
|
410
|
-
|
|
411
|
-
save(profile: ProfileInfo): void | Promise<void>;
|
|
136
|
+
list(): ProfileInfo[] | Promise<ProfileInfo[]>;
|
|
137
|
+
save(profile: ProfileInfo): void | Promise<void>;
|
|
412
138
|
delete(name: string): void | Promise<void>;
|
|
413
139
|
};
|
|
414
|
-
/** Non-durable store for tests and ephemeral deployments. */
|
|
415
140
|
declare function createMemoryProfileStore(seed?: ProfileInfo[]): ProfileStore;
|
|
416
|
-
/**
|
|
417
|
-
* JSON-file store: one array of profiles at `path` (default
|
|
418
|
-
* `<cwd>/.workerdeck/profiles.json`). Writes go through a temp file and a
|
|
419
|
-
* rename so a crash mid-write cannot truncate the operator's profile list.
|
|
420
|
-
*
|
|
421
|
-
* Single-process by design, exactly like the bundled queue adapter — two servers
|
|
422
|
-
* sharing one file would race. That is what the seam is for.
|
|
423
|
-
*/
|
|
424
141
|
declare function createFileProfileStore(path?: string): ProfileStore;
|
|
425
142
|
//#endregion
|
|
426
143
|
//#region src/options.d.ts
|
|
@@ -429,400 +146,108 @@ type SdkSessionLister = (options: {
|
|
|
429
146
|
limit?: number;
|
|
430
147
|
offset?: number;
|
|
431
148
|
}) => Promise<SdkSessionSummary[]>;
|
|
432
|
-
/**
|
|
433
|
-
* Return a principal (any truthy value) to accept the request, or null/undefined to
|
|
434
|
-
* reject with 401. The host app supplies this — the worker has no auth story of its
|
|
435
|
-
* own. A principal object may carry `allowedProfiles: string[]` to restrict which
|
|
436
|
-
* profiles the caller can create sessions/jobs under (and see in GET /profiles) —
|
|
437
|
-
* without it the caller may use every declared profile. It may also carry
|
|
438
|
-
* `canManageProfiles: true` to allow creating/editing/deleting managed profiles
|
|
439
|
-
* (requires the `profileStore` option); anything else means no.
|
|
440
|
-
*
|
|
441
|
-
* It may also carry `scope: Record<string, string>` — the opaque tags deciding
|
|
442
|
-
* which *sessions* this caller may see at all (see
|
|
443
|
-
* {@link WorkerServerOptions.authorizeSession}). A principal carrying a scope is
|
|
444
|
-
* an embedded end user rather than the operator, and is refused the
|
|
445
|
-
* operator-privileged surfaces outright: `/fs/*`, `/sdk-sessions`, `/queue` and
|
|
446
|
-
* `/queue/ws`.
|
|
447
|
-
*
|
|
448
|
-
* **This is the place to be expensive.** It is already async and already runs
|
|
449
|
-
* once per request, so a lookup (which spaces is this user in?) belongs here,
|
|
450
|
-
* landing its answer on the principal. The visibility check itself is
|
|
451
|
-
* synchronous by design: it runs on every route and every row of every list.
|
|
452
|
-
*/
|
|
453
149
|
type Authenticator = (req: IncomingMessage) => unknown | Promise<unknown>;
|
|
454
150
|
type WorkerServerOptions = {
|
|
455
|
-
|
|
151
|
+
authenticate?: Authenticator;
|
|
456
152
|
allowUnauthenticated?: boolean;
|
|
457
|
-
|
|
458
|
-
* Whether a principal may see one session — the policy half of
|
|
459
|
-
* {@link CreateSessionRequest.scope}. WorkerDeck stores the opaque tags and
|
|
460
|
-
* enforces the answer at every door; what the tags *mean* is the host's, and
|
|
461
|
-
* has to be, because "space" and "user" are one app's vocabulary and the next
|
|
462
|
-
* embedder has tenants or projects or nothing.
|
|
463
|
-
*
|
|
464
|
-
* **Synchronous, deliberately.** It runs per route and per row of every list,
|
|
465
|
-
* so resolving it against a database per request is the failure mode this
|
|
466
|
-
* signature designs out: do the lookup in {@link Authenticator} and put the
|
|
467
|
-
* answer on the principal.
|
|
468
|
-
*
|
|
469
|
-
* Unset, the default rule applies: every key the principal's `scope` pins must
|
|
470
|
-
* equal the session's, and a principal with no scope (`undefined` or `{}`) is
|
|
471
|
-
* unrestricted — the same "unset means all" rule `allowedProfiles` uses, so an
|
|
472
|
-
* operator's dashboard is unaffected. A consequence worth stating: a session
|
|
473
|
-
* carrying *no* scope is invisible to a scoped principal, which is the right
|
|
474
|
-
* fail direction — sessions predating this feature never leak into an
|
|
475
|
-
* end user's list.
|
|
476
|
-
*
|
|
477
|
-
* **False means the session does not exist**: every refusal answers 404, never
|
|
478
|
-
* 403, matching `host-files.ts`' uniform-disclosure discipline. A predicate
|
|
479
|
-
* that *throws* has not said yes — it is caught and read as false, so one
|
|
480
|
-
* surprising row cannot turn a hundred-row list into a page-wide error.
|
|
481
|
-
*
|
|
482
|
-
* **Declaring this withdraws the unscoped-means-operator default.** The
|
|
483
|
-
* gateway-wide surfaces (`/fs/*`, `/sdk-sessions`, `/queue`, `/queue/ws`) key
|
|
484
|
-
* on {@link Authenticator}'s principal carrying no `scope` — but a host may
|
|
485
|
-
* well write this predicate over its own principal shape and never set one,
|
|
486
|
-
* and reading that as "everyone is the operator" would serve the host
|
|
487
|
-
* filesystem to every end user whose sessions this correctly walls off. So
|
|
488
|
-
* with a policy declared, operator principals must say `operator: true`.
|
|
489
|
-
*
|
|
490
|
-
* **True means full control, not read access.** An attach can send
|
|
491
|
-
* `user_message`, `permission_decision`, `interrupt` and `close`, and a
|
|
492
|
-
* bridged client can settle a tool call — so this is one boolean over "may
|
|
493
|
-
* drive this session", not a visibility level. A read-only-for-my-team policy
|
|
494
|
-
* is not expressible here yet; do not approximate it with `readOnly`, which is
|
|
495
|
-
* affordance removal in a client and not an authorization boundary.
|
|
496
|
-
*/
|
|
497
|
-
authorizeSession?: (principal: unknown, session: SessionInfo) => boolean; /** If set, session cwd must resolve inside one of these roots. Strongly recommended. */
|
|
153
|
+
authorizeSession?: (principal: unknown, session: SessionInfo) => boolean;
|
|
498
154
|
allowedCwdRoots?: string[];
|
|
499
|
-
/**
|
|
500
|
-
* The host filesystem routes (`{basePath}/fs/*`) — browse and read the
|
|
501
|
-
* operator's real project tree, and optionally write to it.
|
|
502
|
-
*
|
|
503
|
-
* **Reading follows {@link allowedCwdRoots} and needs no grant of its own.**
|
|
504
|
-
* A caller holding the auth key can already start a session in any allowed root
|
|
505
|
-
* and have the agent read whatever is in it, so serving those same trees over
|
|
506
|
-
* `/fs` adds no authority — it only removes the absurdity of going through a
|
|
507
|
-
* language model to `cat` a file. Set `roots` here only to *narrow* that (or to
|
|
508
|
-
* expose a tree sessions may not run in).
|
|
509
|
-
*
|
|
510
|
-
* With neither set the routes 404. That is not the same as inheriting
|
|
511
|
-
* `allowedCwdRoots`' permissive "unset means anywhere": no cwd policy means
|
|
512
|
-
* there is nothing to inherit, and "anywhere" is a statement about paths the
|
|
513
|
-
* operator types at a keyboard, not one about what a phone may read.
|
|
514
|
-
*
|
|
515
|
-
* **Writing is a separate opt-in**, because it is the one part that is not
|
|
516
|
-
* already implied. An agent's writes go through the permission flow; a `PUT` to
|
|
517
|
-
* `/fs/write` does not. These routes are operator-privileged by design — the
|
|
518
|
-
* caller is the operator — but that is a reason to make the bypass deliberate,
|
|
519
|
-
* not a reason to skip the switch.
|
|
520
|
-
*
|
|
521
|
-
* Containment is *not* `cwdAllowed`, whichever roots are in play: these routes
|
|
522
|
-
* walk paths the agent may have authored, so a symlink can escape a lexical
|
|
523
|
-
* prefix check. See `host-files.ts` — canonicalize, then re-check.
|
|
524
|
-
*/
|
|
525
155
|
hostFiles?: {
|
|
526
|
-
|
|
527
|
-
* array disables the routes (a policy, not an absence). */
|
|
528
|
-
roots?: string[]; /** Enable `PUT {basePath}/fs/write`. Default false — read-only. */
|
|
156
|
+
roots?: string[];
|
|
529
157
|
write?: boolean;
|
|
530
|
-
/** Refuse reads above this (413) rather than streaming a gigabyte to a phone.
|
|
531
|
-
* Default 1 MiB. Writes are bounded by {@link maxBodyBytes} instead. */
|
|
532
158
|
maxFileBytes?: number;
|
|
533
|
-
/** Cap on entries returned per directory (the response says `truncated`).
|
|
534
|
-
* Default 5000. */
|
|
535
159
|
maxEntries?: number;
|
|
536
|
-
/** Directory names `GET /fs/find` will not descend into. Defaults to
|
|
537
|
-
* `DEFAULT_IGNORED_DIRS` (`.git`, `node_modules`, build output…) — the thing
|
|
538
|
-
* that keeps a per-keystroke search cheap on a real source tree. */
|
|
539
160
|
ignore?: string[];
|
|
540
161
|
};
|
|
541
|
-
/**
|
|
542
|
-
* Message attachments (`{basePath}/sessions/:id/attachments`) — the photos and
|
|
543
|
-
* files a client sends alongside a message. Always on; these knobs only size it.
|
|
544
|
-
*
|
|
545
|
-
* There is no grant to make here the way `hostFiles.write` is one: an upload
|
|
546
|
-
* lands in the session's own in-memory hold and reaches the model as message
|
|
547
|
-
* content, which is exactly what typing does. What it *can* do is cost memory,
|
|
548
|
-
* so both caps default low enough that a phone camera roll cannot fill the
|
|
549
|
-
* gateway.
|
|
550
|
-
*/
|
|
551
162
|
attachments?: {
|
|
552
|
-
|
|
163
|
+
maxFileBytes?: number;
|
|
553
164
|
maxSessionBytes?: number;
|
|
554
165
|
};
|
|
555
|
-
/**
|
|
556
|
-
* Named Claude Code config directories sessions can run under (each becomes the
|
|
557
|
-
* session's CLAUDE_CONFIG_DIR — settings, memory, skills, and the credentials the
|
|
558
|
-
* SDK resolves from it). Declared here at startup; the API only reads them
|
|
559
|
-
* (GET {basePath}/profiles). With more than one declared, every session/job create
|
|
560
|
-
* must name its profile; with exactly one it is implicit. Unset: a 'default'
|
|
561
|
-
* profile is auto-created from $CLAUDE_CONFIG_DIR or ~/.claude when that directory
|
|
562
|
-
* exists. Pass [] to run without profiles (no env pinning at all).
|
|
563
|
-
*/
|
|
564
166
|
profiles?: ProfileInfo[];
|
|
565
|
-
/**
|
|
566
|
-
* Persistence for dashboard-managed profiles, which mounts the profile
|
|
567
|
-
* management routes (`POST /profiles`, `PATCH`/`DELETE /profiles/:name`).
|
|
568
|
-
* Without it the profile set is startup config and the API stays read-only.
|
|
569
|
-
*
|
|
570
|
-
* Profiles declared in `profiles` are never stored and never editable over
|
|
571
|
-
* HTTP — they are code. The two sets are unioned by name, declared winning.
|
|
572
|
-
* Callers still need `canManageProfiles` on their principal.
|
|
573
|
-
*/
|
|
574
167
|
profileStore?: ProfileStore;
|
|
575
|
-
/**
|
|
576
|
-
* Config-dir roots a *managed* Claude profile's `configDir` must resolve inside
|
|
577
|
-
* (mirrors {@link allowedCwdRoots}). Unset — the default — means the management
|
|
578
|
-
* routes create provider profiles only: naming a config directory is choosing
|
|
579
|
-
* which credential store a session runs on, so it stays operator-bounded.
|
|
580
|
-
* Declared profiles are unaffected.
|
|
581
|
-
*/
|
|
582
168
|
allowedConfigDirRoots?: string[];
|
|
583
|
-
|
|
584
|
-
* env, tool policy, per-skill constraints...). Defaults to identity.
|
|
585
|
-
*
|
|
586
|
-
* What this hook injects is **not** durable: a session rebuilt from a parked
|
|
587
|
-
* record is built from the stored config, and a durable store persists neither
|
|
588
|
-
* `env` nor injected functions (see `toDurableRecord` in session-store.ts). That costs the
|
|
589
|
-
* Claude engine nothing, since it cannot park — but a provider host that
|
|
590
|
-
* resolves credentials into `config.env` here for its `createEngineRunner` to
|
|
591
|
-
* read back loses them on the wake. Resolve them in the factory instead. */
|
|
592
|
-
buildRunnerConfig?: (req: CreateSessionRequest) => SessionRunnerConfig; /** URL prefix for all routes. Default '/v1'. */
|
|
169
|
+
buildRunnerConfig?: (req: CreateSessionRequest) => SessionRunnerConfig;
|
|
593
170
|
basePath?: string;
|
|
594
|
-
/**
|
|
595
|
-
* Handle requests that fall outside `basePath` instead of 404ing them. The
|
|
596
|
-
* turnkey CLI serves the dashboard through this, which is the whole reason it
|
|
597
|
-
* exists: a browser cannot put a header on a WebSocket handshake, so the only
|
|
598
|
-
* credential a tab can present on a session attach is a cookie — and a cookie
|
|
599
|
-
* only rides requests to the origin that set it. Serving the app and the API
|
|
600
|
-
* from one origin is therefore not a convenience, it is what makes an
|
|
601
|
-
* authenticated dashboard possible without a stamping proxy in front.
|
|
602
|
-
*
|
|
603
|
-
* Upgrades are not routed here: anything outside `basePath` is still refused.
|
|
604
|
-
*/
|
|
605
171
|
fallback?: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;
|
|
606
|
-
/**
|
|
607
|
-
* Browser origins allowed to call this API cross-origin — for a dashboard
|
|
608
|
-
* served somewhere other than this gateway.
|
|
609
|
-
*
|
|
610
|
-
* Off unless configured, and even then it is *sharing policy, not a
|
|
611
|
-
* credential*: preflights are answered before auth (browsers strip
|
|
612
|
-
* credentials from them, so they would otherwise 401), but every real request
|
|
613
|
-
* still goes through `authenticate`, and an allowlisted page that does not
|
|
614
|
-
* hold the key gets nothing.
|
|
615
|
-
*
|
|
616
|
-
* Two rules the implementation must keep: **exact origins only**, no
|
|
617
|
-
* wildcards or suffix matching; and `Access-Control-Allow-Credentials` is
|
|
618
|
-
* **never** sent, which is what keeps an ambient cookie from becoming
|
|
619
|
-
* cross-origin authority. WebSocket upgrades are exempt from CORS entirely
|
|
620
|
-
* and are unaffected by this — their credential is whatever the host's
|
|
621
|
-
* `authenticate` accepts on the handshake.
|
|
622
|
-
*/
|
|
623
172
|
cors?: {
|
|
624
173
|
origins: string[];
|
|
625
|
-
};
|
|
174
|
+
};
|
|
626
175
|
maxBodyBytes?: number;
|
|
627
|
-
/**
|
|
628
|
-
* Server-wide bypass policy: refuse `permissionMode: 'bypassPermissions'` on
|
|
629
|
-
* session/job creation (403), and strip the `allowDangerouslySkipPermissions`
|
|
630
|
-
* pre-authorization from requests (so clients that ask for the capability by
|
|
631
|
-
* default keep working — their later switch attempt fails with the CLI's own
|
|
632
|
-
* visible error instead). Mirrors Claude Code's
|
|
633
|
-
* `permissions.disableBypassPermissionsMode` setting, enforced at the gateway.
|
|
634
|
-
*/
|
|
635
176
|
disableBypassPermissions?: boolean;
|
|
636
|
-
/**
|
|
637
|
-
* Fail closed on subscription credentials: if a session initializes with
|
|
638
|
-
* `apiKeySource: 'oauth'` (a claude.ai login rather than an API key / Bedrock / Vertex),
|
|
639
|
-
* it is terminated with a session_error. Recommended for services and any
|
|
640
|
-
* unattended/scheduled use — Anthropic's terms require API-key auth for those.
|
|
641
|
-
* Off by default: single-user personal deployments may legitimately run on the
|
|
642
|
-
* operator's own subscription; the server then logs a one-time notice instead.
|
|
643
|
-
*/
|
|
644
177
|
requireApiKey?: boolean;
|
|
645
|
-
/**
|
|
646
|
-
* Launch-time credential sanity check: once `listen()` binds, each Claude
|
|
647
|
-
* profile's session environment — exactly what `buildRunnerConfig` would hand
|
|
648
|
-
* a session, host hook included — is probed with the SDK-bundled CLI's
|
|
649
|
-
* `claude auth status`, concurrently and fire-and-forget, and a profile that
|
|
650
|
-
* reports logged-out gets one console warning. Warn, never fail: the operator
|
|
651
|
-
* may be about to log in, and a probe that cannot run at all (missing binary,
|
|
652
|
-
* a CLI without `auth status`, unparseable output) stays silent — "couldn't
|
|
653
|
-
* check" is not "not logged in". No credential material is read or logged.
|
|
654
|
-
* Off by default (this is a library; tests must spawn nothing) — the turnkey
|
|
655
|
-
* CLI turns it on. Pass an object to inject the probe (tests) or a timeout.
|
|
656
|
-
*/
|
|
657
178
|
checkCredentials?: boolean | {
|
|
658
179
|
probe?: ClaudeAuthProbe;
|
|
659
180
|
timeoutMs?: number;
|
|
660
181
|
};
|
|
661
|
-
/**
|
|
662
|
-
* Refuse to create a session or submit a job on a profile the credential
|
|
663
|
-
* probe has reported **unavailable** — 503 with the probe's own reason —
|
|
664
|
-
* rather than letting the run start and die mid-turn on a raw provider error.
|
|
665
|
-
*
|
|
666
|
-
* Off by default, and that default is right for an operator's own gateway:
|
|
667
|
-
* the verdict can be stale in both directions, the operator may be three
|
|
668
|
-
* seconds from finishing a login, and turning a probe bug into an outage is
|
|
669
|
-
* worse than one confusing failure. It is wrong in front of an **end user**,
|
|
670
|
-
* who cannot read a provider stack trace and did not choose the deployment's
|
|
671
|
-
* credentials — which is why every embedder otherwise grows its own
|
|
672
|
-
* `available` flag in front of the create button.
|
|
673
|
-
*
|
|
674
|
-
* Requires `checkCredentials`; without probes nothing is ever unavailable.
|
|
675
|
-
* A profile whose verdict is 'unknown' (never probed, probe couldn't run) is
|
|
676
|
-
* always allowed through — "couldn't check" is not "not available".
|
|
677
|
-
*/
|
|
678
182
|
requireAvailableProfile?: boolean;
|
|
679
|
-
/** Injectable lister for GET /sdk-sessions (tests) — honored for the CLAUDE
|
|
680
|
-
* engine only, like the injectable claude auth probe (it predates the adapter
|
|
681
|
-
* layer). Defaults to the claude adapter's lister (the SDK's on-disk session
|
|
682
|
-
* store); other engines always answer through their adapter's
|
|
683
|
-
* `listSessions`. */
|
|
684
183
|
listSdkSessions?: SdkSessionLister;
|
|
685
|
-
/** Enable the job queue (`/jobs` + `/queue` routes). Jobs run as ordinary registry
|
|
686
|
-
* sessions — attachable over the sessions WS — governed by these limits. */
|
|
687
184
|
queue?: QueueServerOptions;
|
|
688
|
-
/**
|
|
689
|
-
* Out-of-band notification for interactive sessions: the four moments a person
|
|
690
|
-
* away from the screen needs (permission requested, turn done, error, closed)
|
|
691
|
-
* POSTed to a webhook and/or handed to a local observer. Off unless configured.
|
|
692
|
-
*
|
|
693
|
-
* Server-wide, unlike the queue's per-job webhook — the point is to hear about
|
|
694
|
-
* sessions you neither created nor are attached to, which is the situation a
|
|
695
|
-
* mobile client is in permanently (iOS will not hold a WebSocket open in the
|
|
696
|
-
* background). Every registry session qualifies, job runs included, so a job
|
|
697
|
-
* carrying its own webhook is reported on both channels.
|
|
698
|
-
*
|
|
699
|
-
* This is the primitive, and it stays transport-agnostic on purpose: the OSS
|
|
700
|
-
* server holds no push credentials. Turning a notification into an APNs push is
|
|
701
|
-
* a forwarder's job — see the turnkey CLI.
|
|
702
|
-
*/
|
|
703
185
|
notifications?: SessionNotificationOptions;
|
|
704
|
-
/** Browser-bridged tool execution: how long a bridged call may go unanswered
|
|
705
|
-
* before it fails (default 60000), and where terminal results are delivered.
|
|
706
|
-
* The hub is always available on the returned server as `bridge`. */
|
|
707
186
|
bridge?: BridgeHubOptions;
|
|
708
|
-
/**
|
|
709
|
-
* Deferred execution: a session that parks on an execution nothing here is
|
|
710
|
-
* running has its state persisted and its runner torn down, and comes back when
|
|
711
|
-
* the result is POSTed to `{basePath}/executions/:executionId/result`.
|
|
712
|
-
*
|
|
713
|
-
* On by default with an in-memory store, so a park survives a disconnect but not
|
|
714
|
-
* a restart; pass `store: createFileSessionStore()` (or your own) to change that
|
|
715
|
-
* — read its doc first, the record holds the whole transcript.
|
|
716
|
-
*/
|
|
717
187
|
parking?: {
|
|
718
|
-
store?: SessionStore;
|
|
188
|
+
store?: SessionStore;
|
|
719
189
|
parkDelayMs?: number;
|
|
720
|
-
/** Grace given on boot to an execution whose deadline passed while the server
|
|
721
|
-
* was down (durable stores only — nothing else survives a restart). Default 60000. */
|
|
722
190
|
expiredGraceMs?: number;
|
|
723
|
-
/**
|
|
724
|
-
* Keep live `provider` sessions written through to the store after each
|
|
725
|
-
* turn, so they survive a gateway restart. **Off by default** — this writes
|
|
726
|
-
* a session's whole transcript to `store` and a library must not start doing
|
|
727
|
-
* that because someone upgraded.
|
|
728
|
-
*
|
|
729
|
-
* It is the restart story for the one engine dormancy cannot cover: claude
|
|
730
|
-
* and codex are remembered by *engine session id* and resumed from their own
|
|
731
|
-
* on-disk store, which a provider session does not have. Pair it with a
|
|
732
|
-
* durable `store` — with the default in-memory one it does nothing a park
|
|
733
|
-
* did not already do.
|
|
734
|
-
*
|
|
735
|
-
* The record is rebuilt lazily, on first attach, exactly like a dormant one;
|
|
736
|
-
* a boot with fifty remembered sessions spawns nothing.
|
|
737
|
-
*/
|
|
738
191
|
persistLive?: boolean;
|
|
739
|
-
|
|
740
|
-
* session errors. 'remember' is the write that lets a live session survive a
|
|
741
|
-
* restart; losing one costs that session its way back and nothing else. */
|
|
742
|
-
onError?: (error: unknown, context: {
|
|
743
|
-
sessionId: string;
|
|
744
|
-
phase: 'park' | 'remember' | 'resume';
|
|
745
|
-
}) => void;
|
|
192
|
+
onError?: (error: unknown, context: ParkErrorContext) => void;
|
|
746
193
|
};
|
|
747
|
-
/**
|
|
748
|
-
* Build a runner for a `provider` profile (the model-agnostic engine).
|
|
749
|
-
* Required if any such profile is declared — the server refuses to start
|
|
750
|
-
* otherwise, rather than failing at create time.
|
|
751
|
-
*
|
|
752
|
-
* Kept as a host hook so the server package neither imports a model SDK nor
|
|
753
|
-
* decides how provider credentials are resolved: the factory reads them from
|
|
754
|
-
* the operator's environment, exactly like the Claude credential chain.
|
|
755
|
-
* `claude` and `codex` profiles never come through here — those engines ship
|
|
756
|
-
* as in-repo adapters (`@workerdeck/core`'s `getEngineAdapter`).
|
|
757
|
-
*
|
|
758
|
-
* May be async: assembly that has to await — a per-session MCP connect, a
|
|
759
|
-
* credential lookup — belongs here, with `AiSdkRunnerConfig.onClose` as the
|
|
760
|
-
* disposer. A rejection fails the create — the session POST answers 500 with
|
|
761
|
-
* the message, a job goes straight to `failed`.
|
|
762
|
-
*/
|
|
763
194
|
createEngineRunner?: (context: EngineRunnerContext) => Runner | Promise<Runner>;
|
|
764
|
-
/**
|
|
765
|
-
* Adapter overrides, keyed by engine — **for tests only** (the server
|
|
766
|
-
* integration suite injects a fake codex engine so `pnpm test` spawns no
|
|
767
|
-
* binary). Not a public extension point: third engines belong in core as
|
|
768
|
-
* adapters, or behind `createEngineRunner` as provider profiles.
|
|
769
|
-
*/
|
|
770
195
|
engines?: Partial<Record<ProfileEngine, EngineAdapter>>;
|
|
771
196
|
};
|
|
772
197
|
type EngineRunnerContext = {
|
|
773
|
-
|
|
198
|
+
config: SessionRunnerConfig;
|
|
774
199
|
profile: ProfileInfo;
|
|
775
|
-
/** Bridge hub, for handing the runner a browser-backed ToolExecutor
|
|
776
|
-
* (`bridge.executorFor(sessionId)`). */
|
|
777
200
|
bridge: BridgeHub;
|
|
778
|
-
/**
|
|
779
|
-
* Set when rebuilding a session that parked on a deferred execution. Forward it
|
|
780
|
-
* as `restore` on the engine config (`createEngineSession({ config: { ...config,
|
|
781
|
-
* restore } })`) — the engine then adopts the session's id, event log, seq
|
|
782
|
-
* numbering, history, and scratch filesystem instead of starting fresh.
|
|
783
|
-
*/
|
|
784
201
|
restore?: RunnerSnapshot;
|
|
785
|
-
/**
|
|
786
|
-
* Set when rehydrating a session across a gateway restart: build the runner
|
|
787
|
-
* under exactly this id rather than a fresh one. Never set together with
|
|
788
|
-
* `restore` (a snapshot carries its own id). Ignoring it strands every
|
|
789
|
-
* client's watermarks and routes, and the rebuild is refused.
|
|
790
|
-
*/
|
|
791
202
|
id?: string;
|
|
792
203
|
};
|
|
793
204
|
type QueueServerOptions = {
|
|
794
|
-
|
|
795
|
-
sessionTokenLimit?: number;
|
|
796
|
-
dailyTokenLimit?: number;
|
|
797
|
-
maxJobDurationMs?: number;
|
|
205
|
+
maxConcurrency?: number;
|
|
206
|
+
sessionTokenLimit?: number;
|
|
207
|
+
dailyTokenLimit?: number;
|
|
208
|
+
maxJobDurationMs?: number;
|
|
798
209
|
killGraceMs?: number;
|
|
799
|
-
/** Expire terminal jobs after `maxAgeMs` (the in-memory adapter otherwise grows
|
|
800
|
-
* unboundedly). */
|
|
801
210
|
retention?: {
|
|
802
211
|
maxAgeMs: number;
|
|
803
212
|
sweepIntervalMs?: number;
|
|
804
213
|
};
|
|
805
|
-
|
|
806
|
-
* no persistence) — redis/bullmq/pubsub adapters implement the same interface. */
|
|
807
|
-
adapter?: QueueAdapter; /** Webhook delivery attempts per event (default 3, exponential backoff). */
|
|
214
|
+
adapter?: QueueAdapter;
|
|
808
215
|
webhookAttempts?: number;
|
|
809
|
-
webhookRetryDelayMs?: number;
|
|
216
|
+
webhookRetryDelayMs?: number;
|
|
810
217
|
onEvent?: (event: JobEvent) => void;
|
|
811
218
|
};
|
|
219
|
+
/** A point-in-time answer to "is it safe to stop yet?", as reported while draining. */
|
|
220
|
+
type DrainReport = {
|
|
221
|
+
/** Sessions mid-turn. These resolve on their own, so the drain waits for them. */
|
|
222
|
+
working: string[];
|
|
223
|
+
/**
|
|
224
|
+
* Sessions blocked on a human — a pending approval. The drain names these but never waits for them: nothing about
|
|
225
|
+
* shutting down will answer the prompt, so waiting is a hang with better manners.
|
|
226
|
+
*/
|
|
227
|
+
awaitingHuman: string[];
|
|
228
|
+
/** True when the deadline passed with work still running. */
|
|
229
|
+
timedOut: boolean;
|
|
230
|
+
};
|
|
231
|
+
type DrainOptions = {
|
|
232
|
+
/** Overall budget. The drain gives up and reports rather than blocking shutdown forever. */
|
|
233
|
+
timeoutMs?: number;
|
|
234
|
+
pollMs?: number;
|
|
235
|
+
onProgress?: (report: DrainReport) => void;
|
|
236
|
+
};
|
|
812
237
|
type WorkerServer = {
|
|
813
238
|
server: Server;
|
|
814
|
-
registry: SessionRegistry;
|
|
239
|
+
registry: SessionRegistry;
|
|
815
240
|
queue?: JobQueue;
|
|
816
|
-
/** Routes tool executions to attached browser clients. `bridge.executorFor(id)`
|
|
817
|
-
* is the `ToolExecutor` to hand a runner that should execute in the tab. */
|
|
818
241
|
bridge: BridgeHub;
|
|
819
|
-
/** Parked sessions: the store, the execution index, and the rehydration path.
|
|
820
|
-
* Deliver a deferred result with `parking.submitResult(...)` in-process, or POST
|
|
821
|
-
* it to `{basePath}/executions/:executionId/result`. */
|
|
822
242
|
parking: SessionParkManager;
|
|
823
243
|
listen: (port: number, host?: string) => Promise<{
|
|
824
244
|
port: number;
|
|
825
245
|
}>;
|
|
246
|
+
/**
|
|
247
|
+
* Let running turns finish before `close()`. A courtesy, never a correctness requirement: records are written
|
|
248
|
+
* continuously, so a hard stop already loses nothing. Refuses new sessions for as long as it runs.
|
|
249
|
+
*/
|
|
250
|
+
drain: (options?: DrainOptions) => Promise<DrainReport>;
|
|
826
251
|
close: () => Promise<void>;
|
|
827
252
|
};
|
|
828
253
|
//#endregion
|
|
@@ -830,147 +255,40 @@ type WorkerServer = {
|
|
|
830
255
|
declare function createWorkerServer(options?: WorkerServerOptions): WorkerServer;
|
|
831
256
|
//#endregion
|
|
832
257
|
//#region src/lib/sandboxed-profile.d.ts
|
|
833
|
-
/**
|
|
834
|
-
* A `provider` profile that grants a session nothing but the sandbox: the
|
|
835
|
-
* QuickJS guest, the in-memory VFS, and the model.
|
|
836
|
-
*
|
|
837
|
-
* This adds no mechanism. `capabilities: []` and `mcpServers: []` already mean
|
|
838
|
-
* what they mean, and `createToolContext` already withholds a tool whose backend
|
|
839
|
-
* the host did not inject. What the helper buys is that the locked-down profile
|
|
840
|
-
* is one call rather than three fields an operator has to get right together —
|
|
841
|
-
* the failure mode being a profile that *looks* sandboxed and still grants
|
|
842
|
-
* `deliver_file` because nobody wrote the empty array.
|
|
843
|
-
*
|
|
844
|
-
* What a session under it can do:
|
|
845
|
-
* - run untrusted JavaScript in the WASM guest, under the interpreter's own
|
|
846
|
-
* timeout and memory limits (`eval_script`),
|
|
847
|
-
* - read and write the session's in-memory VFS, which is a map and not a
|
|
848
|
-
* filesystem — no host path is reachable from it.
|
|
849
|
-
*
|
|
850
|
-
* What it cannot do: read or write a host path, spawn a process, reach the
|
|
851
|
-
* network (`web_fetch`/`download`/`web_search` are capabilities, and none is
|
|
852
|
-
* granted), deliver a file, or use an MCP server.
|
|
853
|
-
*
|
|
854
|
-
* Two things this helper does **not** do, because they are not a profile's to
|
|
855
|
-
* decide. It does not authorize anyone — visibility is
|
|
856
|
-
* `CreateSessionRequest.scope` plus the gateway's `authorizeSession`. And it
|
|
857
|
-
* does not make the model's *input* trustworthy: content the loop reads is
|
|
858
|
-
* attacker-influenced by default, and a sandbox bounds what a tool can reach,
|
|
859
|
-
* not what a prompt can talk the model into asking for.
|
|
860
|
-
*
|
|
861
|
-
* @param name Profile name clients name in `CreateSessionRequest.profile`.
|
|
862
|
-
* @param provider Which model to run (credentials stay in the operator's
|
|
863
|
-
* environment and are resolved by the host's `createEngineRunner` — never
|
|
864
|
-
* here, and never on the wire).
|
|
865
|
-
*/
|
|
866
258
|
declare function sandboxedProviderProfile(name: string, provider: ProviderConfig, options?: {
|
|
867
|
-
description?: string;
|
|
259
|
+
description?: string;
|
|
868
260
|
instructions?: string;
|
|
869
|
-
/** Profile-level run defaults (model, permission mode) — see
|
|
870
|
-
* {@link ProfileInfo.defaults}. */
|
|
871
261
|
defaults?: ProfileInfo['defaults'];
|
|
872
|
-
/**
|
|
873
|
-
* Capabilities to grant on top of the floor. Default `[]` — the floor is
|
|
874
|
-
* nothing, and every entry here is a deliberate widening you are writing
|
|
875
|
-
* down: `web_fetch` gives the loop egress (SSRF-guarded, but egress),
|
|
876
|
-
* `download` and `web_search` reach whatever backends you injected, and
|
|
877
|
-
* `deliver_file` lets it hand a file to the client.
|
|
878
|
-
*/
|
|
879
262
|
capabilities?: SessionCapability[];
|
|
880
|
-
/**
|
|
881
|
-
* MCP servers, **by name**, whose tools sessions may use. Default `[]`.
|
|
882
|
-
* MCP tools are authoritative — they run with the host's credentials and
|
|
883
|
-
* are never bridged — so naming one here is a larger grant than any
|
|
884
|
-
* capability above it.
|
|
885
|
-
*/
|
|
886
263
|
mcpServers?: string[];
|
|
887
264
|
}): ProfileInfo;
|
|
888
265
|
//#endregion
|
|
889
266
|
//#region src/lib/provider-runner.d.ts
|
|
890
267
|
type ProviderRunnerOptions = {
|
|
891
|
-
/**
|
|
892
|
-
* The model to run. A function is called per turn with the session's
|
|
893
|
-
* requested model id (undefined = the profile's default), which is what makes
|
|
894
|
-
* the in-session model switcher work; a bare instance pins one model.
|
|
895
|
-
*/
|
|
896
268
|
model: LanguageModel | ((modelId: string | undefined) => LanguageModel);
|
|
897
|
-
/**
|
|
898
|
-
* Where sandboxed tools (`eval_script` and any `sandboxed` entry in `tools`)
|
|
899
|
-
* execute. This is a real architectural choice, not a default worth guessing
|
|
900
|
-
* at, so it is required:
|
|
901
|
-
*
|
|
902
|
-
* - a {@link ToolExecutor} — an in-process guest (`new QuickJsExecutor(...)`
|
|
903
|
-
* from `@workerdeck/core`), which is right when the data the loop reasons
|
|
904
|
-
* over lives in this process. It is also the only option that works when no
|
|
905
|
-
* client is attached, which is every unattended job.
|
|
906
|
-
* - `'browser'` — the attached tab, resolved per call from the bridge. Right
|
|
907
|
-
* when the data is *there* (a document the user is editing) and it should
|
|
908
|
-
* not travel to the gateway at all. Note the trade: it hands an executor to
|
|
909
|
-
* the party being sandboxed against, so its results are untrusted input.
|
|
910
|
-
* - a **function** — selects per call, so `eval_script` can run in-process
|
|
911
|
-
* while a custom tool goes to the browser. The function receives the
|
|
912
|
-
* {@link ToolExecutionCall} and returns an executor or `'browser'`.
|
|
913
|
-
*/
|
|
914
269
|
executor: ToolExecutor | 'browser' | ((call: ToolExecutionCall) => ToolExecutor | 'browser');
|
|
915
|
-
|
|
916
|
-
* Wiring one only offers it; the profile and request decide the grant. */
|
|
917
|
-
capabilities?: EngineSessionOptions['capabilities']; /** Host tools at explicit trust levels (`@workerdeck/core`'s `withHostTools`). */
|
|
270
|
+
capabilities?: EngineSessionOptions['capabilities'];
|
|
918
271
|
tools?: Record<string, HostToolDefinition>;
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
mcp?: McpConnection; /** A bare MCP tool set, for a host assembling one itself. */
|
|
923
|
-
mcpTools?: ToolSet; /** System-prompt addition, unless the profile declares its own. */
|
|
924
|
-
instructions?: string; /** Sandbox limits per execution. */
|
|
272
|
+
mcp?: McpConnection;
|
|
273
|
+
mcpTools?: ToolSet;
|
|
274
|
+
instructions?: string;
|
|
925
275
|
executionLimits?: {
|
|
926
276
|
timeoutMs?: number;
|
|
927
277
|
memoryLimitBytes?: number;
|
|
928
278
|
};
|
|
929
|
-
/** Scratch-filesystem seed for a new session. Ignored on a rehydration, so a
|
|
930
|
-
* parked turn's files are never overwritten. */
|
|
931
279
|
seedVfs?: Record<string, string>;
|
|
932
|
-
/**
|
|
933
|
-
* Gate tool execution behind user approval. See
|
|
934
|
-
* {@link EngineSessionOptions.shouldApprove} — this is a straight pass-through.
|
|
935
|
-
*/
|
|
936
280
|
shouldApprove?: (call: {
|
|
937
281
|
toolName: string;
|
|
938
282
|
input: unknown;
|
|
939
|
-
}) => boolean;
|
|
283
|
+
}) => boolean;
|
|
940
284
|
approvalTimeoutMs?: number;
|
|
941
|
-
/** Release per-session resources: the MCP connection, an issued token, a
|
|
942
|
-
* watcher. Runs on close **and on park** — parking releases the same things. */
|
|
943
285
|
onClose?: () => void | Promise<void>;
|
|
944
286
|
};
|
|
945
|
-
/**
|
|
946
|
-
* Build a provider-engine runner from the server's `createEngineRunner` context.
|
|
947
|
-
*
|
|
948
|
-
* `createEngineRunner` is a blank sheet: it hands you a context and wants a
|
|
949
|
-
* `Runner`, and four of the five things a correct one must do are invisible in
|
|
950
|
-
* the types — forward `restore`, adopt `id`, seed the VFS only when *not*
|
|
951
|
-
* restoring, and dispose per-session resources. Each is a runtime-only failure
|
|
952
|
-
* (a woken session that starts empty, a refused rebuild, an overwritten
|
|
953
|
-
* filesystem, a connection leaked per session), and each is handled here.
|
|
954
|
-
*
|
|
955
|
-
* ```ts
|
|
956
|
-
* createEngineRunner: (ctx) =>
|
|
957
|
-
* createProviderRunner(ctx, {
|
|
958
|
-
* model: (id) => openai(id ?? 'gpt-5.6-luna'),
|
|
959
|
-
* executor: quickjs,
|
|
960
|
-
* capabilities: { webFetch: {} },
|
|
961
|
-
* mcp,
|
|
962
|
-
* onClose: () => mcp.close(),
|
|
963
|
-
* }),
|
|
964
|
-
* ```
|
|
965
|
-
*
|
|
966
|
-
* The hook itself stays open for anything this does not cover — this is the
|
|
967
|
-
* 80% case, not a replacement for it.
|
|
968
|
-
*/
|
|
969
287
|
declare function createProviderRunner(ctx: EngineRunnerContext, options: ProviderRunnerOptions): Promise<Runner>;
|
|
970
288
|
//#endregion
|
|
971
289
|
//#region src/services/attachments.d.ts
|
|
972
290
|
type AttachmentStoreOptions = {
|
|
973
|
-
|
|
291
|
+
maxFileBytes?: number;
|
|
974
292
|
maxSessionBytes?: number;
|
|
975
293
|
};
|
|
976
294
|
type AttachmentRejection = {
|
|
@@ -993,35 +311,12 @@ type PutResult = {
|
|
|
993
311
|
ok: false;
|
|
994
312
|
error: AttachmentRejection;
|
|
995
313
|
};
|
|
996
|
-
/**
|
|
997
|
-
* Per-session hold for files the user attached to a message.
|
|
998
|
-
*
|
|
999
|
-
* In memory, and deliberately so. An attachment is only *needed* for the instant
|
|
1000
|
-
* between the upload and the message that names it; everything after that is
|
|
1001
|
-
* convenience (a client re-rendering a thumbnail after a reattach). That is the
|
|
1002
|
-
* same bargain `GET /sessions/:id/files` makes — the session's lifetime, no
|
|
1003
|
-
* durability tier — and it keeps the gateway from accumulating a photo library
|
|
1004
|
-
* on disk that nobody asked it to look after.
|
|
1005
|
-
*
|
|
1006
|
-
* Both caps are enforced here rather than at the route, so a host embedding the
|
|
1007
|
-
* server cannot forget one: a single file that is too big is a 413, and so is a
|
|
1008
|
-
* session whose total would go over.
|
|
1009
|
-
*/
|
|
1010
314
|
declare class AttachmentStore {
|
|
1011
315
|
#private;
|
|
1012
316
|
constructor(options?: AttachmentStoreOptions);
|
|
1013
317
|
get maxFileBytes(): number;
|
|
1014
318
|
put(sessionId: string, name: string, mediaType: string, body: Buffer): PutResult;
|
|
1015
|
-
/** The stored record, bytes included — for the download route and for the send
|
|
1016
|
-
* path that turns ids into content blocks. */
|
|
1017
319
|
get(sessionId: string, id: string): AttachmentInput | undefined;
|
|
1018
|
-
/**
|
|
1019
|
-
* Resolve the ids a `user_message` named, in the order given.
|
|
1020
|
-
*
|
|
1021
|
-
* Missing ids are reported rather than skipped: a message that quietly lost its
|
|
1022
|
-
* picture reads as the model ignoring it, which is a far worse failure than a
|
|
1023
|
-
* command that errors.
|
|
1024
|
-
*/
|
|
1025
320
|
resolve(sessionId: string, ids: readonly string[]): {
|
|
1026
321
|
ok: true;
|
|
1027
322
|
attachments: AttachmentInput[];
|
|
@@ -1033,101 +328,25 @@ declare class AttachmentStore {
|
|
|
1033
328
|
}
|
|
1034
329
|
//#endregion
|
|
1035
330
|
//#region src/services/produced-files.d.ts
|
|
1036
|
-
/** One host file an engine reported writing, as the store holds it. */
|
|
1037
331
|
type ProducedFile = {
|
|
1038
|
-
fileId: string;
|
|
332
|
+
fileId: string;
|
|
1039
333
|
path: string;
|
|
1040
|
-
mediaType?: string;
|
|
334
|
+
mediaType?: string;
|
|
1041
335
|
bytes?: number;
|
|
1042
336
|
sessionId: string;
|
|
1043
337
|
};
|
|
1044
|
-
/**
|
|
1045
|
-
* The paths this gateway will serve from `GET /sessions/:id/produced/:fileId`.
|
|
1046
|
-
*
|
|
1047
|
-
* **This is the whole access-control model, so it is worth being precise about
|
|
1048
|
-
* what it is.** The store is an allowlist built from one source and one only:
|
|
1049
|
-
* `file_produced` events, which a runner emits about a file its own engine just
|
|
1050
|
-
* wrote. It is not a directory grant. Nothing else can add to it — not a
|
|
1051
|
-
* request, not a config, and in particular not the agent, whose own path claims
|
|
1052
|
-
* go through `/fs/*` and that route's root allowlist.
|
|
1053
|
-
*
|
|
1054
|
-
* That is why the route needs neither `hostFiles.roots` nor `maxFileBytes`:
|
|
1055
|
-
* "somewhere under a root the operator declared" is a guess about which paths
|
|
1056
|
-
* are safe, while "the exact path this session's runner reported producing" is
|
|
1057
|
-
* a fact about one file. A 2 MB generated PNG is the common case, and making
|
|
1058
|
-
* the operator raise a byte cap to see their own picture was the bug this
|
|
1059
|
-
* replaces.
|
|
1060
|
-
*
|
|
1061
|
-
* Lifetime is the session's, like `AttachmentStore`'s: in memory, dropped when
|
|
1062
|
-
* the session is removed. The bytes are never held here — only the path, so a
|
|
1063
|
-
* gateway serving a long session accumulates a few hundred bytes per picture
|
|
1064
|
-
* rather than the pictures.
|
|
1065
|
-
*/
|
|
1066
338
|
declare class ProducedFileStore {
|
|
1067
339
|
#private;
|
|
1068
|
-
/**
|
|
1069
|
-
* Register a runner's produced files for its lifetime.
|
|
1070
|
-
*
|
|
1071
|
-
* Subscribes from seq 0, which is the opposite of what `SessionNotifier` wants
|
|
1072
|
-
* and correct for the same reason: registration is idempotent (a `fileId` is
|
|
1073
|
-
* derived from its path, so re-registering overwrites with itself), and a
|
|
1074
|
-
* session rebuilt from a park must re-learn every file it produced before the
|
|
1075
|
-
* park — otherwise a client's transcript keeps rendering image cards whose
|
|
1076
|
-
* bytes have quietly become unreachable.
|
|
1077
|
-
*/
|
|
1078
340
|
watch(runner: Runner): void;
|
|
1079
341
|
get(sessionId: string, fileId: string): ProducedFile | undefined;
|
|
1080
|
-
/** Everything one session has produced, newest registration last. */
|
|
1081
342
|
list(sessionId: string): ProducedFile[];
|
|
1082
343
|
drop(sessionId: string): void;
|
|
1083
344
|
}
|
|
1084
345
|
//#endregion
|
|
1085
346
|
//#region src/services/profile-usage.d.ts
|
|
1086
|
-
/**
|
|
1087
|
-
* The gateway's single plan-usage state per profile, fed from every session's
|
|
1088
|
-
* `rate_limit` events and served on `GET /profiles` (`ProfileInfo.usage`).
|
|
1089
|
-
*
|
|
1090
|
-
* Why this exists at all: usage had only ever lived in session transcripts, so
|
|
1091
|
-
* a client attaching to a session that idled since yesterday replayed
|
|
1092
|
-
* yesterday's reading as if current — and a session opened today knew nothing
|
|
1093
|
-
* of what a sibling session on the same account spent an hour ago. The profile
|
|
1094
|
-
* is the account boundary (one config dir / codex home / provider key = one
|
|
1095
|
-
* plan), so the newest reading across all of a profile's sessions is the one
|
|
1096
|
-
* usage state that is ever worth showing. No history: last-write-wins per
|
|
1097
|
-
* window, exactly the reducer's rule on the client side.
|
|
1098
|
-
*
|
|
1099
|
-
* Last-write-wins goes by the **event's own clock**, not arrival order:
|
|
1100
|
-
* `watch()` subscribes from seq 0 (a replayed log is how a rebuilt session's
|
|
1101
|
-
* readings arrive at all), and a replayed yesterday-reading must not clobber
|
|
1102
|
-
* the fresher one another session on the same profile reported live. All
|
|
1103
|
-
* events are stamped by this gateway's clock at emit time, so the comparison
|
|
1104
|
-
* is sound across sessions.
|
|
1105
|
-
*
|
|
1106
|
-
* In-memory on purpose, like the learned default models and the availability
|
|
1107
|
-
* cache: display-only state may start empty after a restart (absent = unknown,
|
|
1108
|
-
* never 0%), and the first session to report refills it.
|
|
1109
|
-
*/
|
|
1110
347
|
declare class ProfileUsageTracker {
|
|
1111
348
|
#private;
|
|
1112
|
-
/** Follow a runner's `rate_limit` events for its lifetime. Sessions without a
|
|
1113
|
-
* profile have no account to attribute usage to and are skipped. */
|
|
1114
349
|
watch(runner: Runner): void;
|
|
1115
|
-
/**
|
|
1116
|
-
* The profile's windows as they should be served *now*. Undefined until any
|
|
1117
|
-
* session on the profile has reported (unknown, never 0%).
|
|
1118
|
-
*
|
|
1119
|
-
* The 0%-after-reset inference lives here — at serve time — and nowhere
|
|
1120
|
-
* else, because it is a function of the wall clock: a window whose own
|
|
1121
|
-
* `resetsAt` has passed with no newer reading has provably rolled, so the
|
|
1122
|
-
* pre-reset utilization is no longer merely stale but *wrong*. It cannot be
|
|
1123
|
-
* a producer's job (the producers only relay what the engine said, and the
|
|
1124
|
-
* whole problem is the engine's silence; a fabricated 0% event would be
|
|
1125
|
-
* replayed from transcripts forever as if reported) and must not be every
|
|
1126
|
-
* renderer's (N clients would each reimplement the clock math). The held
|
|
1127
|
-
* reading stays untouched, so a late fresh report still lands by ts, and the
|
|
1128
|
-
* served zero is labeled `inferredReset` — it is a floor, not a report: the
|
|
1129
|
-
* account may have been used outside this gateway since the reset.
|
|
1130
|
-
*/
|
|
1131
350
|
usage(profile: string, now?: number): ProfileUsage | undefined;
|
|
1132
351
|
}
|
|
1133
352
|
//#endregion
|