@workerdeck/protocol 0.23.0 → 1.0.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 CHANGED
@@ -1,120 +1,46 @@
1
1
  //#region src/session-list.d.ts
2
- /**
3
- * How a sessions list is filtered, grouped and sorted — the whole of the view
4
- * config, kept pure and separate from the components so every surface renders
5
- * one derived list and nothing else decides what is visible.
6
- *
7
- * Framework-free like `transcript.ts`, and for the same reason: more than one
8
- * party has to agree. In the VS Code extension the webview renders the list and
9
- * the extension host counts the activity-bar badge over the same rows (a badge
10
- * that ignored the filter would announce work in sessions the list is
11
- * deliberately hiding); in the dashboard the list and its subset line derive
12
- * from it; on iOS it is mirrored to Swift the way the reducer is.
13
- *
14
- * Sessions are shown across ALL gateways by default; the gateway is a facet like
15
- * any other, not the frame the list lives in.
16
- */
17
- /** Coarse lifecycle bucket — what a person actually filters on. Raw statuses are
18
- * too many and too engine-shaped ('starting' vs 'running' is not a decision). */
19
2
  type SessionState = 'attention' | 'working' | 'idle' | 'ended';
20
3
  declare const STATE_ORDER: readonly SessionState[];
21
4
  declare const STATE_LABELS: Record<SessionState, string>;
22
5
  declare function sessionState(info: SessionInfo): SessionState;
23
- /**
24
- * The sub-agents a list row draws as live.
25
- *
26
- * `sessionState` deliberately does **not** grow a `subagents` bucket — a fifth
27
- * state would split `working` in two for every client that filters by it,
28
- * including the ones that have not shipped this yet. Instead `working` *counts*
29
- * them: a synchronous `Task` keeps the turn in flight so the status already
30
- * says `working`, and a **background** agent — which outlives its turn on
31
- * purpose — is the carve-out the extra arm in `sessionState` exists for.
32
- * That is what makes "sub-agents are an annotation on a working row" true
33
- * rather than assumed: the row is in the working bucket whichever kind is
34
- * running, and this list only says more about it.
35
- */
36
6
  declare function runningSubagents(info: SessionInfo): SubagentInfo[];
37
- /**
38
- * A sub-agent's identity on one line: `Explore · find the auth check`.
39
- *
40
- * The same two fields `taskLabel` builds its transcript row from, minus the
41
- * `Task(…)` wrapper — a list row is already inside a session, so naming the tool
42
- * spends the width that the description needs. Falls back to the bare agent type,
43
- * then to a generic word: a row with no label at all reads as a rendering bug,
44
- * and an engine is free to send neither field.
45
- */
46
- /**
47
- * Does this record name an **agent**, as opposed to a task the model merely
48
- * described?
49
- *
50
- * The tracker opens a record for every spawner call and for any nested event
51
- * whose parent it has not seen, so the list holds two different things wearing
52
- * one shape. One carries a `subagent_type` — a delegated agent with an identity
53
- * (`Explore`), whose own work is worth a surface of its own. The other carries
54
- * only a description, and there is no agent there to open: a row that offered a
55
- * screen and then showed a frame with nothing in it would be worse than a row
56
- * that offered nothing.
57
- *
58
- * Here rather than in a client because it decides two things a list must not
59
- * disagree about across surfaces — what is pressable, and what wears the
60
- * sub-agent colour.
61
- */
62
7
  declare function isAgentRecord(sub: SubagentInfo): boolean;
63
8
  declare function subagentLabel(sub: SubagentInfo): string;
64
- /** The facets a session can be grouped or sorted by. */
65
9
  type Facet = 'gateway' | 'adapter' | 'state' | 'project';
66
10
  type GroupBy = 'none' | Facet;
67
11
  type SortBy = 'recent' | 'name' | Facet;
68
12
  type ViewConfig = {
69
- search: string; /** Empty = no filter. Ids, not names: names are editable. */
13
+ search: string;
70
14
  gateways: string[];
71
15
  adapters: string[];
72
16
  states: SessionState[];
73
- /**
74
- * Empty = no filter. Keys are {@link projectKey} output — never names, which
75
- * are neither unique (two repos both called "api") nor stable (editing
76
- * `.workerdeck.json` renames every session at once and must not empty a
77
- * saved filter). Optional, unlike its three siblings, because stored view
78
- * configs predate it: a config restored from `localStorage`/`globalState`
79
- * without the key must keep filtering, so absent and empty mean the same
80
- * thing.
81
- */
82
17
  projects?: string[];
83
- /** Show only sessions inside the host's own folders. Inert where there is no
84
- * such notion (no folder open, a dashboard with no workspace), which is why it
85
- * can default on. */
86
18
  scoped: boolean;
87
19
  groupBy: GroupBy;
88
20
  sortBy: SortBy;
89
21
  };
90
22
  declare const DEFAULT_VIEW_CONFIG: ViewConfig;
91
- /**
92
- * One folder the surrounding host has open, as a place sessions can live in.
93
- *
94
- * `hostId` present = the folder belongs to exactly that gateway. Absent = a real
95
- * local folder, which only a loopback gateway's cwds can be inside: a remote
96
- * gateway's paths are on another machine, where an identical-looking path means
97
- * nothing.
98
- */
99
23
  type ScopeRoot = {
100
24
  hostId?: string;
101
25
  path: string;
102
26
  };
103
- /** The host's own folders — the sessions list's intrinsic scope. */
104
27
  type WorkspaceScope = {
105
28
  label: string;
106
29
  roots: ScopeRoot[];
107
30
  };
108
- /** A session with everything the list needs to filter, group and label it. */
109
31
  type SessionRow = {
110
32
  hostId: string;
111
- hostName: string; /** Its gateway is loopback — its cwds are paths on this machine. */
33
+ hostName: string;
112
34
  local: boolean;
113
35
  adapter: string;
114
36
  state: SessionState;
115
37
  info: SessionInfo;
116
- /** Transcript rows since this session was last on screen. 0 = nothing new (or
117
- * never visited, which is not the same as unread). */
38
+ /**
39
+ * Messages this client has not read — `unseenCount` against its watermark, so the
40
+ * unit is prose (`SessionInfo.proseCount`) wherever the gateway reports it. A session
41
+ * that is only running tools contributes 0: the badge answers "is there something to
42
+ * read", not "is anything happening", which is what `state` is for.
43
+ */
118
44
  unseen: number;
119
45
  };
120
46
  type SessionGroup = {
@@ -122,300 +48,88 @@ type SessionGroup = {
122
48
  label?: string;
123
49
  rows: SessionRow[];
124
50
  };
125
- /** The adapters actually present, for the filter chips — derived rather than
126
- * enumerated, so a new engine needs no change here. */
127
51
  declare function adaptersOf(rows: readonly SessionRow[]): string[];
128
- /**
129
- * The projects actually present, as `{ key, label }` for a filter control —
130
- * derived like {@link adaptersOf}, and paired because the two halves differ:
131
- * the *key* is what {@link ViewConfig.projects} holds (gateway-qualified root,
132
- * so a rename regroups nothing) and the *label* is what a person picks by.
133
- *
134
- * Sorted by label, deduped by key. Two projects with the same name on two
135
- * gateways therefore stay two entries wearing one word — which is honest: they
136
- * really are two different directories, and the alternative is a filter that
137
- * silently selects both.
138
- */
139
52
  declare function projectsOf(rows: readonly SessionRow[]): {
140
53
  key: string;
141
54
  label: string;
142
55
  }[];
143
56
  declare function sessionLabel(info: SessionInfo): string;
144
- /**
145
- * The project facet's grouping key: gateway id + the project root, falling
146
- * back to the session's cwd when no project is declared.
147
- *
148
- * The root and not the name, because a name is not a key (two repos can both
149
- * be called "api", and a rename must regroup nothing); qualified by gateway,
150
- * because a remote gateway's identical-looking path is another machine's
151
- * directory — the same rule `ScopeRoot` states. The cwd fallback is what makes
152
- * grouping by project useful before anyone has written a `.workerdeck.json`:
153
- * undeclared sessions group by their folder, declared ones by their root, and
154
- * a session in `packages/ui` joins its repo's group the moment the file
155
- * exists. Sessions with no cwd at all (a filesystem-less engine) share one
156
- * per-gateway bucket — see {@link projectLabel}.
157
- */
158
57
  declare function projectKey(row: SessionRow): string;
159
- /**
160
- * What a project group (or a row's project slot) is called: the declared name,
161
- * else the cwd's basename — the exact string clients rendered before this
162
- * feature existed, so an undeclared project looks like today. 'No project' is
163
- * only ever the no-cwd case (a sandboxed provider session), where there is no
164
- * folder to name.
165
- *
166
- * Takes only the `info` it reads, so a surface holding a bare `SessionInfo` —
167
- * a row component, an iOS cell — can call it without inventing the rest of a
168
- * `SessionRow`. That matters more than it looks: this string is what a client
169
- * renders *in place of* the cwd basename it used to draw, and two spellings of
170
- * it would put the list and its group headers on different names.
171
- */
172
58
  declare function projectLabel(row: Pick<SessionRow, 'info'>): string;
173
- /**
174
- * Where inside its project a session actually sits — the cwd with the project
175
- * root taken off the front, or `undefined` when it sits at the root, has no
176
- * declared project, or has no cwd at all.
177
- *
178
- * The companion to {@link projectLabel}, and it exists for one situation: a list
179
- * **grouped by project**. There the header has already said the project's name,
180
- * so repeating it on every row spends the row's most valuable line on the one
181
- * fact the reader already has. What the header cannot say is which *part* of the
182
- * project a session is working in, and two sessions in the same repo are told
183
- * apart by exactly that.
184
- *
185
- * Undefined is the honest answer for a session at the project root, and callers
186
- * must render nothing rather than a `.` or a repeated name — the slot simply
187
- * goes away, which is the point.
188
- */
189
59
  declare function projectSubpath(row: Pick<SessionRow, 'info'>): string | undefined;
190
- /**
191
- * This session is a job run — the queue created it, and `JobInfo.sessionId`
192
- * points at it.
193
- *
194
- * A job run is an ordinary registry session in every other respect, which is
195
- * what makes this worth spelling once: a client that renders jobs on their own
196
- * surface should not list them again among the sessions, and a client with no
197
- * jobs surface (the extension, the phone) should, or they would be invisible.
198
- * The queue stamps `meta.jobId`; nothing else may write that key.
199
- */
200
60
  declare function isJobRun(info: SessionInfo): boolean;
201
- /**
202
- * Is this session inside one of the host's folders? A gateway-tagged root only
203
- * ever matches its own gateway; an untagged one only matches a loopback gateway,
204
- * because a remote gateway's identical-looking path is a different machine's
205
- * directory.
206
- */
207
61
  declare function inScope(row: SessionRow, scope: WorkspaceScope): boolean;
208
- /** Whether the scope filter is actually hiding anything — it is inert with no
209
- * folder open, and that is the difference between a default and a filter. */
210
62
  declare function scopeActive(config: ViewConfig, scope: WorkspaceScope | undefined): boolean;
211
63
  declare function filterRows(rows: readonly SessionRow[], config: ViewConfig, scope?: WorkspaceScope): SessionRow[];
212
- /**
213
- * The list as rendered: filtered, grouped, and sorted within each group. Groups
214
- * themselves come out in the sort's own order — grouping by state and sorting by
215
- * name should still put "Needs attention" first, so groups are ordered by their
216
- * facet rank, never by the row sort.
217
- */
218
64
  declare function groupRows(rows: readonly SessionRow[], config: ViewConfig): SessionGroup[];
219
- /**
220
- * What the list is hiding, and why — the one "you are seeing a subset" signal.
221
- *
222
- * There used to be two: a dot on the funnel and a scope line above the list.
223
- * They competed (the scope line said one thing, the dot counted a superset of
224
- * it) and neither said how much was missing. This is the single rule both the
225
- * count and the wording come from: absent when nothing is hidden, and otherwise
226
- * naming every cause, so the line is never "12 of 30" with no way to guess why.
227
- *
228
- * Search is a cause like any other. Its box is visible, but the *consequence*
229
- * of it — rows gone from the list — is the thing being reported, and leaving it
230
- * out would make the arithmetic wrong.
231
- */
232
65
  type SubsetSummary = {
233
66
  shown: number;
234
67
  total: number;
235
68
  causes: string[];
236
69
  };
237
70
  declare function subsetSummary(config: ViewConfig, scope: WorkspaceScope | undefined, shown: number, total: number): SubsetSummary | undefined;
238
- /**
239
- * Is anything OTHER than the workspace scope narrowing the list?
240
- *
241
- * The distinction an empty list turns on: "this project has no sessions" wants a
242
- * different sentence, and a different way out, from "your filters match none".
243
- * Scope is excluded because it is on by default — it is the state, not a choice
244
- * someone made.
245
- */
246
71
  declare function hasFacetFilter(config: ViewConfig): boolean;
247
- /** "Show me everything": every filter off, including scope. The group/sort
248
- * choices are a layout preference and survive. */
249
72
  declare function clearFilters(config: ViewConfig): ViewConfig;
250
73
  //#endregion
251
74
  //#region src/usage.d.ts
252
- /**
253
- * What one session was last told about the plan's windows: the transcript's own
254
- * rate-limit state, and the event clock of the newest reading in it.
255
- *
256
- * Deliberately structural rather than `TranscriptState` — protocol may not
257
- * import a client — and it is exactly the two fields the reducer keeps.
258
- */
259
75
  type SessionUsage = {
260
- /** Keyed by `rateLimitType`, as the reducer stores it. */rateLimits?: Record<string, RateLimitInfo>;
261
- /** Epoch ms of the newest `rate_limit` event this session saw — one clock for
262
- * the whole map, which is all the reducer records. */
76
+ rateLimits?: Record<string, RateLimitInfo>;
263
77
  updatedAt?: number;
264
78
  };
265
- /**
266
- * The usage a client should render: the gateway's per-profile state where it has
267
- * the window, this session's own reading where it does not.
268
- *
269
- * Why the profile wins outright rather than by comparing timestamps: the
270
- * gateway's `ProfileUsageTracker` is fed from **every** session on the profile —
271
- * including this one, from seq 0 — and keeps the newest reading per window by
272
- * the event's own `ts`. So for any window it holds, it holds a reading at least
273
- * as new as the one in this transcript, and a timestamp comparison could only
274
- * ever go wrong: the reducer keeps a *single* `updatedAt` for the whole map, so
275
- * a `five_hour` reading from this morning is dated with the afternoon's
276
- * `seven_day` event and would beat a genuinely fresher profile entry.
277
- *
278
- * The session half is not a fallback for correctness but for *coverage*: the
279
- * profile map is in-memory, so a restarted gateway serves nothing until a
280
- * session reports again, and a session with no profile has no account state at
281
- * all. In both cases the transcript's reading is the only one there is, and it
282
- * is dated honestly (see {@link SessionUsage.updatedAt}) rather than as now.
283
- *
284
- * Absent stays absent throughout: a window nobody has reported is **unknown,
285
- * never 0%**, and this returns an empty map rather than inventing entries.
286
- */
287
79
  declare function mergeUsage(session: SessionUsage, profile: ProfileUsage | undefined): ProfileUsage;
288
- /** One window as a surface draws it: the reading, its own date, and whether the
289
- * gateway is the one that zeroed it. */
290
80
  type UsageWindowRow = {
291
81
  key: string;
292
- info: RateLimitInfo; /** Epoch ms of the reading. Absent only for a hand-built state with no clock. */
82
+ info: RateLimitInfo;
293
83
  updatedAt?: number;
294
84
  inferredReset?: boolean;
295
85
  };
296
- /**
297
- * The windows in reading order: the session window, the weekly one, then the
298
- * per-model weeklies alphabetically.
299
- *
300
- * Discovered rather than hardcoded — the engine's set of windows is an open
301
- * union and has grown before — but ordered, so the first two always mean the
302
- * same thing wherever they are drawn. A window with no `utilization` is
303
- * **unknown, not zero**, and is dropped entirely rather than rendered as an
304
- * empty bar that reads as "plenty left".
305
- *
306
- * Here rather than in a client because two surfaces now render the same windows
307
- * from different sources — the session panel from its merged state, the
308
- * dashboard's profile page straight off `ProfileInfo.usage` — and a list that
309
- * ordered or filtered differently would be the same account described two ways.
310
- */
311
86
  declare function orderUsageWindows(usage: ProfileUsage | undefined): UsageWindowRow[];
312
- /** The flat `rateLimitType → reading` map every existing renderer takes, out of
313
- * the dated form. Undefined in, undefined out — so a surface can keep telling
314
- * "no reading" apart from "an empty one". */
315
87
  declare function usageInfos(usage: ProfileUsage | undefined): Record<string, RateLimitInfo> | undefined;
316
88
  //#endregion
317
89
  //#region src/watermarks.d.ts
318
- /**
319
- * "What had you seen, and when" — per session, across reloads.
320
- *
321
- * Two numbers because two surfaces ask different questions. A session list has
322
- * only the REST rollup for sessions it isn't showing, so it compares **rows the
323
- * gateway counted**; a panel has the whole transcript, so it compares **rows it
324
- * rendered** and can put the mark in the right place. Keeping both means neither
325
- * surface has to attach to something it isn't rendering.
326
- *
327
- * A watermark is only written while a session is genuinely on screen. A surface
328
- * nobody can see is not being read, and marking it read is how an unread badge
329
- * silently stops working.
330
- *
331
- * Storage is a seam (`WatermarkStore`) rather than a dependency: the VS Code
332
- * extension backs it with `globalState`, the dashboard with `localStorage`, and
333
- * neither belongs in this package.
334
- */
335
90
  type Watermark = {
336
- /** Transcript rows seen (`SessionVitals.itemCount`). */itemCount: number;
337
- /** Rows the gateway had counted (`SessionInfo.activityCount`) — the same unit
338
- * as `itemCount`, but from the rollup, so it is knowable for a session this
339
- * client is not showing. */
91
+ itemCount: number;
340
92
  activity: number;
341
- /** Completed turns seen. The fallback unit for a gateway too old to report
342
- * `activityCount`; five tool calls in one turn count as one. */
343
- turns: number; /** When this was last true. */
93
+ /**
94
+ * Prose rows read (`SessionInfo.proseCount`). Optional because a mark stored before
95
+ * prose counting existed cannot say — see `unseenCount`, which reads that absence as
96
+ * "caught up" rather than badging a whole history the operator has already seen.
97
+ */
98
+ prose?: number;
99
+ turns: number;
344
100
  seenAt: number;
345
101
  };
346
- /** Where the marks are kept. Reads happen once at construction; writes are
347
- * whole-map and may be async — nothing here awaits them. */
348
102
  type WatermarkStore = {
349
103
  read(): Record<string, Watermark> | undefined;
350
104
  write(marks: Record<string, Watermark>): void;
351
105
  };
352
- declare const watermarkKey: (hostId: string, sessionId: string) => string;
106
+ declare function watermarkKey(hostId: string, sessionId: string): string;
353
107
  declare class Watermarks {
354
108
  #private;
355
109
  constructor(store: WatermarkStore);
356
110
  get(hostId: string, sessionId: string): Watermark | undefined;
357
- /** Every mark, for a caller deriving unread counts over a whole list. */
358
111
  all(): Readonly<Record<string, Watermark>>;
359
- /**
360
- * Record what is on screen now. Monotonic on purpose: a transcript that
361
- * *shrank* (a compaction, a fresh attach mid-replay) must not walk the mark
362
- * backwards and resurrect rows the user already read.
363
- *
364
- * Returns whether the mark actually moved, because an unread badge is computed
365
- * from it and nothing else will say so: rows read in a panel do not touch the
366
- * sessions poll, so a caller that doesn't hear about this has no other way to
367
- * learn the count is now wrong.
368
- */
369
112
  mark(hostId: string, sessionId: string, seen: {
370
113
  itemCount?: number;
371
114
  activity?: number;
115
+ prose?: number;
372
116
  turns?: number;
373
117
  }, now?: number): boolean;
374
- /** Forget a session — it was deleted, and its mark is now noise. */
375
118
  forget(hostId: string, sessionId: string): void;
376
119
  }
377
120
  /**
378
- * Rows this client has not seen, from the rollup alone.
379
- *
380
- * `activityCount` is the unit that makes an honest badge: turns undercount badly
381
- * (five tool calls in one turn is one turn) and a stream sequence overcounts
382
- * absurdly (every delta). Turns stay the fallback for a gateway too old to
383
- * report it.
384
- *
385
- * A session never visited returns 0 — "never opened" is not "unread", and a
386
- * badge that counted every session's whole history on first launch would be
387
- * noise on the one day it should be quiet.
121
+ * The badge's number, in the best unit the pair can agree on: prose the human has not
122
+ * read, else rows, else turns. The ladder is what keeps an older gateway (no `proseCount`
123
+ * on the wire) badging exactly as it did before rather than going silent.
388
124
  */
389
125
  declare function unseenCount(mark: Watermark | undefined, info: {
126
+ proseCount?: number;
390
127
  activityCount?: number;
391
128
  turns?: number;
392
129
  }): number;
393
130
  //#endregion
394
131
  //#region src/index.d.ts
395
- /**
396
- * @workerdeck/protocol — the wire protocol between a workerdeck server and its clients.
397
- *
398
- * One session = one ordered stream of {@link SessionEvent}s (each stamped with a monotonically
399
- * increasing `seq`) plus a small command set ({@link SessionCommand}). Clients attach over
400
- * WebSocket, optionally replaying from a known `seq`, and drive the session with commands.
401
- *
402
- * This package is dependency-free and browser-safe. Anthropic API message content is modeled
403
- * structurally (see {@link ApiMessage}) so clients don't need the Agent SDK to render transcripts.
404
- */
405
- /** Bumped on any breaking change to events, commands, or REST shapes. */
406
- declare const PROTOCOL_VERSION = 7;
407
- /**
408
- * - `starting` — runner spawned, waiting for the SDK init handshake
409
- * - `running` — a turn is in progress
410
- * - `awaiting_approval` — blocked on at least one pending permission request
411
- * - `idle` — between turns; accepting user messages
412
- * - `parked` — waiting on a deferred tool execution. The live runner has been torn
413
- * down and the session's state persisted; delivering the execution's result
414
- * (`POST {basePath}/executions/:executionId/result`) rehydrates it under the same
415
- * id and the run continues. Not terminal.
416
- * - `failed` — the underlying query errored; terminal
417
- * - `closed` — closed by a client or the host; terminal
418
- */
132
+ declare const PROTOCOL_VERSION = 1;
419
133
  type SessionStatus = 'starting' | 'running' | 'awaiting_approval' | 'idle' | 'parked' | 'failed' | 'closed';
420
134
  type PermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | 'dontAsk' | 'auto';
421
135
  type TextBlock = {
@@ -441,192 +155,41 @@ type ToolResultBlock = {
441
155
  [key: string]: unknown;
442
156
  }>;
443
157
  is_error?: boolean;
444
- /**
445
- * This block carries only the **head** of the result: the replay truncated it
446
- * (see {@link TOOL_RESULT_HEAD_CHARS}), and the whole thing is one fetch away
447
- * at `GET /sessions/:id/events/:seq/result?toolUseId=`.
448
- *
449
- * On the **block**, never the event, and that is the same argument
450
- * `user_message.patch` has to make in reverse: the patch sits on the event and
451
- * its doc must therefore caveat "only when the message carries exactly one
452
- * `tool_result` block — with two, nothing says which one it belongs to". A
453
- * message answering three calls truncates whichever of them is large, so
454
- * paying that caveat a second time would make the marker unusable exactly
455
- * when it matters. {@link FilePatch.truncated} is the shipped precedent.
456
- *
457
- * Only ever set on a **replay** a client asked for (`truncateResults`), so a
458
- * client that has never heard of this field cannot receive one — which is why
459
- * this is additive at protocol 7 rather than a bump. Absent means the block is
460
- * whole.
461
- */
462
158
  truncated?: boolean;
463
- /** How many characters the untruncated result had. Set iff `truncated`.
464
- *
465
- * A client cannot compute it — it holds the head — and the number is not
466
- * cosmetic: a collapsed row spells "… +N chars", and `height.ts` sizes the row
467
- * by wrapping **that exact string**, so a count derived from the head would be
468
- * both a lie and a different pixel height. */
469
159
  total_chars?: number;
470
160
  };
471
- /**
472
- * How much of a tool result a truncating replay keeps.
473
- *
474
- * Chosen against the two clients' *own* budgets, and the relationship is the
475
- * whole point: the terminal theme shows ~400 characters collapsed and ~2,000
476
- * open, so at 8,000 the collapsed and open states are **byte-identical to an
477
- * untruncated attach** and only the uncapped "show everything" press ever
478
- * fetches. That collapses the entire feature to one press, and it is asserted
479
- * in a test rather than trusted — lowered below the open budget, this would
480
- * silently clip the open state with no marker, which is the one failure this
481
- * design must not have.
482
- *
483
- * Measured justification: on one 1,270-row session three `tool_result` frames
484
- * were 641 / 463 / 396 KB, 68% of a 3.1 MB attach. The cut is *structural* —
485
- * proportional to the thing that is actually large, wherever in the log it sits
486
- * — which a row window is not.
487
- */
488
161
  declare const TOOL_RESULT_HEAD_CHARS = 8000;
489
- /**
490
- * A base64 image part, delivered as an address instead of its bytes.
491
- *
492
- * The **seventh** rule of the family, and the first written *after* its
493
- * measurement rather than before it. Across 214 local sessions, 91% of all
494
- * tool-result payload is base64 image data — 489 MB against 44 MB of text — and
495
- * **no client renders a byte of it**: `blockText` in the reducer and
496
- * `joinedText` on iOS both fold a `tool_result` to its text parts, and both
497
- * clients draw a tool's picture from a host *path* (`savedPath` → `/produced`,
498
- * `/fs/read`), never from block content. So it is `replayRetains`' argument at
499
- * nine times the size of the case that rule was written for: bytes whose entire
500
- * effect on the reader is `return base`.
501
- *
502
- * A **new part type rather than a hollowed-out `image`**, and that is the one
503
- * judgement here worth stating. `headOf`'s shape-preservation rule — "a
504
- * truncation is a shorter result, never a different kind of one" — cuts the
505
- * other way for pixels: a head *is* a valid shorter text, but an image with no
506
- * bytes is not a smaller image, and spelling it `{ type: 'image', source }` with
507
- * no `data` invites precisely the failure shape-preservation exists to prevent,
508
- * a renderer that trusts `source.data` drawing `data:;base64,undefined`. An
509
- * unfamiliar type instead falls through every fold that already exists, exactly
510
- * as the CLI's own `tool_reference` part does: no `text`, so it contributes
511
- * nothing, and an unaware consumer renders what it renders today, which is
512
- * nothing. That is this family's safe failure.
513
- *
514
- * Only ever produced for a socket that asked (`imageRefs`), so a client that has
515
- * never heard of this type cannot receive one — which is why this is additive at
516
- * protocol 7, the same argument {@link ToolResultBlock.truncated} makes. Unlike
517
- * truncation it applies to **live events as well as replays**: the client's one
518
- * render path is ref-then-fetch, so bytes on a live event would either be
519
- * discarded (335 KB median, once per attached watcher) or need a second
520
- * decode-from-event path pinning megabytes inside the transcript cache — the
521
- * disease relocated rather than cured.
522
- */
523
162
  type ImageRefPart = {
524
163
  type: 'image_ref';
525
- /** The stored part's own media type (`image/png`, `image/jpeg` and
526
- * `image/webp` are the three observed), or `application/octet-stream` when it
527
- * had none. Never the membership test — that is `image` plus a base64 source. */
528
164
  media_type: string;
529
- /** Decoded size, which a client cannot compute from an address it has not
530
- * fetched yet. Not cosmetic: the placeholder spells it, and in the terminal
531
- * theme a rendered string *is* a row height. */
532
165
  bytes: number;
533
- /**
534
- * Index of this part in the **stored** block's content array, and the address
535
- * a fetch is made with.
536
- *
537
- * A stamped field rather than the position it arrives at, because that
538
- * position is not stable: `headOf` builds a truncated head by keeping text
539
- * parts up to budget and dropping every other part, so a block that is both
540
- * over the text budget and image-bearing has its parts renumbered the moment
541
- * the two rules compose. Stamped, the address survives any later reshaping —
542
- * and the route verifies it against the stored block rather than trusting it.
543
- */
544
166
  part_index: number;
545
167
  };
546
- /**
547
- * Project one `tool_result` content part onto its {@link ImageRefPart}, or
548
- * `undefined` when the part is not a base64 image and must be delivered as it
549
- * stands.
550
- *
551
- * The rule's **one spelling**, shared by the transform that replaces parts
552
- * (core), the route that serves them back (server) and the property test that
553
- * proves the fold is otherwise unchanged (react) — the same reason every other
554
- * member of this family lives here rather than in whichever package applies it.
555
- *
556
- * Deliberately narrow. The corpus holds exactly two non-text part kinds: this
557
- * one, and the CLI's `tool_reference`, of which every instance across 214
558
- * sessions totals 122 KB. A "drop non-text parts" rule would sweep those in for
559
- * no measurable gain, and narrowness is this family's standing habit.
560
- */
561
168
  declare function imagePartRef(part: {
562
169
  type?: string;
563
170
  [key: string]: unknown;
564
171
  }, index: number): ImageRefPart | undefined;
565
- /** Forward-compatible fallback for block types this protocol version doesn't model. */
566
172
  type UnknownBlock = {
567
173
  type: string;
568
174
  [key: string]: unknown;
569
175
  };
570
- /**
571
- * One hunk of a file edit, in unified-diff terms.
572
- *
573
- * The numbers are the engine's own, not the client's: `newStart` is where this
574
- * hunk begins in the file *after* the edit, which is what a reader needs to jump
575
- * to the change. A client cannot compute them — it has never seen the file — so
576
- * a diff rendered without this is a diff with no line numbers.
577
- */
578
176
  type PatchHunk = {
579
177
  oldStart: number;
580
178
  oldLines: number;
581
179
  newStart: number;
582
180
  newLines: number;
583
- /** Body lines, each prefixed ' ' (context), '-' (removed) or '+' (added), as
584
- * unified diff spells them. The prefix is part of the string. */
585
181
  lines: string[];
586
182
  };
587
- /**
588
- * What a file-editing tool changed — the renderable half of an engine's edit
589
- * output, and deliberately only that half.
590
- *
591
- * Both engines can say far more: the Claude SDK's `FileEditOutput` carries
592
- * `originalFile`, the **entire** contents of the file before the edit. That must
593
- * not travel here. This log is replayed to every attaching client and captured
594
- * into parking snapshots, so a whole file on every edit is paid for again on
595
- * every attach, forever — the same reason attachment bytes are references (see
596
- * {@link MessageAttachment}) rather than inline base64.
597
- *
598
- * So the runner projects the engine's output down to the hunks, which is exactly
599
- * what a diff renders and nothing more.
600
- */
601
183
  type FilePatch = {
602
- /** Absolute path the engine reported, when it named one. */path?: string; /** `create` when the file did not exist before this edit. */
184
+ path?: string;
603
185
  kind?: 'create' | 'update';
604
186
  hunks: PatchHunk[];
605
- /** Hunks were dropped to keep the event small. A renderer must say so rather
606
- * than present a partial diff as the whole change. */
607
187
  truncated?: boolean;
608
188
  };
609
189
  type ContentBlock = TextBlock | ThinkingBlock | ToolUseBlock | ToolResultBlock | UnknownBlock;
610
- /**
611
- * A file the user attached to a message — a photo, a screenshot, a document.
612
- *
613
- * The bytes never travel on this protocol. An attachment is uploaded first
614
- * (`POST {basePath}/sessions/:id/attachments`), and the command that sends the
615
- * message names it by id; what lands in the seq-numbered event log is this
616
- * reference. That is deliberate: the log is replayed to every attaching client
617
- * and captured into parking snapshots, so a few phone photos inlined as base64
618
- * would be paid for on every attach, forever. Clients render a thumbnail by
619
- * fetching `GET {basePath}/sessions/:id/attachments/:attachmentId`.
620
- *
621
- * Lifetime is the session's, like `/files` — the store is in-memory and an
622
- * attachment 404s after a server restart. The message itself is unaffected: the
623
- * model saw the bytes at send time.
624
- */
625
190
  type MessageAttachment = {
626
- /** Server-assigned; the path segment of the download URL. */id: string; /** Display name from the file the user picked. A leaf name, never a path. */
191
+ id: string;
627
192
  name: string;
628
- /** IANA media type, e.g. 'image/jpeg'. The server decides how it reaches the
629
- * model (image block, document block, or inlined text) from this. */
630
193
  mediaType: string;
631
194
  bytes: number;
632
195
  };
@@ -635,8 +198,6 @@ type ApiMessage = {
635
198
  content: string | ContentBlock[];
636
199
  model?: string;
637
200
  stop_reason?: string | null;
638
- /** Per-API-call token usage when the message carries it (assistant messages do).
639
- * Enables mid-run token accounting; result-message usage stays authoritative. */
640
201
  usage?: {
641
202
  input_tokens?: number;
642
203
  output_tokens?: number;
@@ -644,181 +205,80 @@ type ApiMessage = {
644
205
  cache_read_input_tokens?: number;
645
206
  };
646
207
  };
647
- /** A tool call promoted into a pending approval by the runner's canUseTool hook
648
- * (Claude), or an engine ask-channel request surfaced by its runner (codex).
649
- * The two do not share a tense: Claude asks BEFORE a tool runs; codex's command
650
- * approval is usually an escalation AFTER its sandbox already ran and refused
651
- * the command ("command failed; retry without sandbox?"). The runner authors
652
- * `title`/`description`/`decisionReason` to say which — clients should render
653
- * those fields rather than composing their own "X wants to run Y" sentence. */
654
208
  type PermissionRequest = {
655
- /** Server-assigned id; used by the `permission_decision` command. */id: string;
209
+ id: string;
656
210
  toolName: string;
657
211
  input: Record<string, unknown>;
658
- toolUseId: string; /** Full prompt sentence from the SDK, e.g. "Claude wants to read foo.txt". */
659
- title?: string; /** Short noun phrase for the tool action, e.g. "Read file". */
660
- displayName?: string; /** Human-readable subtitle, e.g. "Claude will have read access to ~/x". */
661
- description?: string; /** Why this permission request was triggered. */
662
- decisionReason?: string; /** If raised from within a subagent, that subagent's id. */
663
- agentId?: string; /** Epoch ms after which the server resolves it via its timeout policy. */
212
+ toolUseId: string;
213
+ title?: string;
214
+ displayName?: string;
215
+ description?: string;
216
+ decisionReason?: string;
217
+ agentId?: string;
664
218
  expiresAt?: number;
665
219
  };
666
220
  type PermissionDecisionSource = 'client' | 'timeout' | 'policy';
667
- /** One choice of an AskUserQuestion question (SDK tool-input mirror). */
668
221
  type UserQuestionOption = {
669
222
  label: string;
670
223
  description?: string;
671
- /** Optional preview content (markdown unless the session configures html)
672
- * rendered when the option is focused. */
673
224
  preview?: string;
674
225
  };
675
- /** One question from the AskUserQuestion tool's input. By the tool's convention the
676
- * first option is the model's recommended choice. */
677
226
  type UserQuestion = {
678
- question: string; /** Short chip/tag label (max ~12 chars), e.g. "Auth method". */
227
+ question: string;
679
228
  header: string;
680
229
  options: UserQuestionOption[];
681
230
  multiSelect?: boolean;
682
231
  };
683
- /** How a session treats the AskUserQuestion tool:
684
- * - 'ask' (default) — a pending permission like any other: interactive UIs render the
685
- * question form; job webhooks carry the full request so a remote controller can
686
- * answer over REST (POST /sessions/:id/permissions/:requestId).
687
- * - 'auto' — resolved immediately with each question's first (recommended) option.
688
- * - 'deny' — the tool is refused with guidance to decide autonomously (unattended runs).
689
- * Answers ride a permission allow as `updatedInput.answers`: question text → chosen
690
- * option label(s), multi-select labels comma-joined — the shape the CLI's own UI uses. */
691
232
  type QuestionBehavior = 'ask' | 'auto' | 'deny';
692
- /** A model the session can switch to (SDK ModelInfo mirror; fields it may grow stay unknown). */
693
233
  type ModelOption = {
694
- /** Model id for createSession.model / set_model. */value: string;
695
- /** Wire model id this row resolves to ('sonnet' → 'claude-sonnet-5'). What a
696
- * session actually reports as its model is the resolved form, so this is how a
697
- * client matches the running model back to the row that names it. */
234
+ value: string;
698
235
  resolvedModel?: string;
699
236
  displayName: string;
700
237
  description?: string;
701
- /** Whether this belongs in a picker's main list rather than behind a "more
702
- * models" step: the newest model of each family. Derived server-side — the CLI
703
- * reports one flat list — so that every client groups it the same way. */
704
238
  primary?: boolean;
705
- /** Reasoning efforts this model supports at create time (codex catalogs carry
706
- * them, from the binary's own per-model list). Absent = the engine's default
707
- * set ({@link EngineCapabilities.reasoningEfforts}) applies. Open strings —
708
- * the binary's vocabulary outruns its SDK's union. */
709
239
  reasoningEfforts?: readonly string[];
710
240
  };
711
- /**
712
- * A skill the engine can decide to use — **not** a command.
713
- *
714
- * The distinction is the whole point of this type existing beside
715
- * {@link SlashCommandInfo}. A slash command is wire syntax: the CLI parses
716
- * `/wrapup` out of the message and runs it. A skill is a capability the model
717
- * *chooses* from its description; there is no `/skillname` the engine would
718
- * recognise, and sending one reaches the model as literal text.
719
- *
720
- * So a client may list these, and may offer them as a **typing aid** that
721
- * inserts ordinary editable prose ({@link SkillInfo.defaultPrompt}) — but it
722
- * must never render them as command chips, and must never put them in
723
- * `capabilities.commands`, which means "the CLI accepts these as commands".
724
- */
725
241
  type SkillInfo = {
726
- /** Directory name under the skills root — the identity the model refers to. */name: string;
727
- /** What the skill is for, as its own manifest states it. This is the text the
728
- * MODEL selects on, so it is also the most honest thing to show a human. */
242
+ name: string;
729
243
  description?: string;
730
- /** A one-liner where the skill declares one, for a list row too narrow for
731
- * `description`. */
732
244
  shortDescription?: string;
733
- /** Human-facing name from the skill's own interface block, when it differs
734
- * from `name`. */
735
245
  displayName?: string;
736
- /**
737
- * The engine's own suggested opening message for this skill. A client that
738
- * offers a picker INSERTS this as plain editable text for the user to finish
739
- * and send; it is a draft, never something submitted on selection.
740
- */
741
246
  defaultPrompt?: string;
742
- /** Where the skill came from: 'user' | 'repo' | 'system' | 'admin' — kept as
743
- * a string, the engine's set may grow. */
744
247
  scope?: string;
745
- /** False when the operator has this skill switched off: still listed, because
746
- * "installed but off" is a different answer from "not installed". */
747
248
  enabled: boolean;
748
249
  };
749
- /** A slash command the CLI accepts as user-message text (SDK SlashCommand mirror). */
750
250
  type SlashCommandInfo = {
751
- /** Command name without the leading slash. */name: string;
752
- description?: string; /** Hint for arguments, e.g. "<file>". */
753
- argumentHint?: string; /** Alternate names resolving to this command. */
251
+ name: string;
252
+ description?: string;
253
+ argumentHint?: string;
754
254
  aliases?: string[];
755
255
  };
756
- /** One category row from the CLI's context-usage breakdown (system prompt, tools, ...). */
757
256
  type ContextUsageCategory = {
758
257
  name: string;
759
258
  tokens: number;
760
- /** Color the CLI assigns the category. Often a CLI theme token name ('inactive',
761
- * 'promptBorder', ...), not a CSS color — validate before styling with it. */
762
259
  color: string;
763
260
  };
764
- /** Context-window usage snapshot (SDK getContextUsage mirror), polled after each turn. */
765
261
  type ContextUsage = {
766
262
  categories: ContextUsageCategory[];
767
263
  totalTokens: number;
768
- maxTokens: number; /** Used share of the window, 0–100. */
769
- percentage: number; /** Model the window sizing applies to. */
264
+ maxTokens: number;
265
+ percentage: number;
770
266
  model?: string;
771
267
  };
772
- /**
773
- * The context-window reading that rides the **sessions list**, as opposed to the
774
- * full {@link ContextUsage} that rides the event stream.
775
- *
776
- * Three numbers, and the omission is the design: `categories` is a breakdown for
777
- * a dialog that has a live session behind it, and this field is on every row of
778
- * `GET /sessions`, which a busy client polls at 1.2s. Same attachment-bytes
779
- * discipline as {@link SessionInfo.subagents}. `percentage` alone would size a
780
- * ring, but the token pair is what lets a row *say* `142k / 200k` on hover or
781
- * long-press without a second round trip, and it is two numbers.
782
- *
783
- * **Absent is a real state and is not zero.** A parked session, one that has
784
- * never run a turn, or an engine that reports no window has no reading — render
785
- * nothing, never an empty ring, which claims "context is empty" rather than "no
786
- * answer". Also absent on an older server.
787
- */
788
268
  type ContextReading = {
789
269
  totalTokens: number;
790
- maxTokens: number; /** Used share of the window, 0–100. */
270
+ maxTokens: number;
791
271
  percentage: number;
792
272
  };
793
- /**
794
- * One rate-limit window snapshot (SDK SDKRateLimitInfo mirror). Emitted only for
795
- * claude.ai subscription sessions — API-key sessions may never produce one, so
796
- * clients must render nothing (not 0%) until data arrives.
797
- */
798
273
  type RateLimitInfo = {
799
- /** 'allowed' | 'allowed_warning' | 'rejected' — kept as string, the SDK union may grow. */status: string;
800
- /** Which window: 'five_hour' (session), 'seven_day' (weekly), 'seven_day_opus',
801
- * 'seven_day_sonnet', 'overage', ... — kept as string, the SDK union may grow. */
274
+ status: string;
802
275
  rateLimitType?: string;
803
- /** Used share of the window, 0–100. The CLI omits it on some updates — treat
804
- * absent as unknown, not 0. */
805
- utilization?: number; /** Epoch **seconds** when the window resets (render countdowns client-side). */
276
+ utilization?: number;
806
277
  resetsAt?: number;
807
278
  isUsingOverage?: boolean;
808
279
  };
809
- /**
810
- * Lifecycle of one tool execution, correlated by `executionId` end to end.
811
- *
812
- * - `pending` — dispatched, result not in yet (bridged to a client, or queued).
813
- * - `deferred` — parked beyond this turn/process; may outlive the session's
814
- * liveness and be applied on rehydration.
815
- * - `settled` / `failed` — terminal. Results are applied idempotently by id, so
816
- * a duplicate delivery is a no-op rather than a second application.
817
- */
818
280
  type ToolExecutionStatus = 'pending' | 'deferred' | 'settled' | 'failed';
819
- /** Where a tool execution ran (or is running). Advisory: for display and routing. */
820
281
  type ToolExecutionBackend = 'server' | 'browser' | 'managed' | 'remote';
821
- /** Result payload of a tool execution, by value — never a live host reference. */
822
282
  type ToolExecutionOutput = {
823
283
  type: 'text';
824
284
  value: string;
@@ -826,14 +286,11 @@ type ToolExecutionOutput = {
826
286
  type: 'json';
827
287
  value: unknown;
828
288
  };
829
- type SessionEventBody = /** SDK init handshake: what this session actually is. */{
289
+ type SessionEventBody = {
830
290
  type: 'system_init';
831
291
  sdkSessionId: string;
832
292
  model: string;
833
293
  cwd: string;
834
- /** Where the session's Anthropic auth came from: 'oauth' means a claude.ai
835
- * subscription login; other values ('user' | 'project' | 'org' | 'temporary')
836
- * are API-key provenance. Kept as string — the SDK union may grow. */
837
294
  apiKeySource: string;
838
295
  tools: string[];
839
296
  skills: string[];
@@ -848,136 +305,55 @@ type SessionEventBody = /** SDK init handshake: what this session actually is. *
848
305
  type: 'status_changed';
849
306
  status: SessionStatus;
850
307
  detail?: string;
851
- }
852
- /** Models and slash commands available to this session; fetched from the CLI after
853
- * init. Late attachers get it via replay like any other event. */
854
- | {
308
+ } | {
855
309
  type: 'capabilities';
856
310
  models: ModelOption[];
857
311
  commands: SlashCommandInfo[];
858
- /** Wire id the session's *default* resolves to, from the CLI's own `default`
859
- * row. Answers "what will this session answer as" before it has answered
860
- * anything — `system_init` carries the model, but a promptless session gets
861
- * no `system_init` until its first message. */
862
312
  defaultModel?: string;
863
- }
864
- /**
865
- * The skills this session's engine can reach ({@link SkillInfo}) — a full
866
- * replacement each time, not a delta, so a late attacher's replay of several
867
- * of these converges on the last one. Emitted once the session's engine has
868
- * enumerated them, and again whenever the engine reports the set changed
869
- * (a skill added or edited on disk).
870
- *
871
- * Deliberately NOT folded into `capabilities.commands`: skills are not
872
- * commands (see {@link SkillInfo}). Only engines whose record sets
873
- * {@link EngineCapabilities.skillsList} ever emit it.
874
- */
875
- | {
313
+ } | {
876
314
  type: 'skills';
877
315
  skills: SkillInfo[];
878
- }
879
- /**
880
- * The engine wrote a file on the **host filesystem** and handed over its path
881
- * — codex's `image_gen` saving a PNG is the case that motivated it. The
882
- * host-filesystem sibling of `file_delivered` (which is the scratch-VFS one).
883
- *
884
- * Fetch it at `GET {basePath}/sessions/:id/produced/:fileId` for as long as
885
- * the session lives. That route has no root allowlist and no size cap, and
886
- * that is sound *because of where the path came from*: this event is authored
887
- * by the runner about a file the engine itself just wrote, not by the agent
888
- * about a path it chose. `/fs/*` gates the second kind and must keep doing so
889
- * — a file the agent merely *read* is not a produced file and does not belong
890
- * here.
891
- *
892
- * Re-emitting the same file is a no-op: `fileId` is derived from the path, so
893
- * a runner that learns the path twice (codex reports `savedPath` on both the
894
- * progress and completed item) registers it once.
895
- */
896
- | {
897
- type: 'file_produced'; /** Opaque, stable per session+path. The route's path segment. */
316
+ } | {
317
+ type: 'file_produced';
898
318
  fileId: string;
899
- /** Absolute host path, as the engine reported it. Shown to the operator,
900
- * and what a client matches against a tool card's own `savedPath`. */
901
319
  path: string;
902
- /** Media type when the runner could determine one (usually from the
903
- * extension). Absent = let the route's own sniffing decide. */
904
- mediaType?: string; /** Size at the time it was reported, when the runner knew it. */
905
- bytes?: number; /** The tool call that produced it, when one did. */
320
+ mediaType?: string;
321
+ bytes?: number;
906
322
  toolUseId?: string;
907
- } /** The session's model changed via `set_model`. `model` undefined = back to default. */ | {
323
+ } | {
908
324
  type: 'model_changed';
909
325
  model?: string;
910
- } /** The session's permission mode changed via `set_permission_mode`. */ | {
326
+ } | {
911
327
  type: 'permission_mode_changed';
912
328
  mode: PermissionMode;
913
- } /** Context-window usage snapshot; the runner polls it after each turn. */ | {
329
+ } | {
914
330
  type: 'context_usage';
915
331
  usage: ContextUsage;
916
- } /** Subscription rate-limit update for one window (see {@link RateLimitInfo}). */ | {
332
+ } | {
917
333
  type: 'rate_limit';
918
334
  info: RateLimitInfo;
919
- }
920
- /** Which claude.ai plan the rate-limit windows belong to: 'pro' | 'max' | 'team' |
921
- * 'enterprise' — kept as string, the set may grow. Emitted from the same poll as
922
- * `rate_limit`, once per change, and never for an API-key session (which has no
923
- * plan). It names the windows; it does not size them — the tier suffix a
924
- * subscription page shows ("Max 20x") is not in the data. */
925
- | {
335
+ } | {
926
336
  type: 'plan_info';
927
337
  subscriptionType: string;
928
- }
929
- /**
930
- * The engine started a **fresh conversation inside the same session** — the
931
- * CLI's `/clear`, a plan-mode exit, and whatever fresh-conversation flows the
932
- * SDK grows. The session id, registry row, workspace and scope are all
933
- * unchanged; only the conversation is new. Clients empty the transcript and
934
- * keep every session-scoped fact (models, commands, skills, produced files,
935
- * rate limits, cwd, permission mode).
936
- *
937
- * The server's replay honours it too: an attach after a reset does not
938
- * resurrect the cleared rows, because the runner skips *transcript content*
939
- * below the latest reset (see {@link transcriptContent}) while still
940
- * replaying every state-bearing event. `SessionInfo.activityCount` stays
941
- * monotonic across a reset on purpose — it is an unread cursor, not an item
942
- * count, and winding it back would kill every stored watermark above it.
943
- */
944
- | {
338
+ } | {
945
339
  type: 'conversation_reset';
946
- /** The engine session id the fresh conversation runs under (the SDK's
947
- * `new_conversation_id`), when the engine reported one. The follow-up
948
- * `system_init` remains authoritative. */
949
340
  sdkSessionId?: string;
950
341
  } | {
951
342
  type: 'assistant_message';
952
- message: ApiMessage; /** Set when the message was produced inside a subagent (Task tool). */
953
- parentToolUseId: string | null; /** True when backfilled from a resumed session's history. */
343
+ message: ApiMessage;
344
+ parentToolUseId: string | null;
954
345
  replay?: boolean;
955
346
  uuid: string;
956
347
  } | {
957
348
  type: 'user_message';
958
349
  message: ApiMessage;
959
- parentToolUseId: string | null; /** True when replayed from a resumed session's history. */
960
- replay?: boolean; /** True for tool results and other synthetic user-role messages. */
350
+ parentToolUseId: string | null;
351
+ replay?: boolean;
961
352
  synthetic?: boolean;
962
- /** Files sent with this message, by reference (see {@link MessageAttachment}).
963
- * `message.content` carries the typed text only — the attachment bytes went
964
- * to the model, not into this log. */
965
353
  attachments?: MessageAttachment[];
966
- /**
967
- * What a file-editing tool changed, when this message carries that tool's
968
- * result (see {@link FilePatch}). Set by the runner from the engine's own
969
- * structured output — never derived by a client from the result text.
970
- *
971
- * Only when the message carries exactly one `tool_result` block, which is
972
- * what both engines send: with two, there is nothing that says which one
973
- * the patch belongs to, and guessing would attach a diff to the wrong call.
974
- */
975
354
  patch?: FilePatch;
976
355
  uuid?: string;
977
- }
978
- /** Raw Anthropic streaming event (message_start/content_block_delta/...); emitted only
979
- * when the session was created with `includePartialMessages`. */
980
- | {
356
+ } | {
981
357
  type: 'stream_delta';
982
358
  event: {
983
359
  type: string;
@@ -991,7 +367,7 @@ type SessionEventBody = /** SDK init handshake: what this session actually is. *
991
367
  isError: boolean;
992
368
  durationMs: number;
993
369
  numTurns: number;
994
- totalCostUsd: number; /** Final text of the turn (success only). */
370
+ totalCostUsd: number;
995
371
  result?: string;
996
372
  errors?: string[];
997
373
  usage?: unknown;
@@ -1002,48 +378,34 @@ type SessionEventBody = /** SDK init handshake: what this session actually is. *
1002
378
  type: 'permission_resolved';
1003
379
  requestId: string;
1004
380
  behavior: 'allow' | 'deny';
1005
- resolvedBy: PermissionDecisionSource; /** Denial message, when denied. */
381
+ resolvedBy: PermissionDecisionSource;
1006
382
  message?: string;
1007
- }
1008
- /** A tool execution was dispatched to a backend. For bridged executions this
1009
- * precedes the `tool_call_request` frame; for deferred ones it is the record
1010
- * that survives a teardown. */
1011
- | {
383
+ } | {
1012
384
  type: 'execution_dispatched';
1013
385
  executionId: string;
1014
386
  toolName: string;
1015
- backend: ToolExecutionBackend; /** True when the execution may outlive this turn or process. */
1016
- deferred?: boolean; /** Epoch ms after which the server applies its timeout policy. */
387
+ backend: ToolExecutionBackend;
388
+ deferred?: boolean;
1017
389
  expiresAt?: number;
1018
- } /** A dispatched execution produced a result. Applied idempotently by `executionId`. */ | {
390
+ } | {
1019
391
  type: 'execution_result';
1020
392
  executionId: string;
1021
- output: ToolExecutionOutput; /** Guest/agent-visible logs, if the backend captured any. */
393
+ output: ToolExecutionOutput;
1022
394
  logs?: string[];
1023
395
  durationMs?: number;
1024
- }
1025
- /** A dispatched execution failed, timed out, or was orphaned. The failure is fed
1026
- * back into the loop as tool output so the agent can adapt — it is not a session error. */
1027
- | {
396
+ } | {
1028
397
  type: 'execution_failed';
1029
- executionId: string; /** Machine-readable cause: 'timeout' | 'oom' | 'exception' | 'orphaned' | backend-specific. */
398
+ executionId: string;
1030
399
  reason: string;
1031
400
  error: string;
1032
401
  logs?: string[];
1033
402
  durationMs?: number;
1034
- }
1035
- /** The agent handed over a file from its session scratch filesystem (the
1036
- * `deliver_file` tool). Download it via `GET {basePath}/sessions/:id/files/<path>`
1037
- * for as long as the session lives (the VFS is in-memory). */
1038
- | {
403
+ } | {
1039
404
  type: 'file_delivered';
1040
405
  path: string;
1041
406
  bytes: number;
1042
407
  description?: string;
1043
- }
1044
- /** Any SDKMessage this protocol version doesn't model first-class (task progress,
1045
- * compaction boundaries, auth status, ...). Payload is the raw SDK message. */
1046
- | {
408
+ } | {
1047
409
  type: 'sdk_event';
1048
410
  payload: {
1049
411
  type: string;
@@ -1057,68 +419,36 @@ type SessionEventBody = /** SDK init handshake: what this session actually is. *
1057
419
  reason: 'client' | 'server' | 'error';
1058
420
  };
1059
421
  type SessionEvent = SessionEventBody & {
1060
- /** Monotonic per-session sequence number, starting at 1. */seq: number; /** Epoch ms when the server emitted the event. */
422
+ seq: number;
1061
423
  ts: number;
1062
424
  };
1063
425
  type SessionCommand = {
1064
426
  type: 'user_message';
1065
427
  text: string;
1066
- /** Ids from `POST {basePath}/sessions/:id/attachments`, in the order they
1067
- * should reach the model. Unknown ids fail the command rather than sending
1068
- * a message that quietly lost its picture. */
1069
428
  attachmentIds?: string[];
1070
429
  } | {
1071
430
  type: 'permission_decision';
1072
431
  requestId: string;
1073
- behavior: 'allow' | 'deny'; /** allow only: modified tool input to run instead of the original. */
1074
- updatedInput?: Record<string, unknown>; /** deny only: reason surfaced to the model. */
1075
- message?: string; /** deny only: also interrupt the running turn. */
432
+ behavior: 'allow' | 'deny';
433
+ updatedInput?: Record<string, unknown>;
434
+ message?: string;
1076
435
  interrupt?: boolean;
1077
436
  } | {
1078
437
  type: 'interrupt';
1079
- }
1080
- /**
1081
- * Reset the conversation in place — same session id, same watermarks, same
1082
- * row in the list, empty context. The server answers with a
1083
- * `conversation_reset` event, whose replay rules are what stop a later attach
1084
- * from resurrecting the cleared transcript.
1085
- *
1086
- * Send only where {@link EngineCapabilities.clearContext} says so: a server
1087
- * that predates this command rejects it as unknown, and an engine that cannot
1088
- * do it errors rather than quietly doing nothing.
1089
- *
1090
- * Sent while a turn is running, it **queues** behind that turn rather than
1091
- * cutting it short — a clear is not an interrupt, and one that landed in the
1092
- * middle of the turn it was clearing would be neither. Interrupt first if the
1093
- * intent was to stop the work as well as forget it.
1094
- */
1095
- | {
438
+ } | {
1096
439
  type: 'clear_context';
1097
440
  } | {
1098
441
  type: 'set_permission_mode';
1099
442
  mode: PermissionMode;
1100
- } /** Switch the model for subsequent responses; omit `model` for the default. */ | {
443
+ } | {
1101
444
  type: 'set_model';
1102
445
  model?: string;
1103
- }
1104
- /**
1105
- * Result of a tool execution the server bridged to this client (see
1106
- * {@link ToolCallRequestFrame}). Unknown or already-settled `executionId`s are
1107
- * ignored — delivery is idempotent, and a late result after a timeout must not
1108
- * re-open a settled call.
1109
- *
1110
- * Browser-returned results are UNTRUSTED input: acceptable for the user's own
1111
- * data, never a source for server-authoritative state.
1112
- */
1113
- | {
446
+ } | {
1114
447
  type: 'tool_call_result';
1115
448
  executionId: string;
1116
449
  output: ToolExecutionOutput;
1117
450
  logs?: string[];
1118
- }
1119
- /** The client could not execute a bridged call (unsupported tool, guest error,
1120
- * tab closing). Fed back to the agent as tool output. */
1121
- | {
451
+ } | {
1122
452
  type: 'tool_call_error';
1123
453
  executionId: string;
1124
454
  reason: string;
@@ -1127,40 +457,28 @@ type SessionCommand = {
1127
457
  } | {
1128
458
  type: 'close';
1129
459
  };
1130
- /** First frame the server sends after a successful attach. */
1131
460
  type AttachedFrame = {
1132
461
  type: 'attached';
1133
462
  protocolVersion: number;
1134
- session: SessionInfo; /** Events with seq > the client's `afterSeq` follow as `event` frames. */
463
+ session: SessionInfo;
1135
464
  replayingFrom: number;
1136
465
  };
1137
- /**
1138
- * Ask the attached client to execute a tool call in its own sandbox (browser
1139
- * bridge). The client answers with `tool_call_result` or `tool_call_error`
1140
- * carrying the same `executionId`.
1141
- *
1142
- * Only sandbox-benefiting tools are ever bridged. Authenticated/authoritative
1143
- * tools (MCP, secret-bearing APIs) execute server-side and never appear here.
1144
- */
1145
466
  type ToolCallRequestFrame = {
1146
467
  type: 'tool_call_request';
1147
468
  executionId: string;
1148
469
  toolName: string;
1149
- input: unknown; /** Files to seed the client's scratch VFS with, path → contents. */
470
+ input: unknown;
1150
471
  vfsSeed?: Record<string, string>;
1151
472
  limits?: {
1152
473
  timeoutMs?: number;
1153
474
  memoryLimitBytes?: number;
1154
- }; /** Epoch ms after which the server gives up and fails the execution. */
475
+ };
1155
476
  expiresAt?: number;
1156
477
  };
1157
478
  type ServerFrame = AttachedFrame | {
1158
479
  type: 'event';
1159
480
  event: SessionEvent;
1160
- } | ToolCallRequestFrame
1161
- /** A bridged execution no longer needs an answer (turn interrupted, timed out,
1162
- * or the session closed) — the client should abandon it. */
1163
- | {
481
+ } | ToolCallRequestFrame | {
1164
482
  type: 'tool_call_canceled';
1165
483
  executionId: string;
1166
484
  reason: string;
@@ -1169,283 +487,87 @@ type ServerFrame = AttachedFrame | {
1169
487
  message: string;
1170
488
  };
1171
489
  type ClientFrame = SessionCommand;
1172
- /** Per-profile fallbacks filled into session/job requests that leave the field
1173
- * unset. Defaults, not enforced caps — an explicit request value always wins. */
1174
490
  type ProfileDefaults = {
1175
491
  model?: string;
1176
492
  permissionMode?: PermissionMode;
1177
493
  };
1178
- /**
1179
- * A named Claude Code config directory sessions can run under: the session's CLI
1180
- * process gets it as CLAUDE_CONFIG_DIR, so the profile carries that directory's
1181
- * settings, memory, skills, and whatever credentials the SDK/CLI resolves from it.
1182
- * Profiles are declared in server options at startup (or a 'default' one is
1183
- * auto-created from the operator's own config dir) — the API only reads them.
1184
- */
1185
- /**
1186
- * Which engine a profile runs on. A **closed union, deliberately**: both clients
1187
- * switch exhaustively, the Swift mirror ships in lockstep, and a closed set is
1188
- * what lets this package carry per-engine capability defaults
1189
- * ({@link ENGINE_CAPABILITIES}) browser-safe, with no server round-trip. Adding a
1190
- * member is a versioned protocol event.
1191
- *
1192
- * - `claude` (default) — Claude Code via the Agent SDK, configured by a config dir.
1193
- * - `codex` — OpenAI Codex over the codex CLI binary's `app-server` JSON-RPC
1194
- * surface, configured by a CODEX_HOME (auth resolved by the binary itself,
1195
- * like claude).
1196
- * - `provider` — a model-agnostic provider over the AI SDK, assembled by the
1197
- * host's `createEngineRunner` hook.
1198
- */
1199
494
  type ProfileEngine = 'claude' | 'codex' | 'provider';
1200
- /**
1201
- * What an engine does and does not do — one axis per real difference, each field
1202
- * answering a concrete UI or gateway question. Clients render from this record
1203
- * instead of switching on the engine name: an absent capability means the
1204
- * affordance is *hidden*, never a control that silently does nothing.
1205
- *
1206
- * Reaches clients in two places, same shape: `ProfileInfo.capabilities` (stamped
1207
- * by the server; the create form's source) and `SessionInfo.capabilities`
1208
- * (reported by the runner; the session surface's source). When the field is
1209
- * absent — an older server — {@link ENGINE_CAPABILITIES} keyed by the engine
1210
- * name is the browser-safe default.
1211
- */
1212
495
  type EngineCapabilities = {
1213
- /** PermissionRequest / permission_resolved can occur; approval UI is live.
1214
- * False: hide approval affordances entirely (and `questionBehavior` on jobs). */
1215
496
  interactiveApprovals: boolean;
1216
- /** Modes this engine can honor. A stored choice outside the set is coerced to
1217
- * {@link EngineCapabilities.defaultPermissionMode}, not submitted. */
1218
- permissionModes: readonly PermissionMode[]; /** Coercion target for a stored/unsupported mode choice (always ∈ permissionModes). */
1219
- defaultPermissionMode: PermissionMode; /** CreateSessionRequest.resume works (an engine session id continues). */
497
+ permissionModes: readonly PermissionMode[];
498
+ defaultPermissionMode: PermissionMode;
1220
499
  resume: boolean;
1221
- /** Resume replays prior history into the transcript (Claude's backfill).
1222
- * False + resume: show a "history predates this attach" notice instead of
1223
- * treating an empty transcript as a bug. */
1224
- resumeBackfill: boolean; /** GET /sdk-sessions offers a resume picker for this engine. */
1225
- listSessions: boolean; /** context_usage events can occur. False: render nothing — never a 0% ring. */
1226
- contextUsage: boolean; /** rate_limit / plan_info events can occur. False: render nothing. */
500
+ resumeBackfill: boolean;
501
+ listSessions: boolean;
502
+ contextUsage: boolean;
1227
503
  rateLimits: boolean;
1228
- /** GET /sessions/:id/mcp works (else 501) — the engine can *list* its MCP
1229
- * servers. Gates the MCP panel's existence. */
1230
504
  mcpStatus: boolean;
1231
- /**
1232
- * POST /sessions/:id/mcp/:name works — the engine can reconnect, enable and
1233
- * disable a server. Separate from {@link EngineCapabilities.mcpStatus}
1234
- * because listing and acting are genuinely different powers: codex reports
1235
- * rich status but exposes no per-server action on this transport, and a panel
1236
- * that rendered the buttons anyway would present three controls that do
1237
- * nothing and then report success. False: render the panel read-only.
1238
- */
1239
- mcpServerActions: boolean; /** A session request may bring its own mcpServers. */
1240
- sessionMcpServers: boolean; /** capabilities events carry slash commands (composer popover). */
505
+ mcpServerActions: boolean;
506
+ sessionMcpServers: boolean;
1241
507
  slashCommands: boolean;
1242
- /**
1243
- * The `clear_context` command works — the engine can reset the conversation
1244
- * *in place*, keeping the session id, its watermarks and its place in the
1245
- * list, and announce it with {@link SessionEventBody} `conversation_reset`.
1246
- *
1247
- * Separate from {@link EngineCapabilities.slashCommands} because the two
1248
- * answer different questions and only one engine has both: Claude reaches a
1249
- * clear through the `/clear` its CLI already lists, codex has no command
1250
- * surface at all and needs the explicit operation, and a client that gated
1251
- * the control on `slashCommands` would offer it exactly where it was already
1252
- * offered and hide it where it is the only route.
1253
- *
1254
- * Absent = false, so an older gateway hides the control rather than
1255
- * presenting one that 501s.
1256
- */
1257
508
  clearContext?: boolean;
1258
- /** `skills` events can occur — the engine can enumerate its skills. False:
1259
- * hide the skills panel entirely rather than showing an empty one. Orthogonal
1260
- * to `slashCommands`: an engine can have skills and no commands (codex), or
1261
- * commands and no skill listing (claude, whose skills reach clients only as
1262
- * `system_init.skills` names). */
1263
- skillsList: boolean; /** settingSources / allowDangerouslySkipPermissions-style CLI options apply. */
1264
- settingSources: boolean; /** maxTurns / maxBudgetUsd are honored (else the gateway 400s them). */
509
+ skillsList: boolean;
510
+ settingSources: boolean;
1265
511
  budgets: boolean;
1266
- /** Attachment kinds sendMessage can deliver to the model. Filter the attach
1267
- * menu by kind; refuse locally before the server's 415. */
1268
512
  attachments: ReadonlyArray<'image' | 'pdf' | 'text'>;
1269
- /** Efforts offerable at create time; absent = not settable (hide the control).
1270
- * Open strings — Codex's own binary already outruns its SDK's union. */
1271
- reasoningEfforts?: readonly string[]; /** Sessions expose a scratch VFS (GET /sessions/:id/files, deliverables panel). */
513
+ reasoningEfforts?: readonly string[];
1272
514
  vfs: boolean;
1273
- /**
1274
- * The engine runs against a host directory, so `CreateSessionRequest.cwd` is
1275
- * required and meaningful (and a create form should ask for it). False: the
1276
- * engine has no host filesystem — the gateway accepts a session with no
1277
- * `cwd`, `SessionInfo.cwd` reports `''`, and there is no path to validate.
1278
- *
1279
- * Absent = true, so a wire copy from an older gateway keeps the old
1280
- * always-required behaviour rather than silently relaxing it.
1281
- */
1282
515
  hostCwd?: boolean;
1283
- /** stream_delta granularity: per-token, coarse item updates (no typing
1284
- * cursor), or none. */
1285
516
  streaming: 'token' | 'item' | 'none';
1286
517
  };
1287
- /**
1288
- * The static capability record of each engine — the browser-safe default for
1289
- * `ProfileInfo.capabilities` / `SessionInfo.capabilities`, and the single place
1290
- * the values are written down. Core's adapters *reference* this record and a
1291
- * conformance test compares runner behaviour against it, so it cannot silently
1292
- * diverge from the code. When both a wire copy and this default exist, the wire
1293
- * copy wins.
1294
- */
1295
518
  declare const ENGINE_CAPABILITIES: Record<ProfileEngine, EngineCapabilities>;
1296
- /**
1297
- * Permission modes the model-agnostic provider engine understands.
1298
- * @deprecated Read `ENGINE_CAPABILITIES.provider.permissionModes` (this is an
1299
- * alias of it, kept for protocol-5 consumers).
1300
- */
1301
- declare const PROVIDER_PERMISSION_MODES: readonly PermissionMode[];
1302
- /**
1303
- * Whether a profile's engine can run a permission mode. The single source of
1304
- * truth for the restriction: create forms filter what they offer with it, the
1305
- * gateway rejects with it. An absent `engine` means 'claude' (every mode).
1306
- */
1307
519
  declare function supportsPermissionMode(engine: ProfileEngine | undefined, mode: PermissionMode): boolean;
1308
- /**
1309
- * A model provider a `provider` profile can run on. Credentials are ALWAYS
1310
- * resolved from the operator's environment — never carried on the wire, never
1311
- * stored here. `apiKeyEnv` names the variable to read, it does not hold a key.
1312
- */
1313
520
  type ProviderConfig = {
1314
- /** Provider adapter to use, e.g. 'anthropic' | 'openai' | 'moonshotai' |
1315
- * 'openai-compatible'. Kept as a string: the set is host-extensible. */
1316
- id: string; /** Default model id, e.g. 'kimi-k3'. Overridable per session. */
521
+ id: string;
1317
522
  model?: string;
1318
- /** Model ids this profile offers, for the dashboard's picker. Operator-declared
1319
- * rather than discovered: provider engines have no equivalent of the CLI's
1320
- * `supportedModels()`, and only the operator knows which ids their endpoint and
1321
- * key actually serve. Unset → the picker offers {@link ProviderConfig.model} alone. */
1322
- models?: string[]; /** Base URL for OpenAI-compatible providers. */
1323
- baseUrl?: string; /** Environment variable the operator put the key in. Never the key itself. */
523
+ models?: string[];
524
+ baseUrl?: string;
1324
525
  apiKeyEnv?: string;
1325
526
  };
1326
- /**
1327
- * A grantable capability of the model-agnostic engine, named after the tool it
1328
- * yields. The always-present tools (`fs_*`, `eval_script`) are not listed: they
1329
- * are the engine's scratch filesystem and sandbox, not a grant.
1330
- */
1331
527
  type SessionCapability = 'web_search' | 'download' | 'web_fetch' | 'deliver_file';
1332
- /**
1333
- * What sessions under a `provider` profile get, declared by the operator. Meaning-
1334
- * less for `claude` profiles, whose equivalents live in the config directory.
1335
- *
1336
- * MCP servers are named, never configured, here: a server's transport config can
1337
- * carry credentials in its headers, and this type is served by `GET /profiles`.
1338
- * The names refer to servers the host connected in `createEngineRunner`, which is
1339
- * where the configs (and the credentials) stay.
1340
- */
1341
528
  type ProfileSessionDefaults = {
1342
- /** Capabilities granted to sessions under this profile. Absent = no
1343
- * declaration, so a session gets whatever backends the host wired. A session
1344
- * request may narrow this set, never widen it. */
1345
529
  capabilities?: SessionCapability[];
1346
- /** MCP servers, by name, whose tools sessions under this profile may use.
1347
- * Absent = no declaration (every server the host connected). */
1348
- mcpServers?: string[]; /** Prepended to the session's system prompt. */
530
+ mcpServers?: string[];
1349
531
  instructions?: string;
1350
532
  };
1351
- /**
1352
- * One rate-limit window of a profile's plan, as the *gateway* last saw it — the
1353
- * newest {@link RateLimitInfo} any session on the profile reported, across every
1354
- * session, live or since closed. The profile is the account boundary (one config
1355
- * dir / codex home / provider key = one plan), so this is the single usage state
1356
- * per account, where a session's own transcript only knows what *it* was last
1357
- * told.
1358
- *
1359
- * Two rules a client must keep:
1360
- * - An absent window (or an absent {@link ProfileInfo.usage} entirely) is
1361
- * **unknown, not 0%** — render nothing, exactly as for session-level readings.
1362
- * The map is in-memory and starts empty on a cold server.
1363
- * - `inferredReset` marks a reading the server zeroed at serve time because the
1364
- * reading's own `resetsAt` passed with nothing newer: the pre-reset number is
1365
- * then provably wrong, and 0 is the truthful *floor* (the account may have
1366
- * been used outside this gateway since). Distinguishable on the wire from an
1367
- * engine-reported 0, which carries no flag.
1368
- */
1369
533
  type ProfileUsageWindow = {
1370
- /** The reading, exactly as the session event carried it — except after an
1371
- * elapsed reset, when `utilization` is 0 and `resetsAt` is dropped (the old
1372
- * one names the *previous* window; a countdown from it would be nonsense). */
1373
534
  info: RateLimitInfo;
1374
- /** Epoch ms of the event that carried the reading — honest for "Updated …"
1375
- * lines even when the served utilization is inferred. */
1376
- updatedAt: number; /** Present (true) only on the served-as-0 inference described above. */
535
+ updatedAt: number;
1377
536
  inferredReset?: boolean;
1378
537
  };
1379
- /** Per-window plan usage, keyed by `rateLimitType` ('five_hour', 'seven_day',
1380
- * ...) — the same keying as a transcript's rate-limit state. */
1381
538
  type ProfileUsage = Record<string, ProfileUsageWindow>;
1382
539
  type ProfileInfo = {
1383
- /** Unique name, used as {@link CreateSessionRequest.profile}. */name: string;
1384
- /** Engine this profile runs on. Defaults to 'claude' when absent, so profiles
1385
- * written before provider support keep working unchanged. */
540
+ name: string;
1386
541
  engine?: ProfileEngine;
1387
- /** Absolute path set as CLAUDE_CONFIG_DIR for the session's CLI process.
1388
- * Required for 'claude' profiles; meaningless for the other engines. */
1389
542
  configDir?: string;
1390
- /** Codex profiles: absolute path set as CODEX_HOME for the session's codex
1391
- * process (auth, config.toml, thread storage) — the `configDir` analogue,
1392
- * request-writable like it. Unset = the binary's own `~/.codex`. */
1393
- codexHome?: string; /** Provider wiring for 'provider' profiles. */
543
+ codexHome?: string;
1394
544
  provider?: ProviderConfig;
1395
545
  description?: string;
1396
- defaults?: ProfileDefaults; /** Provider-engine session grants (capabilities, MCP servers, instructions). */
546
+ defaults?: ProfileDefaults;
1397
547
  session?: ProfileSessionDefaults;
1398
- /** Response-only: the engine's model catalog, shipped with the release and
1399
- * served from the first request (no process spawned, no warm-up session).
1400
- * For provider profiles the ids come from `provider.models` instead. Never
1401
- * contains a 'default' sentinel row — forms add their own "Profile default"
1402
- * row mapping to an unset model. Ignored on the way in. */
1403
548
  models?: ModelOption[];
1404
- /** Response-only: what this profile's default model resolves to. For claude
1405
- * profiles this is the operator's CLI config — unknowable statically — so it
1406
- * is absent until a session on this profile reports it. */
1407
549
  defaultModel?: string;
1408
- /** Response-only: the engine's capability record (see {@link EngineCapabilities}).
1409
- * Absent = use ENGINE_CAPABILITIES[engine]. Ignored on the way in. */
1410
550
  capabilities?: EngineCapabilities;
1411
- /** Response-only: whether the profile's credentials probe as usable right now.
1412
- * Absent = unknown/unchecked — treat as available. **Display-only**: create
1413
- * against an unavailable profile still proceeds and fails with the engine's
1414
- * own error (the probe can be stale in both directions). */
1415
551
  available?: boolean;
1416
- /** Response-only: one operator-actionable line, present only when
1417
- * `available === false`. */
1418
552
  unavailableReason?: string;
1419
- /** Response-only: the plan's rate-limit windows as last reported by any
1420
- * session on this profile (see {@link ProfileUsageWindow}). Absent = unknown
1421
- * — no session has reported yet (API-key sessions never do), or the server
1422
- * restarted. **Display-only**, like `available`: never a gate. */
1423
553
  usage?: ProfileUsage;
1424
- /** Response-only, computed by the server: this profile came from the profile
1425
- * store and can be edited or deleted through the API. Profiles declared in
1426
- * server options are absent/false — they are code. Ignored on the way in. */
1427
554
  managed?: boolean;
1428
555
  };
1429
- /**
1430
- * Curated, read-only snapshot of what a profile's config directory contains —
1431
- * the parts relevant to running worker sessions. Values that could carry secrets
1432
- * (env var values) never leave the server; only names are listed.
1433
- */
1434
556
  type ProfileConfigSnapshot = {
1435
- /** From the config dir's settings.json; absent when missing or unparseable. */settings?: {
1436
- /** Configured default model. */model?: string; /** permissions.defaultMode — the CLI's default permission mode. */
1437
- defaultPermissionMode?: string; /** Rule counts from permissions.allow / ask / deny. */
557
+ settings?: {
558
+ model?: string;
559
+ defaultPermissionMode?: string;
1438
560
  permissionRules?: {
1439
561
  allow: number;
1440
562
  ask: number;
1441
563
  deny: number;
1442
- }; /** Env var NAMES declared in settings.json env (values never included). */
1443
- envKeys?: string[]; /** Hook event names with at least one hook configured. */
564
+ };
565
+ envKeys?: string[];
1444
566
  hooks?: string[];
1445
- }; /** CLAUDE.md (user memory) present in the config dir. */
1446
- hasUserMemory: boolean; /** Skill names (skills/<name>/). */
1447
- skills: string[]; /** Agent names (agents/<name>.md). */
1448
- agents: string[]; /** Custom slash-command names (commands/<name>.md). */
567
+ };
568
+ hasUserMemory: boolean;
569
+ skills: string[];
570
+ agents: string[];
1449
571
  commands: string[];
1450
572
  };
1451
573
  type McpServerConfigWire = {
@@ -1462,9 +584,6 @@ type McpServerConfigWire = {
1462
584
  url: string;
1463
585
  headers?: Record<string, string>;
1464
586
  };
1465
- /** One tool an MCP server exposes, as the session's engine reports it.
1466
- * Parameters are deliberately absent: the CLI's status payload names and
1467
- * describes each tool but does not carry its input schema. */
1468
587
  type McpServerToolInfo = {
1469
588
  name: string;
1470
589
  description?: string;
@@ -1473,224 +592,64 @@ type McpServerToolInfo = {
1473
592
  destructive?: boolean;
1474
593
  openWorld?: boolean;
1475
594
  };
1476
- /**
1477
- * The tool's JSON Schema, where the engine reports one. **Engine-dependent,
1478
- * and that is not an oversight**: the Agent SDK's `McpServerStatus` names and
1479
- * describes each tool but carries no schema at all, while codex's
1480
- * `mcpServerStatus/list` returns the full one. So a client renders parameters
1481
- * where they exist and says they are unavailable where they don't — rather
1482
- * than either leaving a silent gap or claiming the absence is universal.
1483
- *
1484
- * Opaque on purpose: this is a JSON Schema document, not a shape this
1485
- * protocol models.
1486
- */
1487
595
  inputSchema?: unknown;
1488
596
  };
1489
- /**
1490
- * Live status of one MCP server on a session — what `GET
1491
- * {basePath}/sessions/:id/mcp` answers with, and what the `/mcp` screens render.
1492
- *
1493
- * The connection *identity* is here (transport, command, url, scope) but never
1494
- * its secrets: the engine's config carries `env` for stdio servers and `headers`
1495
- * for HTTP/SSE ones, and both are dropped on the way out. A client that can read
1496
- * this is not thereby entitled to the tokens the operator configured.
1497
- */
1498
597
  type McpServerStatusInfo = {
1499
598
  name: string;
1500
- /** 'connected' | 'failed' | 'needs-auth' | 'pending' | 'disabled' — kept open,
1501
- * the engine's set may grow. */
1502
- status: string; /** Where the server was configured: 'project' | 'user' | 'local' | 'dynamic' | … */
1503
- scope?: string; /** Present when `status` is 'failed'. */
1504
- error?: string; /** Name and version the server announced on connect. */
599
+ status: string;
600
+ scope?: string;
601
+ error?: string;
1505
602
  serverInfo?: {
1506
603
  name: string;
1507
604
  version: string;
1508
605
  };
1509
- transport?: 'stdio' | 'http' | 'sse' | 'sdk'; /** stdio only. */
606
+ transport?: 'stdio' | 'http' | 'sse' | 'sdk';
1510
607
  command?: string;
1511
- /** stdio only. Secrets do occasionally ride argv; the operator's own client
1512
- * shows them, and hiding them here would only mislead. `env` is not exposed. */
1513
- args?: string[]; /** http/sse only. */
1514
- url?: string; /** Present when connected. */
608
+ args?: string[];
609
+ url?: string;
1515
610
  tools?: McpServerToolInfo[];
1516
611
  };
1517
612
  type McpServersResponse = {
1518
613
  servers: McpServerStatusInfo[];
1519
614
  };
1520
- /** `POST {basePath}/sessions/:id/mcp/:name` — answers with the refreshed status. */
1521
615
  type McpServerActionRequest = {
1522
616
  action: 'reconnect' | 'enable' | 'disable';
1523
617
  };
1524
- /** `POST {basePath}/sessions/:id/attachments?name=<name>` — the body is the raw
1525
- * file, the `content-type` header its media type. Answers with the reference to
1526
- * name on the next `user_message`. */
1527
618
  type UploadAttachmentResponse = {
1528
619
  attachment: MessageAttachment;
1529
620
  };
1530
621
  type CreateSessionRequest = {
1531
- /** Directory the session is rooted at. Required for any engine whose
1532
- * capability record declares {@link EngineCapabilities.hostCwd} — `cwd` is
1533
- * per-query in the SDK and the server re-pins it on every call. Omittable for
1534
- * an engine that has no host filesystem at all (the provider engine, whose
1535
- * tools run against the in-memory VFS): there the field would be a required
1536
- * lie, and `allowedCwdRoots` would look like a sandbox boundary it is not. */
1537
622
  cwd?: string;
1538
- /** Profile (named Claude Code config dir) to run under. Required when the server
1539
- * declares more than one profile; implicit when exactly one exists. */
1540
- profile?: string; /** Optional initial prompt (may be a skill invocation like "/verify-content 123"). */
623
+ profile?: string;
1541
624
  prompt?: string;
1542
625
  permissionMode?: PermissionMode;
1543
- /** Pre-authorize 'bypassPermissions' (the CLI's --dangerously-skip-permissions
1544
- * capability) so the mode can be switched on mid-session. Without it the CLI
1545
- * rejects `set_permission_mode: 'bypassPermissions'` on a running session.
1546
- * Implied when `permissionMode` is already 'bypassPermissions'. */
1547
626
  allowDangerouslySkipPermissions?: boolean;
1548
627
  allowedTools?: string[];
1549
628
  disallowedTools?: string[];
1550
629
  mcpServers?: Record<string, McpServerConfigWire>;
1551
- /** Which filesystem settings the session loads. Include 'project' to pick up the
1552
- * target repo's skills and CLAUDE.md ("close-to-real" fidelity). */
1553
630
  settingSources?: Array<'user' | 'project' | 'local'>;
1554
631
  model?: string;
1555
632
  maxTurns?: number;
1556
- maxBudgetUsd?: number; /** Resume an existing SDK session by id. */
1557
- resume?: string; /** With `resume`: fork to a new session id instead of continuing. */
633
+ maxBudgetUsd?: number;
634
+ resume?: string;
1558
635
  forkSession?: boolean;
1559
- /** Reasoning effort for the session's model (codex engine). Open string —
1560
- * offerable values come from the profile's catalog/capability record. The
1561
- * gateway 400s it when the engine's record declares no `reasoningEfforts`. */
1562
- reasoningEffort?: string; /** Emit `stream_delta` events for token-by-token rendering. Default true. */
1563
- includePartialMessages?: boolean; /** Per-session override of the server's permission-request timeout (ms). */
1564
- approvalTimeoutMs?: number; /** AskUserQuestion handling (see {@link QuestionBehavior}). Default 'ask'. */
636
+ reasoningEffort?: string;
637
+ includePartialMessages?: boolean;
638
+ approvalTimeoutMs?: number;
1565
639
  questionBehavior?: QuestionBehavior;
1566
- /** Provider engine only: run with fewer capabilities than the profile grants
1567
- * (see {@link ProfileSessionDefaults.capabilities}). Narrowing only — naming a
1568
- * capability the profile does not grant is a 400, not a silent upgrade. */
1569
- capabilities?: SessionCapability[]; /** Free-form metadata echoed back on SessionInfo (host app bookkeeping). */
640
+ capabilities?: SessionCapability[];
1570
641
  meta?: Record<string, unknown>;
1571
- /**
1572
- * Opaque string tags naming what this session *belongs to* — the gateway's
1573
- * only intra-deployment scoping primitive. Assigned at create, **immutable
1574
- * afterwards** (no route writes it), echoed on {@link SessionInfo}, and
1575
- * carried through parking/dormancy so a restart cannot un-scope a session.
1576
- *
1577
- * WorkerDeck never interprets a key: an embedder writes `{ space, user }` or
1578
- * `{ tenant }` or nothing at all. What the tags *mean* is the host's
1579
- * `authorizeSession` predicate; absent one, the default rule is that every
1580
- * key the authenticated principal pins must match here (an unset principal
1581
- * scope is unrestricted — the same "unset means all" rule `allowedProfiles`
1582
- * uses, so an operator's dashboard keeps working unchanged).
1583
- *
1584
- * NOT `meta`: `meta` is free-form, client-settable and echoed, and an
1585
- * enforcement rule whose input the caller supplies is not an enforcement
1586
- * rule. Values are visible to any principal the policy admits a session to,
1587
- * so use opaque ids rather than names you would not show that audience.
1588
- */
1589
642
  scope?: Record<string, string>;
1590
643
  };
1591
- /**
1592
- * One sub-agent (a `Task` call and the sidechain it spawned), as a *list* surface
1593
- * sees it — without attaching.
1594
- *
1595
- * Sub-agent work is otherwise attach-only: it exists on the wire as
1596
- * `parentToolUseId` on three event bodies, and is reconstructed into rows by the
1597
- * react reducer and grouped per-Task by `terminalBlocks`. A sessions list never
1598
- * attaches (one live attach per session, owned by the panel), so it reads
1599
- * `SessionInfo` over REST and would otherwise have no way to know a session has
1600
- * six agents running inside one turn.
1601
- *
1602
- * This is a **runner-owned rollup computed at read time**, exactly like
1603
- * {@link SessionInfo.pendingPermissionCount}: it is not an event, it is not
1604
- * persisted separately, and it therefore rides the REST list, the WS attach
1605
- * snapshot and parking snapshots for free.
1606
- *
1607
- * **The claude and codex engines both produce it; the provider engine's absence
1608
- * is the truth.** The AI SDK has no multi-agent primitive and no tool that runs
1609
- * a nested agent loop, so `parentToolUseId: null` on every provider event is
1610
- * honest. Codex's spawn signal is the `subAgentActivity` item, whose own `id` is
1611
- * the model's `spawn_agent` call id — a genuine tool-use id — so `toolUseId`
1612
- * keeps its documented meaning there: the codex runner authors the anchor
1613
- * `tool_use` itself and keys every event of the agent's *thread* to it
1614
- * (`engines/codex/subagents.ts`). The earlier version of this comment asserted
1615
- * codex had no sidechains; that was true of the exec era and has not been true
1616
- * for a while.
1617
- *
1618
- * It is deliberately **not** the input to `taskSummary`. That string is spelled
1619
- * from the absorbed transcript items and must stay that way, so a transcript
1620
- * replayed tomorrow spells the same line from the same items it holds today.
1621
- */
1622
644
  type SubagentInfo = {
1623
- /** The `tool_use` id of the `Task` call that spawned it — the same id its
1624
- * nested events carry as `parentToolUseId`, and therefore the handle a client
1625
- * uses to jump to that Task's row. */
1626
- toolUseId: string; /** The Task input's `subagent_type` (e.g. "Explore"), when it named one. */
645
+ toolUseId: string;
1627
646
  agentType?: string;
1628
- /** The Task input's short `description`, clipped by the runner. Together with
1629
- * `agentType` this is what makes two parallel sub-agents tell apart in a list;
1630
- * a row reading only `Task` answers nothing. */
1631
647
  description?: string;
1632
- /**
1633
- * `running` until the Task's own `tool_result` arrives, then `done`/`failed`
1634
- * from that result's `is_error`. A turn that ends without that result — an
1635
- * interrupt, a session error, a turn or budget cap — settles what is still
1636
- * running as `failed`: the report never came, which is the one thing `done`
1637
- * could have claimed, and a `running` badge on an idle session would be a
1638
- * lie a list re-renders at every poll.
1639
- *
1640
- * Deliberately **narrower than `taskFailed`** in `@workerdeck/ui`'s
1641
- * `tool-run.ts`, which reddens a Task row when *any child call* failed. That is
1642
- * right for a transcript row the reader can expand — the failure is one press
1643
- * away and hiding it would be worse. It is wrong for a list: a grep that
1644
- * matched nothing inside an otherwise successful Explore agent would put
1645
- * `failed` beside the session's name with nothing to open. So this reports the
1646
- * sub-agent's own outcome. If you are here to "fix" the inconsistency, this is
1647
- * the reason it exists.
1648
- */
1649
- status: 'running' | 'done' | 'failed'; /** Epoch ms the `Task` call was emitted. */
648
+ status: 'running' | 'done' | 'failed';
1650
649
  startedAt: number;
1651
- /** Tool calls the sub-agent has made so far — its progress reading while
1652
- * running, counted from nested `tool_use` blocks. */
1653
650
  toolCount: number;
1654
651
  };
1655
- /**
1656
- * How many *settled* sub-agents {@link SessionInfo.subagents} keeps behind the
1657
- * running ones. Small on purpose: the point of the tail is that a list row does
1658
- * not go blank the instant a run finishes, not that it is a history.
1659
- */
1660
652
  declare const SUBAGENT_HISTORY = 8;
1661
- /**
1662
- * A project's icon, as declared by its `.workerdeck.json` — either a named
1663
- * glyph or a reference to an image the gateway serves.
1664
- *
1665
- * A discriminated union rather than one stringly field, because the two arms
1666
- * have opposite render paths: a glyph is looked up in the client's own icon
1667
- * set with no I/O, an image is a fetch. Collapsing them would put "is this a
1668
- * name or an address" back on every renderer, which is the inference this
1669
- * family keeps refusing (`ImageRefPart` is a new part type, never a
1670
- * hollowed-out `image`, for the same reason).
1671
- *
1672
- * `glyph.name` is a lucide icon name, validated by the gateway for *shape*
1673
- * only (lowercase kebab-case): the gateway has no lucide catalog and must not
1674
- * grow one — icon sets version independently of this protocol. A client whose
1675
- * set lacks the name renders its no-project fallback rather than erroring;
1676
- * an unknown name is a stale row, never withheld state.
1677
- *
1678
- * `image` carries an **address, never bytes** — the attachment-bytes rule.
1679
- * `SessionInfo` rides every row of `GET /sessions`, which clients poll at 1.2s
1680
- * while anything is working, so an inlined base64 icon would be paid for on
1681
- * every poll of every session forever (the same argument that keeps
1682
- * `originalFile` off {@link FilePatch} and message bytes off events). The
1683
- * bytes come from `GET {basePath}/sessions/:id/project/icon` — session-scoped
1684
- * on purpose, so the fetch rides the same `canSee` gate as every other
1685
- * `/sessions/:id/*` route and a scoped principal's miss is the uniform 404. A
1686
- * project-keyed route would need the project root in the URL, and a route
1687
- * addressed by host paths is an existence oracle for the gateway's
1688
- * filesystem. `hash` (sha256 hex of the bytes) is the cross-session cache
1689
- * key: two sessions in one project serve identical bytes, so a client caches
1690
- * by hash rather than by URL and fetches once per project, not once per
1691
- * session. The route answers with `ETag: "<hash>"` and honors
1692
- * `If-None-Match`.
1693
- */
1694
653
  type ProjectIcon = {
1695
654
  type: 'glyph';
1696
655
  name: string;
@@ -1699,381 +658,74 @@ type ProjectIcon = {
1699
658
  mediaType: 'image/png' | 'image/svg+xml';
1700
659
  hash: string;
1701
660
  };
1702
- /**
1703
- * Project identity for a session — what a `.workerdeck.json` in the session's
1704
- * ancestry declares, resolved by the **gateway** and shipped on
1705
- * {@link SessionInfo.project}.
1706
- *
1707
- * The gateway reads the file, not each client: the iOS app and a browser
1708
- * pointed at a remote gateway have no access to that filesystem, so a
1709
- * per-client reader would make the feature exist on exactly one client.
1710
- * Discovery is an ancestor walk from the session's `cwd` upward — nearest
1711
- * `.workerdeck.json` wins, so a session started in `packages/ui` still says
1712
- * "WorkerDeck" — over the *realpath'd* cwd, which is what makes `root`
1713
- * canonical below.
1714
- *
1715
- * The file's schema, stated here because this type is its wire projection
1716
- * (clients never read the file; the gateway is its only parser):
1717
- *
1718
- * ```json
1719
- * { "name": "WorkerDeck", "icon": "layers" }
1720
- * { "name": "WorkerDeck", "icon": "./docs/assets/icon.png" }
1721
- * ```
1722
- *
1723
- * Both keys optional, unknown keys ignored (forward compatibility). An empty
1724
- * `{}` still marks its directory as the project root — grouping is the point,
1725
- * and the name falls back to the root's basename. `icon` is one string with a
1726
- * total classification rule: a value ending in `.png`/`.svg`
1727
- * (case-insensitive) is a repo-relative image path — relative only, since the
1728
- * file is checked into a repo that clones onto other machines, where an
1729
- * absolute path is wrong by construction — and anything else must be a
1730
- * lucide-shaped glyph name (`^[a-z0-9]+(-[a-z0-9]+)*$`) or it is ignored. The
1731
- * two shapes cannot collide (a glyph name contains no dot), so the rule is a
1732
- * classification, not a guess. Every degradation degrades *fieldwise and
1733
- * silently*: a malformed or oversized file is skipped and the walk continues
1734
- * to an ancestor (a broken nested file must not shadow the repo root's valid
1735
- * one), a junk name falls back to the basename, a junk or escaping icon is
1736
- * dropped — a session must never fail, or even warn, because of a display
1737
- * declaration.
1738
- *
1739
- * `root` — the canonical (realpath'd) absolute directory holding the file, on
1740
- * the **gateway's** filesystem — is the grouping key: two sessions are in the
1741
- * same project iff same root *on the same gateway* (a remote gateway's
1742
- * identical-looking path is another machine's directory — the `ScopeRoot`
1743
- * argument). A *name* is not a key: two repos can both be called "api".
1744
- * Canonicalizing at discovery is what makes two differently-spelled cwds of
1745
- * one project agree on it.
1746
- *
1747
- * Resolved at **serve time** from a TTL cache, never persisted — the same
1748
- * placement argument as the profile tracker's 0%-after-reset inference: it is
1749
- * a function of the gateway's current filesystem, and a copy captured into a
1750
- * parking record would replay a stale name forever. Editing the file shows up
1751
- * on every session in the project within the TTL, with no migration and no
1752
- * event.
1753
- *
1754
- * Additive at protocol **7**: an optional field on `SessionInfo`, where
1755
- * absent means exactly what today's wire means (no project declared — render
1756
- * the folder basename), an old client ignores it, and a new client against an
1757
- * old gateway sees absent. The icon route is likewise unreachable by
1758
- * accident: a client only fetches it when this gateway told it an image
1759
- * exists.
1760
- */
1761
661
  type ProjectInfo = {
1762
- /** Display name — the file's `name`, else the root's basename. Never empty. */name: string;
1763
- /** Canonical absolute path of the directory holding `.workerdeck.json`, on
1764
- * the gateway's filesystem. The grouping key (per gateway); an opaque string
1765
- * to clients beyond equality and display. */
662
+ name: string;
1766
663
  root: string;
1767
- /** Absent = the file declared none (or declared one the gateway refused —
1768
- * malformed, escaping, oversized — which a client cannot and must not
1769
- * distinguish). */
1770
664
  icon?: ProjectIcon;
1771
665
  };
1772
666
  type SessionInfo = {
1773
- /** Server-assigned id (stable across SDK session forks/resumes). */id: string; /** Underlying Agent SDK session id, once known; use for `resume`. */
667
+ id: string;
1774
668
  sdkSessionId?: string;
1775
669
  status: SessionStatus;
1776
- /** Empty string for a session whose engine has no host filesystem (see
1777
- * {@link CreateSessionRequest.cwd}). Deliberately not optional: every client
1778
- * renders and searches it, and a synthetic path would send the workspace and
1779
- * `@file` search probing a directory that does not exist. */
1780
- cwd: string; /** Profile the session runs under (resolved name, present even when implicit). */
670
+ cwd: string;
1781
671
  profile?: string;
1782
- /** Engine actually running this session, reported by the runner itself. Lets a
1783
- * session surface gate CLI-only affordances (permission modes, context usage,
1784
- * rate limits) without looking the profile back up. Absent = 'claude'. */
1785
672
  engine?: ProfileEngine;
1786
- /** The engine's capability record, reported by the runner like `engine`. The
1787
- * attach snapshot is the session-level source (no event carries it). Absent =
1788
- * ENGINE_CAPABILITIES[engine]. */
1789
673
  capabilities?: EngineCapabilities;
1790
674
  model?: string;
1791
675
  permissionMode?: PermissionMode;
1792
- /** Whether this session may be switched into `bypassPermissions`. The CLI only
1793
- * allows it when the process was spawned for it, so it is decided at creation
1794
- * and never changes: a session that did not ask for bypass up front cannot
1795
- * gain it later. Lets a picker disable the mode instead of offering a switch
1796
- * the engine will refuse. Absent = unknown (an older server). */
1797
- canBypassPermissions?: boolean; /** See the `system_init` event; 'oauth' = claude.ai subscription credentials. */
676
+ canBypassPermissions?: boolean;
1798
677
  apiKeySource?: string;
1799
- createdAt: number; /** Highest event seq emitted so far; attach with `afterSeq` to catch up. */
678
+ createdAt: number;
1800
679
  lastSeq: number;
1801
680
  pendingPermissionCount: number;
1802
- /**
1803
- * Sub-agents this session has running, plus a short tail of settled ones — see
1804
- * {@link SubagentInfo}. Absent on an engine that has no sidechains and on an
1805
- * older server; **absent and empty mean the same thing to a client**, so render
1806
- * nothing rather than "0 sub-agents".
1807
- *
1808
- * Bounded on purpose. This rides every row of `GET /sessions`, which a busy
1809
- * client polls at 1.2s, and it is captured into parking snapshots — the same
1810
- * attachment-bytes rule that keeps whole files off {@link FilePatch}. Every
1811
- * *running* sub-agent is always present (they are the live reading and there
1812
- * are never many at once); settled ones are kept newest-first to
1813
- * {@link SUBAGENT_HISTORY} and then dropped, so a day-long session with two
1814
- * hundred Tasks does not grow an unbounded field. A client must therefore not
1815
- * treat this as the session's full Task history — the transcript is that.
1816
- */
1817
681
  subagents?: SubagentInfo[];
1818
- meta?: Record<string, unknown>; /** Display title: `meta.title` if the host set one, else derived (e.g. first prompt). */
1819
- title?: string; /** Cumulative cost across all turns so far (sum of turn_result totals). */
1820
- totalCostUsd?: number; /** Cumulative turn count across the session. */
682
+ meta?: Record<string, unknown>;
683
+ title?: string;
684
+ totalCostUsd?: number;
1821
685
  numTurns?: number;
686
+ activityCount?: number;
1822
687
  /**
1823
- * How many transcript rows this session has produced (see
1824
- * {@link transcriptActivity}) — a monotonic counter a client can diff against
1825
- * a remembered value to answer "how much happened while I wasn't looking",
1826
- * without attaching.
1827
- *
1828
- * `numTurns` cannot answer it: five tool calls inside one turn are one turn.
1829
- * `lastSeq` cannot either — it counts every event, and with token streaming on
1830
- * that is hundreds per reply. Absent on an older server; a client should fall
1831
- * back to `numTurns` rather than showing nothing.
1832
- *
1833
- * Monotonic for the session's whole life, **including across a
1834
- * `conversation_reset`**: after a `/clear` this deliberately exceeds the
1835
- * number of rows a fresh attach renders. It is an unread *cursor* diffed
1836
- * against stored monotonic watermarks (see `watermarks.ts`) — resetting it to
1837
- * the new row count would leave every stored mark above it, and that
1838
- * session's badge dead until the count caught back up.
688
+ * Rows of the kind a person is actually waiting to read — see `transcriptProse`.
689
+ * Absent from a gateway that predates it — additive, so no `PROTOCOL_VERSION` bump —
690
+ * which is why every reader falls back to `activityCount`. This is the badge's number; `activityCount` stays the "has
691
+ * anything happened at all" measure that sorting and dormancy read.
1839
692
  */
1840
- activityCount?: number; /** Epoch ms of the most recent emitted event. */
693
+ proseCount?: number;
1841
694
  lastActivityAt?: number;
1842
- /**
1843
- * The session's latest context-window reading — see {@link ContextReading}.
1844
- *
1845
- * The same number the session screen draws, served on the list so a row can
1846
- * show where a session is bloating **without attaching to it**. That is the
1847
- * whole reason it is here: context fill is the one session metric you want
1848
- * across *all* sessions at once, and until now it existed only as an event on
1849
- * an attached socket.
1850
- *
1851
- * Retained by the runner from the last `context_usage` it emitted, so it is
1852
- * exactly what the transcript last showed — never recomputed on the serve
1853
- * path, which would be a second answer to a question that already has one.
1854
- * Absent until the first reading (a promptless session has none), and cleared
1855
- * by a `conversation_reset` for the same reason the transcript state clears
1856
- * it: the old window is not this conversation's.
1857
- */
1858
695
  contextUsage?: ContextReading;
1859
- /** Opaque scope tags this session was created with — see
1860
- * {@link CreateSessionRequest.scope}. Echoed by the runner, re-stamped by the
1861
- * gateway, and never editable. */
1862
696
  scope?: Record<string, string>;
1863
- /**
1864
- * Project identity discovered from the session's `cwd` — see
1865
- * {@link ProjectInfo}. Stamped by the **gateway at serve time** (runners
1866
- * never set it; a runner-echoed value would be persisted into parking
1867
- * records and replay a stale name forever). Absent = no `.workerdeck.json`
1868
- * in the cwd's ancestry, and also = an older gateway: both mean "render the
1869
- * folder basename", which is exactly today's behaviour.
1870
- */
1871
697
  project?: ProjectInfo;
1872
698
  };
1873
- /**
1874
- * The list-sized context reading an event carries, or `undefined` for the events
1875
- * that carry none — the rule behind {@link SessionInfo.contextUsage}.
1876
- *
1877
- * Here rather than in each runner for the same reason {@link transcriptActivity}
1878
- * is: it is one rule both sides have to agree on, and three copies of "which
1879
- * events move the reading" is three chances to disagree. Runners fold it in
1880
- * their emit path; **clearing on `conversation_reset` is the caller's half** —
1881
- * this function answers "what does this event say the reading is", and a reset
1882
- * says nothing about the window, it retires the conversation the window
1883
- * described.
1884
- */
1885
699
  declare function contextReading(body: SessionEventBody): ContextReading | undefined;
1886
- /**
1887
- * How many transcript rows an event materializes — the unit behind
1888
- * {@link SessionInfo.activityCount}.
1889
- *
1890
- * Deliberately the *reducer's* rule (`@workerdeck/react`'s `transcript.ts`), not
1891
- * a server-side approximation: one row per content block of an assistant
1892
- * message (a text, a thought, each tool call), one for a user message, one per
1893
- * turn result, delivered file or error. Everything else — status changes, usage
1894
- * readings, stream deltas, permission bookkeeping — is state, not a row, and
1895
- * counts zero.
1896
- *
1897
- * It lives in `protocol` because both sides need it and neither may import the
1898
- * other: the runners count with it, and any client compares the totals. If the
1899
- * reducer's row rule changes, change this with it.
1900
- */
1901
700
  declare function transcriptActivity(body: SessionEventBody): number;
1902
701
  /**
1903
- * Whether an event is **transcript content** — whether the reducer
1904
- * (`@workerdeck/react`'s `transcript.ts`, and its Swift mirror) mutates
1905
- * `items` when it applies it. The rule behind `conversation_reset`'s replay
1906
- * semantics: the runner keeps its whole event log, but `subscribe()` skips
1907
- * content below the latest reset so an attaching client does not resurrect a
1908
- * cleared conversation — while every *state-bearing* event (`system_init`,
1909
- * `capabilities`, `skills`, `status_changed`, usage and rate-limit readings,
1910
- * `file_produced`, permission bookkeeping) still replays, because a fresh
1911
- * attacher with no model list and no cwd is broken, not cleared.
1912
- *
1913
- * Deliberately **broader than `transcriptActivity() > 0`**: stream deltas,
1914
- * tool results (synthetic user messages) and execution lifecycle events count
1915
- * zero rows but still mutate items — replaying them across a reset would leave
1916
- * orphaned deltas and results with no parent message.
1917
- *
1918
- * `conversation_reset` itself is content under this rule, and that is load-
1919
- * bearing twice: a *superseded* reset (below a newer one) is skipped with the
1920
- * conversation it cleared, while the latest reset always replays (the skip is
1921
- * strictly-below), which is what clears a reconnecting client that still holds
1922
- * pre-reset rows.
1923
- *
1924
- * Lives here beside {@link transcriptActivity} for the same reason: the
1925
- * reducer owns the rule and the runners filter with it, and the two sides may
1926
- * not import each other. If the reducer's items-mutating set changes, change
1927
- * this with it. Unknown/future event types are NOT content — the safe failure
1928
- * is replaying a stale row, never withholding state.
1929
- */
702
+ * The unread badge's unit: output **addressed to the human**, not evidence of work.
703
+ *
704
+ * `transcriptActivity` counts a tool call and a paragraph alike, which is honest as
705
+ * "how much has happened" and wrong as "how much is there to read" — a session that
706
+ * tool-loops for a minute ticks 6, 7, 8 with nothing said yet. This scores the same
707
+ * events through a narrower door:
708
+ *
709
+ * - assistant `text` blocks only — `thinking` is not addressed to anyone and `tool_use`
710
+ * is the noise being filtered out;
711
+ * - the **sub-agent carve-out is inherited** (`parentToolUseId != null` scores 0): prose a
712
+ * sub-agent wrote to its parent is not addressed to the human either;
713
+ * - a `turn_result` counts only when it **failed**, an interrupt or an error being a thing
714
+ * the human is owed; a successful turn already carried its own prose and would otherwise
715
+ * double-count every answer;
716
+ * - `session_error` and `file_delivered` count — both are output, not work;
717
+ * - `stream_delta` scores 0, exactly as in `transcriptActivity`. The badge is therefore
718
+ * correct within one poll of a message *completing*, never mid-stream, which is the
719
+ * deliberate price of leaving the streaming path alone.
720
+ */
721
+ declare function transcriptProse(body: SessionEventBody): number;
1930
722
  declare function transcriptContent(body: SessionEventBody): boolean;
1931
- /**
1932
- * The dedupe key for an event that is **last-write-wins** on replay, or
1933
- * `undefined` for one that must always be delivered.
1934
- *
1935
- * The problem: the runner polls context usage and the plan's rate limits after
1936
- * every turn, so a fifty-turn session's log holds fifty context readings and
1937
- * fifty per rate-limit window. Replaying all of them is not merely wasteful —
1938
- * it is *visible*. A client applies each in turn, so opening a session shows
1939
- * the usage meters counting up from the session's first reading to its last
1940
- * over the length of the replay, announcing history as if it were news.
1941
- *
1942
- * The fix is a backwards scan over the buffered log keeping the first
1943
- * occurrence of each key, which is `staleReplaySeqs` in `@workerdeck/core`.
1944
- * The key is per *window* for rate limits, not per event type: the reducer
1945
- * stores them keyed by window ("so five_hour and seven_day updates don't
1946
- * clobber each other"), so a single key would keep only the most recently
1947
- * polled window and silently drop the others.
1948
- *
1949
- * **This is a claim about the reducer**, which is why it lives here rather
1950
- * than in core: only the server coalesces, but only `@workerdeck/react` can
1951
- * prove the rule correct, and neither package may import the other. The
1952
- * property that must hold is that coalescing is *unobservable* — folding the
1953
- * full log and the coalesced log through `applyEvent` yields identical state.
1954
- * `packages/react/test/replay-coalesce.test.ts` asserts exactly that, over
1955
- * every event kind. Extend the rule only with a case that test still passes.
1956
- *
1957
- * Three kinds are deliberately **excluded** despite looking eligible:
1958
- *
1959
- * - `capabilities` — `defaultModel: event.defaultModel ?? base.defaultModel`
1960
- * is a fallback *merge*, so a later event without one would erase an earlier
1961
- * event's. (It is also emitted once per session, so there is nothing to win.)
1962
- * - `model_changed` — `undefined` means "reset to the server default" and the
1963
- * reducer *keeps* the last known model, so the last event alone is not the
1964
- * same as the fold.
1965
- * - `system_init` — pure replace for the reducer, but the server's
1966
- * `watchAuthSource` reads the **first** one to decide an auth policy, and
1967
- * parking treats each as a resume point.
1968
- *
1969
- * Coalescing never drops the highest-seq event, and that is load-bearing
1970
- * rather than incidental: the globally-last event is by definition the last of
1971
- * its own key, so it always survives. `useClaudeSession`'s replay hold waits
1972
- * for `state.lastSeq` to reach the attach's `session.lastSeq`, and would hang
1973
- * on a blank panel forever if a coalescer could swallow the final event.
1974
- */
1975
723
  declare function replayCoalesceKey(body: SessionEventBody): string | undefined;
1976
- /**
1977
- * Does a **replay** have to deliver this event, or may it be dropped outright?
1978
- *
1979
- * The fifth of the family, and the closest relative of {@link snapshotRetains} —
1980
- * the same claim ("no client can tell") pointed at the wire instead of at a
1981
- * store. The difference from {@link replayCoalesceKey} is that this is not
1982
- * last-write-wins: there is nothing to keep. These are events the reducer reads
1983
- * and *discards*, so a replay that sends them is spending the reader's network
1984
- * on frames whose whole effect is `return base`.
1985
- *
1986
- * Today that is exactly one thing, and it is the second-largest item in a real
1987
- * attach: the `stream_delta`s the reducer does not model. Measured over one
1988
- * 1,270-row session, the delta run was 774 KB, and **~85% of it was frames the
1989
- * reducer throws away** — `input_json_delta` (a tool call's arguments, streamed
1990
- * character by character, 383 KB), `signature_delta` (encrypted-thinking
1991
- * signatures, 153 KB) and the `message_start`/`content_block_start`/`_stop`
1992
- * scaffolding (244 KB). The reducer models two delta kinds, `text_delta` and
1993
- * `thinking_delta`; everything else falls through its switch untouched.
1994
- *
1995
- * What is deliberately **not** dropped, though the arithmetic would allow it:
1996
- *
1997
- * - `thinking_delta` — the Claude SDK delivers thinking blocks whose `thinking`
1998
- * is `''`, and the reducer backfills them from the accumulated streamed text
1999
- * (`streamedThinking`). Dropping these erases every thought from a replayed
2000
- * transcript. This is the same carve-out `snapshotRetains` documents, and it
2001
- * is the reason that rule is provider-engine-only.
2002
- * - `text_delta` — superseded by the `assistant_message` that follows it, which
2003
- * filters the streaming id and rebuilds from the full content blocks. It could
2004
- * go, but only with a lookahead proving the message arrived, and at 24 KB in
2005
- * the measured session it is not worth a rule that has to be right about
2006
- * supersession. A merge is likewise not worth it: a *drop* needs no synthesized
2007
- * event and therefore no invented seq.
2008
- *
2009
- * A live event is never affected — this is about the buffered replay alone — and
2010
- * the caller must never drop the log's highest-seq event whatever this says, for
2011
- * the reason {@link replayCoalesceKey} gives: the replay hold waits for
2012
- * `state.lastSeq` to reach the attach's `session.lastSeq` and would hang on a
2013
- * blank panel forever.
2014
- *
2015
- * The property is the family's usual one and is a test rather than an argument:
2016
- * folding the full log and the retained log through `applyEvent` yields
2017
- * identical state (`packages/react/test/replay-retain.test.ts`).
2018
- */
2019
724
  declare function replayRetains(body: SessionEventBody): boolean;
2020
- /**
2021
- * Does a `RunnerSnapshot` keep this event in its persisted log?
2022
- *
2023
- * The fourth of the same family, and the same shape of claim as
2024
- * {@link replayCoalesceKey}: which events a *store* may drop without any client
2025
- * being able to tell. It exists because a snapshot embeds the whole event log,
2026
- * and a log is mostly stream deltas — a four-character token rides a ~180-byte
2027
- * JSON envelope, so the delta run is tens of times the size of the text it
2028
- * spells, sitting on disk *beside* the `assistant_message` that respells it in
2029
- * full. That was affordable while a snapshot was written once, at a park. It is
2030
- * not affordable written after every turn, which is what restart-survival needs.
2031
- *
2032
- * So: everything is retained except `stream_delta`. The reason that is safe is
2033
- * not that deltas are unimportant but that they are **superseded by
2034
- * construction**. The reducer upserts them under one constant id and the
2035
- * following `assistant_message` filters exactly that id out and rebuilds from
2036
- * the full content blocks — and a snapshot may only be taken at a rest point,
2037
- * where the stream loop has exited and flushed. Both exits flush, including the
2038
- * error path: an interrupted turn pushes its half-finished buffers into a
2039
- * durable `assistant_message` before it emits the failed `turn_result`. There is
2040
- * no rest state in which a delta is the only record of anything.
2041
- *
2042
- * **Provider engine only**, and this is the carve-out that must not be lost:
2043
- * against a *Claude* log the rule would be wrong. The Claude SDK delivers
2044
- * thinking blocks whose text is `''`, with the human-readable summary existing
2045
- * only in the delta stream, and the reducer carries the streamed text over to
2046
- * fill them (`transcript.ts`, the `streamedThinking` backfill). Dropping deltas
2047
- * there would silently erase every thought from a restored transcript. Today
2048
- * that is unreachable rather than merely avoided — only the provider engine
2049
- * implements `park()`/`snapshot()` at all, and `#restore` refuses a snapshot
2050
- * from another engine — but an engine that gains one inherits this obligation.
2051
- *
2052
- * Two properties hold it up, both of which are tests rather than arguments:
2053
- * folding the full log and the retained log through `applyEvent` yields
2054
- * identical state (`packages/react/test/snapshot-retain.test.ts`, the same
2055
- * property `replay-coalesce.test.ts` asserts), and the retained log's last event
2056
- * still carries the snapshot's own `seq`. The second matters more than it looks:
2057
- * `transcriptActivity(stream_delta)` is 0, so the count `#restore` recomputes
2058
- * from the log is bit-identical — a client's unread cursor cannot move — and the
2059
- * replay hold waits for `state.lastSeq` to reach the attach's `lastSeq`, which a
2060
- * rule that could drop the final event would hang forever.
2061
- */
2062
725
  declare function snapshotRetains(body: SessionEventBody): boolean;
2063
- /**
2064
- * A session in an engine's on-disk store (independent of this server's registry):
2065
- * the Agent SDK's session files, or a codex profile's CODEX_HOME threads. Listed
2066
- * so hosts can offer "resume" across server restarts: feed `sessionId` to
2067
- * CreateSessionRequest.resume — under a profile of the SAME engine, since the id
2068
- * only means something to the store it came from. `GET {basePath}/sdk-sessions`
2069
- * takes an optional `profile` query parameter naming whose store to list; absent,
2070
- * the profile is resolved implicitly when the server declares exactly one, else
2071
- * the Claude engine's store is listed (the pre-engine-aware behavior). Mirrors
2072
- * the SDK's SDKSessionInfo shape, kept browser-safe.
2073
- */
2074
726
  type SdkSessionSummary = {
2075
- sessionId: string; /** Custom title, auto summary, or first prompt — whichever the SDK has. */
2076
- summary: string; /** Epoch ms of last modification. */
727
+ sessionId: string;
728
+ summary: string;
2077
729
  lastModified: number;
2078
730
  createdAt?: number;
2079
731
  customTitle?: string;
@@ -2081,14 +733,10 @@ type SdkSessionSummary = {
2081
733
  gitBranch?: string;
2082
734
  cwd?: string;
2083
735
  };
2084
- /** One deliverable in the session's scratch filesystem (see the `file_delivered` event). */
2085
736
  type SessionFileInfo = {
2086
737
  path: string;
2087
738
  bytes: number;
2088
739
  };
2089
- /** `GET {basePath}/sessions/:id/files` — every file currently in the session's VFS.
2090
- * `GET {basePath}/sessions/:id/files/<path>` downloads one (attachment disposition).
2091
- * 404 when the session's engine exposes no VFS (Claude-engine sessions). */
2092
740
  type ListSessionFilesResponse = {
2093
741
  files: SessionFileInfo[];
2094
742
  };
@@ -2101,26 +749,12 @@ type CreateSessionResponse = {
2101
749
  type GetSessionResponse = {
2102
750
  session: SessionInfo;
2103
751
  };
2104
- /**
2105
- * Body of `PATCH {basePath}/sessions/:id` — the host-facing edits to a live
2106
- * session. Today that is only its display name: `title` writes `meta.title`,
2107
- * which {@link SessionInfo.title} prefers over the derived one, and `null` (or
2108
- * an empty string) clears the override so the derived title comes back. Nothing
2109
- * here reaches the engine — renaming does not speak to the model.
2110
- *
2111
- * 409 when the session is parked: a parked session has no runner to carry the
2112
- * change, and its snapshot is the host's to rewrite, not this route's.
2113
- */
2114
752
  type UpdateSessionRequest = {
2115
753
  title?: string | null;
2116
754
  };
2117
755
  type UpdateSessionResponse = {
2118
756
  session: SessionInfo;
2119
757
  };
2120
- /** Body of `POST {basePath}/sessions/:id/permissions/:requestId` — the REST counterpart
2121
- * of the WS `permission_decision` command, for remote controllers without a socket
2122
- * (e.g. answering a job's AskUserQuestion from a webhook consumer). 404 = the request
2123
- * is unknown, already resolved, or expired. */
2124
758
  type ResolvePermissionRequest = {
2125
759
  behavior: 'allow';
2126
760
  updatedInput?: Record<string, unknown>;
@@ -2132,17 +766,6 @@ type ResolvePermissionRequest = {
2132
766
  type ResolvePermissionResponse = {
2133
767
  resolved: true;
2134
768
  };
2135
- /**
2136
- * Body of `POST {basePath}/executions/:executionId/result` — the way a deferred
2137
- * executor (a remote worker, a batch job, a human) delivers the outcome of an
2138
- * execution the session parked on. The session is rehydrated if its runner was
2139
- * torn down, and the result is folded back into the agent loop; a `failed` result
2140
- * is ordinary tool output the agent adapts to, not a session error.
2141
- *
2142
- * Applied **idempotently by `executionId`**: a duplicate or late delivery (one
2143
- * racing the execution watchdog) answers 200 with `applied: false` rather than
2144
- * erroring or applying twice. 404 means no session is parked on that id.
2145
- */
2146
769
  type SubmitExecutionResultRequest = {
2147
770
  status: 'ok';
2148
771
  output: ToolExecutionOutput;
@@ -2154,129 +777,70 @@ type SubmitExecutionResultRequest = {
2154
777
  logs?: string[];
2155
778
  };
2156
779
  type SubmitExecutionResultResponse = {
2157
- /** False when the id was already settled — the delivery was a no-op. */applied: boolean; /** Session the execution belonged to. */
780
+ applied: boolean;
2158
781
  sessionId: string;
2159
782
  };
2160
783
  type ListSdkSessionsResponse = {
2161
784
  sdkSessions: SdkSessionSummary[];
2162
785
  };
2163
- /** `GET {basePath}/profiles` — filtered to the profiles the caller may use. */
2164
786
  type ListProfilesResponse = {
2165
787
  profiles: ProfileInfo[];
2166
- /** Whether this caller may create profiles here — true only when the server has
2167
- * a profile store AND the principal carries `canManageProfiles`. Lets a UI hide
2168
- * controls that would always be refused. */
2169
788
  canManage?: boolean;
2170
789
  };
2171
- /**
2172
- * `POST {basePath}/profiles` — create a managed profile. Available only when the
2173
- * server was given a profile store, and only to a principal with
2174
- * `canManageProfiles`. Profiles declared in server options are code, not data:
2175
- * they cannot be created, edited, or deleted through these routes.
2176
- */
2177
790
  type CreateProfileRequest = ProfileInfo;
2178
- /** `PATCH {basePath}/profiles/:name` — merge into a managed profile. The name is
2179
- * the route, not the body; pass `null` to clear an optional field. */
2180
791
  type UpdateProfileRequest = Omit<Partial<ProfileInfo>, 'name'>;
2181
- /**
2182
- * The **host's real project tree**, not a session's in-memory VFS — the two are
2183
- * unrelated despite both being "files". {@link SessionFileInfo} is a deliverable
2184
- * the agent produced inside a session; these routes read and write the operator's
2185
- * actual disk.
2186
- *
2187
- * That makes them **operator-privileged**: they are authorized by the server's auth
2188
- * key alone and deliberately sit outside the agent permission flow, because the
2189
- * caller *is* the operator, not the model. A client holding the key can already
2190
- * start a session with any allowed cwd; browsing that same tree grants it nothing
2191
- * new. Writing does, which is why writes are separately enabled server-side.
2192
- *
2193
- * The whole surface is opt-in and root-scoped: with no roots configured every route
2194
- * below 404s. There is no "unset means anything" default here — a phone on a tailnet
2195
- * must never be one request away from `~/.ssh`.
2196
- */
2197
792
  type HostFileRoot = {
2198
- /** Absolute, canonical (symlinks resolved) path of the root. */path: string; /** Last path segment, for display — roots are not named by the operator. */
793
+ path: string;
2199
794
  name: string;
2200
795
  };
2201
- /** `GET {basePath}/fs/roots` — where a client may start browsing. Empty `roots`
2202
- * never happens: the routes are absent entirely when none are configured. */
2203
796
  type ListHostRootsResponse = {
2204
- roots: HostFileRoot[]; /** Whether `PUT /fs/write` is enabled here; lets a UI hide an editor it can't save from. */
797
+ roots: HostFileRoot[];
2205
798
  canWrite: boolean;
2206
799
  };
2207
- /** One entry in a host directory listing. Classified with `lstat` semantics, so a
2208
- * `symlink` is reported as itself and never silently resolved — following it is the
2209
- * *next* request's problem, and that request is refused if it escapes the roots. */
2210
800
  type HostDirEntry = {
2211
- name: string; /** Absolute path, ready to pass back as `?path=`. */
801
+ name: string;
2212
802
  path: string;
2213
- type: 'file' | 'dir' | 'symlink' | 'other'; /** Regular files only. */
2214
- bytes?: number; /** Epoch ms mtime. */
803
+ type: 'file' | 'dir' | 'symlink' | 'other';
804
+ bytes?: number;
2215
805
  modifiedAt?: number;
2216
806
  };
2217
- /** `GET {basePath}/fs/list?path=<abs>` — one directory, not recursive. */
2218
807
  type ListHostDirResponse = {
2219
- /** Canonical path actually listed (the request's path after symlink resolution). */path: string; /** Directories first, then files, each alphabetical. */
2220
- entries: HostDirEntry[]; /** Set when the directory held more entries than the server will return. */
808
+ path: string;
809
+ entries: HostDirEntry[];
2221
810
  truncated?: boolean;
2222
811
  };
2223
- /** One hit from `GET {basePath}/fs/find`. */
2224
812
  type HostFileMatch = {
2225
- /** Absolute path, for a follow-up read. */path: string; /** Path relative to the searched directory — what a picker shows and inserts. */
813
+ path: string;
2226
814
  relative: string;
2227
815
  };
2228
- /**
2229
- * `GET {basePath}/fs/find?path=<dir>&q=<query>&limit=<n>` — recursive fuzzy file
2230
- * search under one directory, which is what an `@file` picker needs and
2231
- * `/fs/list` is not: listing answers "what is in this directory", this answers
2232
- * "which file in this tree did you mean".
2233
- *
2234
- * Subsequence matching (`seslist` finds `SessionListView.swift`), filename hits
2235
- * ranked above path hits, shallow files above deep ones. An empty `q` returns the
2236
- * shallowest files. Build directories (`.git`, `node_modules`, …) are skipped, as
2237
- * is anything behind a symlink — so every path returned is one `/fs/read` will
2238
- * accept.
2239
- */
2240
816
  type FindHostFilesResponse = {
2241
- /** Canonical directory the search ran under; `relative` paths are relative to it. */base: string;
2242
- matches: HostFileMatch[]; /** More matched, or the tree was larger than the server would walk. */
817
+ base: string;
818
+ matches: HostFileMatch[];
2243
819
  truncated: boolean;
2244
820
  };
2245
- /** `GET {basePath}/fs/read?path=<abs>` — one file's contents. Binary files come back
2246
- * base64; 413 rather than a truncated read when the file exceeds the server's cap. */
2247
821
  type ReadHostFileResponse = {
2248
822
  path: string;
2249
823
  content: string;
2250
824
  encoding: 'utf8' | 'base64';
2251
- bytes: number; /** sha256 (hex) of the bytes on disk. Pass it back as `expectedHash` to write. */
825
+ bytes: number;
2252
826
  hash: string;
2253
827
  modifiedAt: number;
2254
828
  };
2255
- /**
2256
- * `PUT {basePath}/fs/write` — replace or create one file.
2257
- *
2258
- * The agent is editing this same tree, so a write is **conditional, always**:
2259
- * `expectedHash` must be the hash from the read this edit is based on, and the
2260
- * server 409s if the file has changed since. Omitting it means "create" and 409s
2261
- * if the path already exists — there is no unconditional overwrite, by design.
2262
- * Directories are never created implicitly: writing under a missing parent is a 404.
2263
- */
2264
829
  type WriteHostFileRequest = {
2265
830
  path: string;
2266
- content: string; /** Default 'utf8'. */
2267
- encoding?: 'utf8' | 'base64'; /** Required to overwrite; omit only to create a new file. */
831
+ content: string;
832
+ encoding?: 'utf8' | 'base64';
2268
833
  expectedHash?: string;
2269
834
  };
2270
835
  type WriteHostFileResponse = {
2271
836
  path: string;
2272
- bytes: number; /** Hash of what was just written — carry it into the next edit. */
837
+ bytes: number;
2273
838
  hash: string;
2274
839
  modifiedAt: number;
2275
840
  };
2276
841
  type SaveProfileResponse = {
2277
842
  profile: ProfileInfo;
2278
843
  };
2279
- /** `GET {basePath}/profiles/:name` — the profile plus a fresh config snapshot. */
2280
844
  type GetProfileResponse = {
2281
845
  profile: ProfileInfo;
2282
846
  config: ProfileConfigSnapshot;
@@ -2284,95 +848,53 @@ type GetProfileResponse = {
2284
848
  type ErrorResponse = {
2285
849
  error: string;
2286
850
  };
2287
- /**
2288
- * The moments in an *interactive* session a person needs to hear about when they
2289
- * are not watching it — the whole point being that a phone cannot hold a
2290
- * WebSocket open in the background, so the server has to reach out.
2291
- *
2292
- * Deliberately four: this is a human-attention channel, not an event mirror. The
2293
- * event log stays on the session WS (attach with `afterSeq` to catch up); if you
2294
- * want every assistant message, subscribe there instead.
2295
- */
2296
- type SessionNotificationType = /** The agent is blocked on an approval — the one that matters most. */'permission_requested' /** A turn finished; the session is idle and waiting for the human. */ | 'turn_completed' /** The session failed (`session_error`). */ | 'session_error' /** The session ended (`session_closed`), whoever ended it. */ | 'session_closed';
2297
- /** One delivery on the session-notification channel (JSON body of a webhook POST). */
851
+ type SessionNotificationType = 'permission_requested' | 'turn_completed' | 'session_error' | 'session_closed';
2298
852
  type SessionNotification = {
2299
853
  type: SessionNotificationType;
2300
- sessionId: string; /** Snapshot at notification time — status, title, cwd, cost, `lastSeq`. */
854
+ sessionId: string;
2301
855
  session: SessionInfo;
2302
- /** Seq of the event behind this notification; attach with `afterSeq: seq - 1` to
2303
- * land on it. */
2304
856
  seq: number;
2305
857
  ts: number;
2306
- /** One line fit for a notification body: the permission title, the turn's final
2307
- * text, the error message. */
2308
858
  preview?: string;
2309
- /** `permission_requested` only: the full request, so a consumer can answer it via
2310
- * `POST {basePath}/sessions/:id/permissions/:requestId` — which is what makes an
2311
- * Approve/Deny action on a lock-screen notification possible. */
2312
- request?: PermissionRequest; /** `turn_completed` only. */
859
+ request?: PermissionRequest;
2313
860
  result?: {
2314
861
  isError: boolean;
2315
862
  durationMs: number;
2316
863
  numTurns: number;
2317
864
  totalCostUsd: number;
2318
- }; /** `session_closed` only. */
865
+ };
2319
866
  reason?: 'client' | 'server' | 'error';
2320
867
  };
2321
- /** Where session notifications are POSTed (JSON body = {@link SessionNotification}).
2322
- * Server-wide, not per session: the point is to hear about sessions you did not
2323
- * create yourself and are not attached to. */
2324
868
  type SessionWebhookConfig = {
2325
- url: string; /** Extra headers sent with every delivery (auth tokens etc.). */
2326
- headers?: Record<string, string>; /** Types to deliver. Default: all of them. */
869
+ url: string;
870
+ headers?: Record<string, string>;
2327
871
  events?: SessionNotificationType[];
2328
872
  };
2329
- /**
2330
- * - `queued` — accepted, waiting for a concurrency slot (or the daily token budget)
2331
- * - `running` — a session is executing the prompt
2332
- * - `parked` — waiting on an external event (a deferred tool execution). Not
2333
- * terminal and not consuming a concurrency slot; resumes to `running` when the
2334
- * result arrives, or fails via the execution watchdog if it never does.
2335
- * - `succeeded` / `failed` — terminal; `result` (and `error` on failure) are set
2336
- * - `canceled` — terminal; canceled by a client before or during the run
2337
- */
2338
873
  type JobStatus = 'queued' | 'running' | 'parked' | 'succeeded' | 'failed' | 'canceled';
2339
- /** Where job progress/completion deliveries are POSTed (JSON body = {@link JobEvent}). */
2340
874
  type WebhookConfig = {
2341
- url: string; /** Extra headers sent with every delivery (auth tokens etc.). */
875
+ url: string;
2342
876
  headers?: Record<string, string>;
2343
- /** Delivery granularity: 'messages' also POSTs job_progress per assistant message /
2344
- * permission request; 'completion' only job_started + job_completed. Default 'messages'. */
2345
877
  progress?: 'messages' | 'completion';
2346
878
  };
2347
- /**
2348
- * Schedule a one-shot run: the session executes `prompt` unattended and the job
2349
- * completes with that run's result. `session.prompt` is the task and is required;
2350
- * `resume`/`forkSession` are not supported for queued jobs.
2351
- */
2352
879
  type CreateJobRequest = {
2353
880
  session: CreateSessionRequest & {
2354
881
  prompt: string;
2355
882
  };
2356
- webhook?: WebhookConfig; /** Per-job token cap; the effective cap is min(this, the server's sessionTokenLimit). */
2357
- maxTokens?: number; /** Per-job wall-clock cap; the effective cap is min(this, the server's maxJobDurationMs). */
883
+ webhook?: WebhookConfig;
884
+ maxTokens?: number;
2358
885
  maxDurationMs?: number;
2359
- /** Total run attempts: failed (not canceled) runs re-queue until this many attempts
2360
- * have been made. Default 1 (no retries). */
2361
- attempts?: number; /** Delay before the first retry, doubled for each subsequent one. Default 5000. */
2362
- retryDelayMs?: number; /** Host bookkeeping echoed back on JobInfo. */
886
+ attempts?: number;
887
+ retryDelayMs?: number;
2363
888
  meta?: Record<string, unknown>;
2364
889
  };
2365
- /** Cumulative resource usage of a job's run. `tokens` counts input + output +
2366
- * cache-creation + cache-read tokens across all turns. */
2367
890
  type JobUsage = {
2368
891
  tokens: number;
2369
892
  totalCostUsd: number;
2370
893
  numTurns: number;
2371
894
  };
2372
- /** Terminal outcome of the job's run (mirrors the final turn_result). */
2373
895
  type JobResult = {
2374
896
  subtype: string;
2375
- isError: boolean; /** Final text of the run (success only). */
897
+ isError: boolean;
2376
898
  result?: string;
2377
899
  errors?: string[];
2378
900
  durationMs: number;
@@ -2380,45 +902,30 @@ type JobResult = {
2380
902
  type JobInfo = {
2381
903
  id: string;
2382
904
  status: JobStatus;
2383
- /** `''` when the run's engine has no host filesystem (see
2384
- * {@link CreateSessionRequest.cwd}). */
2385
- cwd: string; /** Profile the run executes under (resolved name, present even when implicit). */
905
+ cwd: string;
2386
906
  profile?: string;
2387
- prompt: string; /** Server session id once started — attach via the sessions WS to watch the run live. */
907
+ prompt: string;
2388
908
  sessionId?: string;
2389
909
  sdkSessionId?: string;
2390
910
  createdAt: number;
2391
911
  startedAt?: number;
2392
- finishedAt?: number; /** 1-based run attempt this info reflects. */
2393
- attempt?: number; /** Total attempts configured on the request (see CreateJobRequest.attempts). */
2394
- maxAttempts?: number; /** For a job re-queued by retry backoff: earliest time the next attempt may start. */
912
+ finishedAt?: number;
913
+ attempt?: number;
914
+ maxAttempts?: number;
2395
915
  nextRunAt?: number;
2396
- /** Set while `status` is 'parked': when the run parked, and the execution it is
2397
- * waiting on — the id to POST a result to. Cleared when it resumes. */
2398
916
  parkedAt?: number;
2399
- parkedExecutionId?: string; /** Cumulative across attempts. */
917
+ parkedExecutionId?: string;
2400
918
  usage: JobUsage;
2401
- result?: JobResult; /** Failure or cancellation reason (for a queued retry: the previous attempt's error). */
919
+ result?: JobResult;
2402
920
  error?: string;
2403
921
  meta?: Record<string, unknown>;
2404
- /** Scope tags of the session this job runs (see
2405
- * {@link CreateSessionRequest.scope}) — copied from the request at submit so
2406
- * the job routes can be gated by the same rule as the session routes. Without
2407
- * it the queue would be a side door into an unscoped session. */
2408
922
  scope?: Record<string, string>;
2409
923
  };
2410
- /** Latest mid-run activity, carried on job_progress deliveries. */
2411
924
  type JobProgress = {
2412
- kind: 'assistant_text' | 'tool_use' | 'permission_requested' | 'permission_resolved'; /** Short human-readable preview (message excerpt, tool name, permission title). */
925
+ kind: 'assistant_text' | 'tool_use' | 'permission_requested' | 'permission_resolved';
2413
926
  preview?: string;
2414
- /** 'permission_requested' only: the full request (including AskUserQuestion input) so
2415
- * webhook consumers can answer via POST /sessions/:sessionId/permissions/:requestId. */
2416
927
  request?: PermissionRequest;
2417
928
  };
2418
- /** Webhook delivery payload (also the queue's local event shape). `job_submitted` goes
2419
- * to local observers and the queue WS only — the submitter already has the POST
2420
- * response, so webhooks start at `job_started`. `job_retrying` marks a failed run that
2421
- * was re-queued (`job.nextRunAt` says when); `job_completed` is always terminal. */
2422
929
  type JobEvent = {
2423
930
  type: 'job_submitted';
2424
931
  job: JobInfo;
@@ -2432,17 +939,12 @@ type JobEvent = {
2432
939
  job: JobInfo;
2433
940
  progress: JobProgress;
2434
941
  ts: number;
2435
- }
2436
- /** The run parked on a deferred execution; `executionId` says what it waits on —
2437
- * the id to POST a result to. The *work itself* (tool name, input, VFS seed) went
2438
- * to the executor's own dispatch hook, not over this channel: a webhook consumer
2439
- * learns that a run is waiting, the worker learns what to do. */
2440
- | {
942
+ } | {
2441
943
  type: 'job_parked';
2442
944
  job: JobInfo;
2443
945
  executionId: string;
2444
946
  ts: number;
2445
- } /** A parked run resumed because its execution result arrived. */ | {
947
+ } | {
2446
948
  type: 'job_resumed';
2447
949
  job: JobInfo;
2448
950
  executionId: string;
@@ -2460,17 +962,12 @@ type QueueStats = {
2460
962
  maxConcurrency: number;
2461
963
  running: number;
2462
964
  queued: number;
2463
- /** Jobs waiting on a deferred execution. They hold no concurrency slot and
2464
- * their wall-clock budget is not ticking. */
2465
965
  parked: number;
2466
966
  sessionTokenLimit?: number;
2467
- dailyTokenLimit?: number; /** Tokens consumed by queue jobs in the current UTC day. */
2468
- dailyTokensUsed: number; /** True when the daily budget is exhausted and queued jobs are being held. */
967
+ dailyTokenLimit?: number;
968
+ dailyTokensUsed: number;
2469
969
  paused: boolean;
2470
970
  };
2471
- /** Frames sent on the queue WS (`{basePath}/queue/ws`). The stream is one-way
2472
- * (server→client): every job's lifecycle as it happens, plus refreshed stats after
2473
- * lifecycle changes. Clients send nothing; job mutations stay on REST. */
2474
971
  type QueueServerFrame = {
2475
972
  type: 'queue_attached';
2476
973
  protocolVersion: number;
@@ -2495,5 +992,5 @@ type QueueStatsResponse = {
2495
992
  stats: QueueStats;
2496
993
  };
2497
994
  //#endregion
2498
- export { ApiMessage, AttachedFrame, ClientFrame, ContentBlock, ContextReading, ContextUsage, ContextUsageCategory, CreateJobRequest, CreateJobResponse, CreateProfileRequest, CreateSessionRequest, CreateSessionResponse, DEFAULT_VIEW_CONFIG, ENGINE_CAPABILITIES, EngineCapabilities, ErrorResponse, Facet, FilePatch, FindHostFilesResponse, GetJobResponse, GetProfileResponse, GetSessionResponse, GroupBy, HostDirEntry, HostFileMatch, HostFileRoot, ImageRefPart, JobEvent, JobInfo, JobProgress, JobResult, JobStatus, JobUsage, ListHostDirResponse, ListHostRootsResponse, ListJobsResponse, ListProfilesResponse, ListSdkSessionsResponse, ListSessionFilesResponse, ListSessionsResponse, McpServerActionRequest, McpServerConfigWire, McpServerStatusInfo, McpServerToolInfo, McpServersResponse, MessageAttachment, ModelOption, PROTOCOL_VERSION, PROVIDER_PERMISSION_MODES, PatchHunk, PermissionDecisionSource, PermissionMode, PermissionRequest, ProfileConfigSnapshot, ProfileDefaults, ProfileEngine, ProfileInfo, ProfileSessionDefaults, ProfileUsage, ProfileUsageWindow, ProjectIcon, ProjectInfo, ProviderConfig, QuestionBehavior, QueueServerFrame, QueueStats, QueueStatsResponse, RateLimitInfo, ReadHostFileResponse, ResolvePermissionRequest, ResolvePermissionResponse, STATE_LABELS, STATE_ORDER, SUBAGENT_HISTORY, SaveProfileResponse, ScopeRoot, SdkSessionSummary, ServerFrame, SessionCapability, SessionCommand, SessionEvent, SessionEventBody, SessionFileInfo, SessionGroup, SessionInfo, SessionNotification, SessionNotificationType, SessionRow, SessionState, SessionStatus, SessionUsage, SessionWebhookConfig, SkillInfo, SlashCommandInfo, SortBy, SubagentInfo, SubmitExecutionResultRequest, SubmitExecutionResultResponse, SubsetSummary, TOOL_RESULT_HEAD_CHARS, TextBlock, ThinkingBlock, ToolCallRequestFrame, ToolExecutionBackend, ToolExecutionOutput, ToolExecutionStatus, ToolResultBlock, ToolUseBlock, UnknownBlock, UpdateProfileRequest, UpdateSessionRequest, UpdateSessionResponse, UploadAttachmentResponse, UsageWindowRow, UserQuestion, UserQuestionOption, ViewConfig, Watermark, WatermarkStore, Watermarks, WebhookConfig, WorkspaceScope, WriteHostFileRequest, WriteHostFileResponse, adaptersOf, clearFilters, contextReading, filterRows, groupRows, hasFacetFilter, imagePartRef, inScope, isAgentRecord, isJobRun, mergeUsage, orderUsageWindows, projectKey, projectLabel, projectSubpath, projectsOf, replayCoalesceKey, replayRetains, runningSubagents, scopeActive, sessionLabel, sessionState, snapshotRetains, subagentLabel, subsetSummary, supportsPermissionMode, transcriptActivity, transcriptContent, unseenCount, usageInfos, watermarkKey };
995
+ export { ApiMessage, AttachedFrame, ClientFrame, ContentBlock, ContextReading, ContextUsage, ContextUsageCategory, CreateJobRequest, CreateJobResponse, CreateProfileRequest, CreateSessionRequest, CreateSessionResponse, DEFAULT_VIEW_CONFIG, ENGINE_CAPABILITIES, EngineCapabilities, ErrorResponse, Facet, FilePatch, FindHostFilesResponse, GetJobResponse, GetProfileResponse, GetSessionResponse, GroupBy, HostDirEntry, HostFileMatch, HostFileRoot, ImageRefPart, JobEvent, JobInfo, JobProgress, JobResult, JobStatus, JobUsage, ListHostDirResponse, ListHostRootsResponse, ListJobsResponse, ListProfilesResponse, ListSdkSessionsResponse, ListSessionFilesResponse, ListSessionsResponse, McpServerActionRequest, McpServerConfigWire, McpServerStatusInfo, McpServerToolInfo, McpServersResponse, MessageAttachment, ModelOption, PROTOCOL_VERSION, PatchHunk, PermissionDecisionSource, PermissionMode, PermissionRequest, ProfileConfigSnapshot, ProfileDefaults, ProfileEngine, ProfileInfo, ProfileSessionDefaults, ProfileUsage, ProfileUsageWindow, ProjectIcon, ProjectInfo, ProviderConfig, QuestionBehavior, QueueServerFrame, QueueStats, QueueStatsResponse, RateLimitInfo, ReadHostFileResponse, ResolvePermissionRequest, ResolvePermissionResponse, STATE_LABELS, STATE_ORDER, SUBAGENT_HISTORY, SaveProfileResponse, ScopeRoot, SdkSessionSummary, ServerFrame, SessionCapability, SessionCommand, SessionEvent, SessionEventBody, SessionFileInfo, SessionGroup, SessionInfo, SessionNotification, SessionNotificationType, SessionRow, SessionState, SessionStatus, SessionUsage, SessionWebhookConfig, SkillInfo, SlashCommandInfo, SortBy, SubagentInfo, SubmitExecutionResultRequest, SubmitExecutionResultResponse, SubsetSummary, TOOL_RESULT_HEAD_CHARS, TextBlock, ThinkingBlock, ToolCallRequestFrame, ToolExecutionBackend, ToolExecutionOutput, ToolExecutionStatus, ToolResultBlock, ToolUseBlock, UnknownBlock, UpdateProfileRequest, UpdateSessionRequest, UpdateSessionResponse, UploadAttachmentResponse, UsageWindowRow, UserQuestion, UserQuestionOption, ViewConfig, Watermark, WatermarkStore, Watermarks, WebhookConfig, WorkspaceScope, WriteHostFileRequest, WriteHostFileResponse, adaptersOf, clearFilters, contextReading, filterRows, groupRows, hasFacetFilter, imagePartRef, inScope, isAgentRecord, isJobRun, mergeUsage, orderUsageWindows, projectKey, projectLabel, projectSubpath, projectsOf, replayCoalesceKey, replayRetains, runningSubagents, scopeActive, sessionLabel, sessionState, snapshotRetains, subagentLabel, subsetSummary, supportsPermissionMode, transcriptActivity, transcriptContent, transcriptProse, unseenCount, usageInfos, watermarkKey };
2499
996
  //# sourceMappingURL=index.d.mts.map