@workerdeck/server 0.15.0 → 0.17.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/README.md CHANGED
@@ -208,6 +208,16 @@ OAuth, never reads or forwards tokens — see the repo README's
208
208
  VFS only when *not* restoring, and dispose per-session resources via `onClose`. Every one is a
209
209
  runtime-only failure. `createProviderRunner()` does all four; reach for the raw hook only when it
210
210
  genuinely doesn't fit.
211
+ - **`parking.persistLive` is how a *provider* session survives a restart, and it needs a durable
212
+ store to mean anything.** Claude and codex go dormant — remembered by engine session id and
213
+ resumed from the engine's own store — which a provider session cannot do, so its record carries
214
+ the state itself, written through after every turn. With the default in-memory store the option
215
+ does nothing and says nothing. It is off by default: a library must not start writing sessions'
216
+ transcripts to disk because someone upgraded, and the record holds the whole transcript in
217
+ plaintext.
218
+ - **A restored session is refreshed in place, not consumed.** A park's record *is* the session and
219
+ is deleted on wake; a live or dormant one is a way back the session still needs next time. If you
220
+ implement a `SessionStore`, do not "tidy up" a record on read.
211
221
  - **One origin is not a convenience.** A browser cannot put an `Authorization` header on a
212
222
  WebSocket upgrade, so a cookie is the only credential a tab can present on an attach, and a
213
223
  cookie is per-origin. That is what `fallback` is for — an app served from the gateway's own port.
package/build/index.d.mts CHANGED
@@ -1,40 +1,45 @@
1
1
  import { IncomingMessage, Server, ServerResponse } from "node:http";
2
2
  import { AttachmentInput, BridgeAnswer, BrowserBridgeExecutor, ClaudeAuthProbe, EngineAdapter, EngineSessionOptions, HostToolDefinition, LanguageModel, McpConnection, ParkedExecution, Runner, RunnerSnapshot, SessionRunnerConfig, ToolExecutionResult, ToolExecutor, ToolSet } from "@workerdeck/core";
3
3
  import { JobQueue, QueueAdapter } from "@workerdeck/queue";
4
- import { CreateSessionRequest, JobEvent, MessageAttachment, ProfileEngine, ProfileInfo, ProviderConfig, SdkSessionSummary, ServerFrame, SessionCapability, SessionInfo, SessionNotification, SessionWebhookConfig } from "@workerdeck/protocol";
4
+ import { CreateSessionRequest, JobEvent, MessageAttachment, ProfileEngine, ProfileInfo, ProfileUsage, ProviderConfig, SdkSessionSummary, ServerFrame, SessionCapability, SessionInfo, SessionNotification, SessionWebhookConfig } from "@workerdeck/protocol";
5
5
 
6
- //#region src/registry.d.ts
7
- type SessionRegistryOptions = {
8
- /**
9
- * Called once per runner as it enters the table, before it starts — the one
10
- * seam every path goes through (create, prepare, adopt, and the rebuild of a
11
- * parked session), which is what a watcher that must not miss a session needs.
12
- */
13
- onRegister?: (runner: Runner) => void;
6
+ //#region src/services/bridge.d.ts
7
+ type BridgeHubOptions = {
8
+ /** How long a bridged call may stay unanswered before it fails. Default 60000. */timeoutMs?: number;
9
+ /** Called when a bridged execution reaches a terminal result — the host feeds
10
+ * it back into the runner's loop. */
11
+ onResult?: (sessionId: string, executionId: string, result: ToolExecutionResult) => void;
14
12
  };
15
- /** In-memory session table. Terminal sessions stay listed until removed or the process exits. */
16
- declare class SessionRegistry {
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
+ declare class BridgeHub {
17
23
  #private;
18
- constructor(options?: SessionRegistryOptions);
19
- create(config: SessionRunnerConfig): Runner;
20
- /** Build and list a Claude-engine runner without starting it, so watchers can
21
- * subscribe first. Call `start()` once they have. */
22
- prepare(config: SessionRunnerConfig): Runner;
23
- /** Register an already-built runner (a non-Claude engine) and start it. */
24
- adopt(runner: Runner): Runner;
25
- /** List a runner without starting it — for a rehydrated session, whose watchers
26
- * must be subscribed before it comes back up. */
27
- register(runner: Runner): Runner;
28
- get(id: string): Runner | undefined;
29
- list(): SessionInfo[];
30
- remove(id: string): boolean;
31
- /** Drop a runner WITHOUT closing it: the session isn't ending, it parked and
32
- * lives on in its snapshot. Closing here would tell every client it was over. */
33
- evict(id: string): boolean;
34
- closeAll(): void;
24
+ 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
+ executorFor(sessionId: string): BrowserBridgeExecutor;
28
+ /** How many clients are watching this session. Parking consults it: a session
29
+ * someone is watching stays live. */
30
+ attachedCount(sessionId: string): number;
31
+ /** Register an attached client. Returns a detach function. */
32
+ 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
+ resolve(sessionId: string, executionId: string, answer: BridgeAnswer): boolean;
38
+ /** Drop a session's bridge, failing anything still in flight. */
39
+ remove(sessionId: string): void;
35
40
  }
36
41
  //#endregion
37
- //#region src/notifications.d.ts
42
+ //#region src/services/notifications.d.ts
38
43
  type SessionNotificationOptions = {
39
44
  /** POST target for every notification. */webhook?: SessionWebhookConfig;
40
45
  /** Local observer, invoked for every notification whether or not a webhook is
@@ -43,6 +48,11 @@ type SessionNotificationOptions = {
43
48
  onNotification?: (notification: SessionNotification) => void; /** Delivery attempts per notification (exponential backoff). Default 3. */
44
49
  attempts?: number; /** Initial backoff between attempts. Default 500ms. */
45
50
  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
+ decorateInfo?: (info: SessionInfo) => SessionInfo;
46
56
  };
47
57
  /**
48
58
  * Turns session events into the handful of notifications a human away from the
@@ -73,58 +83,74 @@ declare class SessionNotifier {
73
83
  watch(runner: Runner, afterSeq?: number): void;
74
84
  }
75
85
  //#endregion
76
- //#region src/bridge.d.ts
77
- type BridgeHubOptions = {
78
- /** How long a bridged call may stay unanswered before it fails. Default 60000. */timeoutMs?: number;
79
- /** Called when a bridged execution reaches a terminal result — the host feeds
80
- * it back into the runner's loop. */
81
- onResult?: (sessionId: string, executionId: string, result: ToolExecutionResult) => void;
82
- };
83
- /**
84
- * Routes tool executions between a session and the browser tabs attached to it.
85
- *
86
- * A session may have several clients attached (dashboard plus embedded panel);
87
- * the bridge asks the **first attached** one, which is the closest thing to "the
88
- * client driving this session". If none is attached, dispatch fails fast rather
89
- * than hanging — an autonomous job simply never bridges, it uses the server
90
- * executor instead.
91
- */
92
- declare class BridgeHub {
93
- #private;
94
- constructor(options?: BridgeHubOptions);
95
- /** The executor to hand a runner for this session. Created on first use and
96
- * reused, so results routed back always reach the same pending table. */
97
- executorFor(sessionId: string): BrowserBridgeExecutor;
98
- /** How many clients are watching this session. Parking consults it: a session
99
- * someone is watching stays live. */
100
- attachedCount(sessionId: string): number;
101
- /** Register an attached client. Returns a detach function. */
102
- attach(sessionId: string, send: (frame: ServerFrame) => void): () => void;
86
+ //#region src/services/registry.d.ts
87
+ type SessionRegistryOptions = {
103
88
  /**
104
- * Deliver a client's answer to a bridged call. Returns false when the id is
105
- * unknown or already settled late and duplicate answers are ignored.
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.
106
92
  */
107
- resolve(sessionId: string, executionId: string, answer: BridgeAnswer): boolean;
108
- /** Drop a session's bridge, failing anything still in flight. */
109
- remove(sessionId: string): void;
93
+ onRegister?: (runner: Runner) => void;
94
+ };
95
+ /** In-memory session table. Terminal sessions stay listed until removed or the process exits. */
96
+ declare class SessionRegistry {
97
+ #private;
98
+ constructor(options?: SessionRegistryOptions);
99
+ 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
+ prepare(config: SessionRunnerConfig): Runner;
103
+ /** Register an already-built runner (a non-Claude engine) and start it. */
104
+ 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
+ register(runner: Runner): Runner;
108
+ get(id: string): Runner | undefined;
109
+ list(): SessionInfo[];
110
+ 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
+ evict(id: string): boolean;
114
+ closeAll(): void;
110
115
  }
111
116
  //#endregion
112
- //#region src/session-store.d.ts
117
+ //#region src/services/session-store.d.ts
113
118
  /**
114
- * A session with its live runner torn down, waiting on deferred executions.
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.
115
121
  *
116
- * Everything needed to bring it back: the wire-visible info (so it still lists and
117
- * reads over REST while parked), the config to rebuild the runner, the engine's
118
- * snapshot, and what it is waiting for.
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.
119
141
  */
120
142
  type ParkedSessionRecord = {
121
- /** Absent on records written before dormant sessions existed — those are all parked. */kind?: 'parked';
122
- id: string; /** Session info as of the park, with `status: 'parked'`. */
143
+ /** Absent on records written before dormant sessions existed — those are all parked. */kind?: 'parked' | 'live';
144
+ 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. */
123
147
  info: SessionInfo;
124
148
  profile?: string; /** The config the session was created with (profile defaults already applied). */
125
149
  config: SessionRunnerConfig;
126
150
  snapshot: RunnerSnapshot;
127
- executions: ParkedExecution[];
151
+ /** Empty for a live record: an idle session is waiting on nothing, so there is
152
+ * nothing for `hydrate` to arm a watchdog for. */
153
+ executions: ParkedExecution[]; /** When it was written. Named for the park that came first. */
128
154
  parkedAt: number;
129
155
  };
130
156
  /**
@@ -228,7 +254,7 @@ type FileSessionStoreOptions = {
228
254
  */
229
255
  declare function createFileSessionStore(options?: FileSessionStoreOptions): SessionStore;
230
256
  //#endregion
231
- //#region src/parking.d.ts
257
+ //#region src/services/parking.d.ts
232
258
  type SessionParkOptions = {
233
259
  registry: SessionRegistry;
234
260
  store: SessionStore;
@@ -246,6 +272,24 @@ type SessionParkOptions = {
246
272
  /** Wait this long after the last client detaches before parking, so a reconnect
247
273
  * (a wifi blip, a page reload) doesn't cost a teardown. Default 2000. */
248
274
  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
+ persistLive?: boolean;
249
293
  /** Grace given at {@link SessionParkManager.hydrate} to an execution whose
250
294
  * deadline passed while the server was down. Its result could not have been
251
295
  * delivered during the outage, so failing it the instant the process is back
@@ -290,6 +334,18 @@ declare class SessionParkManager {
290
334
  /** Record the config a session was created with. Only sessions the host
291
335
  * remembers can be parked — there is no way to rebuild the others. */
292
336
  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
+ touch(runner: Runner): void;
293
349
  /**
294
350
  * Adopt the store's contents (a durable store after a restart): re-index the
295
351
  * executions and re-arm their watchdogs, no deadline sooner than the grace
@@ -335,7 +391,7 @@ declare class SessionParkManager {
335
391
  close(): void;
336
392
  }
337
393
  //#endregion
338
- //#region src/profile-store.d.ts
394
+ //#region src/services/profile-store.d.ts
339
395
  /**
340
396
  * Where dashboard-managed profiles live. The seam exists for the same reason
341
397
  * `QueueAdapter` does: a single-host deployment wants the bundled file store and
@@ -367,7 +423,7 @@ declare function createMemoryProfileStore(seed?: ProfileInfo[]): ProfileStore;
367
423
  */
368
424
  declare function createFileProfileStore(path?: string): ProfileStore;
369
425
  //#endregion
370
- //#region src/server.d.ts
426
+ //#region src/options.d.ts
371
427
  type SdkSessionLister = (options: {
372
428
  dir?: string;
373
429
  limit?: number;
@@ -664,6 +720,22 @@ type WorkerServerOptions = {
664
720
  /** Grace given on boot to an execution whose deadline passed while the server
665
721
  * was down (durable stores only — nothing else survives a restart). Default 60000. */
666
722
  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
+ persistLive?: boolean;
667
739
  /** Park/remember/resume failures — storage or engine-assembly problems, not
668
740
  * session errors. 'remember' is the write that lets a live session survive a
669
741
  * restart; losing one costs that session its way back and nothing else. */
@@ -753,9 +825,11 @@ type WorkerServer = {
753
825
  }>;
754
826
  close: () => Promise<void>;
755
827
  };
828
+ //#endregion
829
+ //#region src/server.d.ts
756
830
  declare function createWorkerServer(options?: WorkerServerOptions): WorkerServer;
757
831
  //#endregion
758
- //#region src/sandboxed-profile.d.ts
832
+ //#region src/lib/sandboxed-profile.d.ts
759
833
  /**
760
834
  * A `provider` profile that grants a session nothing but the sandbox: the
761
835
  * QuickJS guest, the in-memory VFS, and the model.
@@ -812,7 +886,7 @@ declare function sandboxedProviderProfile(name: string, provider: ProviderConfig
812
886
  mcpServers?: string[];
813
887
  }): ProfileInfo;
814
888
  //#endregion
815
- //#region src/provider-runner.d.ts
889
+ //#region src/lib/provider-runner.d.ts
816
890
  type ProviderRunnerOptions = {
817
891
  /**
818
892
  * The model to run. A function is called per turn with the session's
@@ -882,7 +956,7 @@ type ProviderRunnerOptions = {
882
956
  */
883
957
  declare function createProviderRunner(ctx: EngineRunnerContext, options: ProviderRunnerOptions): Promise<Runner>;
884
958
  //#endregion
885
- //#region src/attachments.d.ts
959
+ //#region src/services/attachments.d.ts
886
960
  type AttachmentStoreOptions = {
887
961
  /** Largest single upload. Default 10 MiB. */maxFileBytes?: number; /** Ceiling on everything one session is holding. Default 64 MiB. */
888
962
  maxSessionBytes?: number;
@@ -946,7 +1020,7 @@ declare class AttachmentStore {
946
1020
  drop(sessionId: string): void;
947
1021
  }
948
1022
  //#endregion
949
- //#region src/produced-files.d.ts
1023
+ //#region src/services/produced-files.d.ts
950
1024
  /** One host file an engine reported writing, as the store holds it. */
951
1025
  type ProducedFile = {
952
1026
  fileId: string; /** Absolute host path, exactly as the runner reported it. */
@@ -996,5 +1070,54 @@ declare class ProducedFileStore {
996
1070
  drop(sessionId: string): void;
997
1071
  }
998
1072
  //#endregion
999
- export { AttachmentStore, type AttachmentStoreOptions, type Authenticator, BridgeHub, type BridgeHubOptions, type EngineRunnerContext, type FileSessionStoreOptions, MemorySessionStore, type ParkedSessionRecord, type ProducedFile, ProducedFileStore, type ProfileStore, type ProviderRunnerOptions, type QueueServerOptions, type SdkSessionLister, type SessionNotificationOptions, SessionNotifier, SessionParkManager, type SessionParkOptions, SessionRegistry, type SessionRegistryOptions, type SessionStore, type WorkerServer, type WorkerServerOptions, createFileProfileStore, createFileSessionStore, createMemoryProfileStore, createProviderRunner, createWorkerServer, sandboxedProviderProfile, toDurableRecord };
1073
+ //#region src/services/profile-usage.d.ts
1074
+ /**
1075
+ * The gateway's single plan-usage state per profile, fed from every session's
1076
+ * `rate_limit` events and served on `GET /profiles` (`ProfileInfo.usage`).
1077
+ *
1078
+ * Why this exists at all: usage had only ever lived in session transcripts, so
1079
+ * a client attaching to a session that idled since yesterday replayed
1080
+ * yesterday's reading as if current — and a session opened today knew nothing
1081
+ * of what a sibling session on the same account spent an hour ago. The profile
1082
+ * is the account boundary (one config dir / codex home / provider key = one
1083
+ * plan), so the newest reading across all of a profile's sessions is the one
1084
+ * usage state that is ever worth showing. No history: last-write-wins per
1085
+ * window, exactly the reducer's rule on the client side.
1086
+ *
1087
+ * Last-write-wins goes by the **event's own clock**, not arrival order:
1088
+ * `watch()` subscribes from seq 0 (a replayed log is how a rebuilt session's
1089
+ * readings arrive at all), and a replayed yesterday-reading must not clobber
1090
+ * the fresher one another session on the same profile reported live. All
1091
+ * events are stamped by this gateway's clock at emit time, so the comparison
1092
+ * is sound across sessions.
1093
+ *
1094
+ * In-memory on purpose, like the learned default models and the availability
1095
+ * cache: display-only state may start empty after a restart (absent = unknown,
1096
+ * never 0%), and the first session to report refills it.
1097
+ */
1098
+ declare class ProfileUsageTracker {
1099
+ #private;
1100
+ /** Follow a runner's `rate_limit` events for its lifetime. Sessions without a
1101
+ * profile have no account to attribute usage to and are skipped. */
1102
+ watch(runner: Runner): void;
1103
+ /**
1104
+ * The profile's windows as they should be served *now*. Undefined until any
1105
+ * session on the profile has reported (unknown, never 0%).
1106
+ *
1107
+ * The 0%-after-reset inference lives here — at serve time — and nowhere
1108
+ * else, because it is a function of the wall clock: a window whose own
1109
+ * `resetsAt` has passed with no newer reading has provably rolled, so the
1110
+ * pre-reset utilization is no longer merely stale but *wrong*. It cannot be
1111
+ * a producer's job (the producers only relay what the engine said, and the
1112
+ * whole problem is the engine's silence; a fabricated 0% event would be
1113
+ * replayed from transcripts forever as if reported) and must not be every
1114
+ * renderer's (N clients would each reimplement the clock math). The held
1115
+ * reading stays untouched, so a late fresh report still lands by ts, and the
1116
+ * served zero is labeled `inferredReset` — it is a floor, not a report: the
1117
+ * account may have been used outside this gateway since the reset.
1118
+ */
1119
+ usage(profile: string, now?: number): ProfileUsage | undefined;
1120
+ }
1121
+ //#endregion
1122
+ export { AttachmentStore, type AttachmentStoreOptions, type Authenticator, BridgeHub, type BridgeHubOptions, type EngineRunnerContext, type FileSessionStoreOptions, MemorySessionStore, type ParkedSessionRecord, type ProducedFile, ProducedFileStore, type ProfileStore, ProfileUsageTracker, type ProviderRunnerOptions, type QueueServerOptions, type SdkSessionLister, type SessionNotificationOptions, SessionNotifier, SessionParkManager, type SessionParkOptions, SessionRegistry, type SessionRegistryOptions, type SessionStore, type WorkerServer, type WorkerServerOptions, createFileProfileStore, createFileSessionStore, createMemoryProfileStore, createProviderRunner, createWorkerServer, sandboxedProviderProfile, toDurableRecord };
1000
1123
  //# sourceMappingURL=index.d.mts.map