@workerdeck/protocol 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -4
- package/build/index.d.mts +738 -2
- package/build/index.mjs +411 -8
- package/build/index.mjs.map +1 -1
- package/package.json +1 -1
package/build/index.d.mts
CHANGED
|
@@ -20,8 +20,32 @@ type SessionState = 'attention' | 'working' | 'idle' | 'ended';
|
|
|
20
20
|
declare const STATE_ORDER: readonly SessionState[];
|
|
21
21
|
declare const STATE_LABELS: Record<SessionState, string>;
|
|
22
22
|
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
|
+
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
|
+
declare function subagentLabel(sub: SubagentInfo): string;
|
|
23
47
|
/** The facets a session can be grouped or sorted by. */
|
|
24
|
-
type Facet = 'gateway' | 'adapter' | 'state';
|
|
48
|
+
type Facet = 'gateway' | 'adapter' | 'state' | 'project';
|
|
25
49
|
type GroupBy = 'none' | Facet;
|
|
26
50
|
type SortBy = 'recent' | 'name' | Facet;
|
|
27
51
|
type ViewConfig = {
|
|
@@ -29,6 +53,16 @@ type ViewConfig = {
|
|
|
29
53
|
gateways: string[];
|
|
30
54
|
adapters: string[];
|
|
31
55
|
states: SessionState[];
|
|
56
|
+
/**
|
|
57
|
+
* Empty = no filter. Keys are {@link projectKey} output — never names, which
|
|
58
|
+
* are neither unique (two repos both called "api") nor stable (editing
|
|
59
|
+
* `.workerdeck.json` renames every session at once and must not empty a
|
|
60
|
+
* saved filter). Optional, unlike its three siblings, because stored view
|
|
61
|
+
* configs predate it: a config restored from `localStorage`/`globalState`
|
|
62
|
+
* without the key must keep filtering, so absent and empty mean the same
|
|
63
|
+
* thing.
|
|
64
|
+
*/
|
|
65
|
+
projects?: string[];
|
|
32
66
|
/** Show only sessions inside the host's own folders. Inert where there is no
|
|
33
67
|
* such notion (no folder open, a dashboard with no workspace), which is why it
|
|
34
68
|
* can default on. */
|
|
@@ -74,7 +108,51 @@ type SessionGroup = {
|
|
|
74
108
|
/** The adapters actually present, for the filter chips — derived rather than
|
|
75
109
|
* enumerated, so a new engine needs no change here. */
|
|
76
110
|
declare function adaptersOf(rows: readonly SessionRow[]): string[];
|
|
111
|
+
/**
|
|
112
|
+
* The projects actually present, as `{ key, label }` for a filter control —
|
|
113
|
+
* derived like {@link adaptersOf}, and paired because the two halves differ:
|
|
114
|
+
* the *key* is what {@link ViewConfig.projects} holds (gateway-qualified root,
|
|
115
|
+
* so a rename regroups nothing) and the *label* is what a person picks by.
|
|
116
|
+
*
|
|
117
|
+
* Sorted by label, deduped by key. Two projects with the same name on two
|
|
118
|
+
* gateways therefore stay two entries wearing one word — which is honest: they
|
|
119
|
+
* really are two different directories, and the alternative is a filter that
|
|
120
|
+
* silently selects both.
|
|
121
|
+
*/
|
|
122
|
+
declare function projectsOf(rows: readonly SessionRow[]): {
|
|
123
|
+
key: string;
|
|
124
|
+
label: string;
|
|
125
|
+
}[];
|
|
77
126
|
declare function sessionLabel(info: SessionInfo): string;
|
|
127
|
+
/**
|
|
128
|
+
* The project facet's grouping key: gateway id + the project root, falling
|
|
129
|
+
* back to the session's cwd when no project is declared.
|
|
130
|
+
*
|
|
131
|
+
* The root and not the name, because a name is not a key (two repos can both
|
|
132
|
+
* be called "api", and a rename must regroup nothing); qualified by gateway,
|
|
133
|
+
* because a remote gateway's identical-looking path is another machine's
|
|
134
|
+
* directory — the same rule `ScopeRoot` states. The cwd fallback is what makes
|
|
135
|
+
* grouping by project useful before anyone has written a `.workerdeck.json`:
|
|
136
|
+
* undeclared sessions group by their folder, declared ones by their root, and
|
|
137
|
+
* a session in `packages/ui` joins its repo's group the moment the file
|
|
138
|
+
* exists. Sessions with no cwd at all (a filesystem-less engine) share one
|
|
139
|
+
* per-gateway bucket — see {@link projectLabel}.
|
|
140
|
+
*/
|
|
141
|
+
declare function projectKey(row: SessionRow): string;
|
|
142
|
+
/**
|
|
143
|
+
* What a project group (or a row's project slot) is called: the declared name,
|
|
144
|
+
* else the cwd's basename — the exact string clients rendered before this
|
|
145
|
+
* feature existed, so an undeclared project looks like today. 'No project' is
|
|
146
|
+
* only ever the no-cwd case (a sandboxed provider session), where there is no
|
|
147
|
+
* folder to name.
|
|
148
|
+
*
|
|
149
|
+
* Takes only the `info` it reads, so a surface holding a bare `SessionInfo` —
|
|
150
|
+
* a row component, an iOS cell — can call it without inventing the rest of a
|
|
151
|
+
* `SessionRow`. That matters more than it looks: this string is what a client
|
|
152
|
+
* renders *in place of* the cwd basename it used to draw, and two spellings of
|
|
153
|
+
* it would put the list and its group headers on different names.
|
|
154
|
+
*/
|
|
155
|
+
declare function projectLabel(row: Pick<SessionRow, 'info'>): string;
|
|
78
156
|
/**
|
|
79
157
|
* This session is a job run — the queue created it, and `JobInfo.sessionId`
|
|
80
158
|
* points at it.
|
|
@@ -136,6 +214,72 @@ declare function hasFacetFilter(config: ViewConfig): boolean;
|
|
|
136
214
|
* choices are a layout preference and survive. */
|
|
137
215
|
declare function clearFilters(config: ViewConfig): ViewConfig;
|
|
138
216
|
//#endregion
|
|
217
|
+
//#region src/usage.d.ts
|
|
218
|
+
/**
|
|
219
|
+
* What one session was last told about the plan's windows: the transcript's own
|
|
220
|
+
* rate-limit state, and the event clock of the newest reading in it.
|
|
221
|
+
*
|
|
222
|
+
* Deliberately structural rather than `TranscriptState` — protocol may not
|
|
223
|
+
* import a client — and it is exactly the two fields the reducer keeps.
|
|
224
|
+
*/
|
|
225
|
+
type SessionUsage = {
|
|
226
|
+
/** Keyed by `rateLimitType`, as the reducer stores it. */rateLimits?: Record<string, RateLimitInfo>;
|
|
227
|
+
/** Epoch ms of the newest `rate_limit` event this session saw — one clock for
|
|
228
|
+
* the whole map, which is all the reducer records. */
|
|
229
|
+
updatedAt?: number;
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* The usage a client should render: the gateway's per-profile state where it has
|
|
233
|
+
* the window, this session's own reading where it does not.
|
|
234
|
+
*
|
|
235
|
+
* Why the profile wins outright rather than by comparing timestamps: the
|
|
236
|
+
* gateway's `ProfileUsageTracker` is fed from **every** session on the profile —
|
|
237
|
+
* including this one, from seq 0 — and keeps the newest reading per window by
|
|
238
|
+
* the event's own `ts`. So for any window it holds, it holds a reading at least
|
|
239
|
+
* as new as the one in this transcript, and a timestamp comparison could only
|
|
240
|
+
* ever go wrong: the reducer keeps a *single* `updatedAt` for the whole map, so
|
|
241
|
+
* a `five_hour` reading from this morning is dated with the afternoon's
|
|
242
|
+
* `seven_day` event and would beat a genuinely fresher profile entry.
|
|
243
|
+
*
|
|
244
|
+
* The session half is not a fallback for correctness but for *coverage*: the
|
|
245
|
+
* profile map is in-memory, so a restarted gateway serves nothing until a
|
|
246
|
+
* session reports again, and a session with no profile has no account state at
|
|
247
|
+
* all. In both cases the transcript's reading is the only one there is, and it
|
|
248
|
+
* is dated honestly (see {@link SessionUsage.updatedAt}) rather than as now.
|
|
249
|
+
*
|
|
250
|
+
* Absent stays absent throughout: a window nobody has reported is **unknown,
|
|
251
|
+
* never 0%**, and this returns an empty map rather than inventing entries.
|
|
252
|
+
*/
|
|
253
|
+
declare function mergeUsage(session: SessionUsage, profile: ProfileUsage | undefined): ProfileUsage;
|
|
254
|
+
/** One window as a surface draws it: the reading, its own date, and whether the
|
|
255
|
+
* gateway is the one that zeroed it. */
|
|
256
|
+
type UsageWindowRow = {
|
|
257
|
+
key: string;
|
|
258
|
+
info: RateLimitInfo; /** Epoch ms of the reading. Absent only for a hand-built state with no clock. */
|
|
259
|
+
updatedAt?: number;
|
|
260
|
+
inferredReset?: boolean;
|
|
261
|
+
};
|
|
262
|
+
/**
|
|
263
|
+
* The windows in reading order: the session window, the weekly one, then the
|
|
264
|
+
* per-model weeklies alphabetically.
|
|
265
|
+
*
|
|
266
|
+
* Discovered rather than hardcoded — the engine's set of windows is an open
|
|
267
|
+
* union and has grown before — but ordered, so the first two always mean the
|
|
268
|
+
* same thing wherever they are drawn. A window with no `utilization` is
|
|
269
|
+
* **unknown, not zero**, and is dropped entirely rather than rendered as an
|
|
270
|
+
* empty bar that reads as "plenty left".
|
|
271
|
+
*
|
|
272
|
+
* Here rather than in a client because two surfaces now render the same windows
|
|
273
|
+
* from different sources — the session panel from its merged state, the
|
|
274
|
+
* dashboard's profile page straight off `ProfileInfo.usage` — and a list that
|
|
275
|
+
* ordered or filtered differently would be the same account described two ways.
|
|
276
|
+
*/
|
|
277
|
+
declare function orderUsageWindows(usage: ProfileUsage | undefined): UsageWindowRow[];
|
|
278
|
+
/** The flat `rateLimitType → reading` map every existing renderer takes, out of
|
|
279
|
+
* the dated form. Undefined in, undefined out — so a surface can keep telling
|
|
280
|
+
* "no reading" apart from "an empty one". */
|
|
281
|
+
declare function usageInfos(usage: ProfileUsage | undefined): Record<string, RateLimitInfo> | undefined;
|
|
282
|
+
//#endregion
|
|
139
283
|
//#region src/watermarks.d.ts
|
|
140
284
|
/**
|
|
141
285
|
* "What had you seen, and when" — per session, across reloads.
|
|
@@ -263,12 +407,171 @@ type ToolResultBlock = {
|
|
|
263
407
|
[key: string]: unknown;
|
|
264
408
|
}>;
|
|
265
409
|
is_error?: boolean;
|
|
410
|
+
/**
|
|
411
|
+
* This block carries only the **head** of the result: the replay truncated it
|
|
412
|
+
* (see {@link TOOL_RESULT_HEAD_CHARS}), and the whole thing is one fetch away
|
|
413
|
+
* at `GET /sessions/:id/events/:seq/result?toolUseId=`.
|
|
414
|
+
*
|
|
415
|
+
* On the **block**, never the event, and that is the same argument
|
|
416
|
+
* `user_message.patch` has to make in reverse: the patch sits on the event and
|
|
417
|
+
* its doc must therefore caveat "only when the message carries exactly one
|
|
418
|
+
* `tool_result` block — with two, nothing says which one it belongs to". A
|
|
419
|
+
* message answering three calls truncates whichever of them is large, so
|
|
420
|
+
* paying that caveat a second time would make the marker unusable exactly
|
|
421
|
+
* when it matters. {@link FilePatch.truncated} is the shipped precedent.
|
|
422
|
+
*
|
|
423
|
+
* Only ever set on a **replay** a client asked for (`truncateResults`), so a
|
|
424
|
+
* client that has never heard of this field cannot receive one — which is why
|
|
425
|
+
* this is additive at protocol 7 rather than a bump. Absent means the block is
|
|
426
|
+
* whole.
|
|
427
|
+
*/
|
|
428
|
+
truncated?: boolean;
|
|
429
|
+
/** How many characters the untruncated result had. Set iff `truncated`.
|
|
430
|
+
*
|
|
431
|
+
* A client cannot compute it — it holds the head — and the number is not
|
|
432
|
+
* cosmetic: a collapsed row spells "… +N chars", and `height.ts` sizes the row
|
|
433
|
+
* by wrapping **that exact string**, so a count derived from the head would be
|
|
434
|
+
* both a lie and a different pixel height. */
|
|
435
|
+
total_chars?: number;
|
|
266
436
|
};
|
|
437
|
+
/**
|
|
438
|
+
* How much of a tool result a truncating replay keeps.
|
|
439
|
+
*
|
|
440
|
+
* Chosen against the two clients' *own* budgets, and the relationship is the
|
|
441
|
+
* whole point: the terminal theme shows ~400 characters collapsed and ~2,000
|
|
442
|
+
* open, so at 8,000 the collapsed and open states are **byte-identical to an
|
|
443
|
+
* untruncated attach** and only the uncapped "show everything" press ever
|
|
444
|
+
* fetches. That collapses the entire feature to one press, and it is asserted
|
|
445
|
+
* in a test rather than trusted — lowered below the open budget, this would
|
|
446
|
+
* silently clip the open state with no marker, which is the one failure this
|
|
447
|
+
* design must not have.
|
|
448
|
+
*
|
|
449
|
+
* Measured justification: on one 1,270-row session three `tool_result` frames
|
|
450
|
+
* were 641 / 463 / 396 KB, 68% of a 3.1 MB attach. The cut is *structural* —
|
|
451
|
+
* proportional to the thing that is actually large, wherever in the log it sits
|
|
452
|
+
* — which a row window is not.
|
|
453
|
+
*/
|
|
454
|
+
declare const TOOL_RESULT_HEAD_CHARS = 8000;
|
|
455
|
+
/**
|
|
456
|
+
* A base64 image part, delivered as an address instead of its bytes.
|
|
457
|
+
*
|
|
458
|
+
* The **seventh** rule of the family, and the first written *after* its
|
|
459
|
+
* measurement rather than before it. Across 214 local sessions, 91% of all
|
|
460
|
+
* tool-result payload is base64 image data — 489 MB against 44 MB of text — and
|
|
461
|
+
* **no client renders a byte of it**: `blockText` in the reducer and
|
|
462
|
+
* `joinedText` on iOS both fold a `tool_result` to its text parts, and both
|
|
463
|
+
* clients draw a tool's picture from a host *path* (`savedPath` → `/produced`,
|
|
464
|
+
* `/fs/read`), never from block content. So it is `replayRetains`' argument at
|
|
465
|
+
* nine times the size of the case that rule was written for: bytes whose entire
|
|
466
|
+
* effect on the reader is `return base`.
|
|
467
|
+
*
|
|
468
|
+
* A **new part type rather than a hollowed-out `image`**, and that is the one
|
|
469
|
+
* judgement here worth stating. `headOf`'s shape-preservation rule — "a
|
|
470
|
+
* truncation is a shorter result, never a different kind of one" — cuts the
|
|
471
|
+
* other way for pixels: a head *is* a valid shorter text, but an image with no
|
|
472
|
+
* bytes is not a smaller image, and spelling it `{ type: 'image', source }` with
|
|
473
|
+
* no `data` invites precisely the failure shape-preservation exists to prevent,
|
|
474
|
+
* a renderer that trusts `source.data` drawing `data:;base64,undefined`. An
|
|
475
|
+
* unfamiliar type instead falls through every fold that already exists, exactly
|
|
476
|
+
* as the CLI's own `tool_reference` part does: no `text`, so it contributes
|
|
477
|
+
* nothing, and an unaware consumer renders what it renders today, which is
|
|
478
|
+
* nothing. That is this family's safe failure.
|
|
479
|
+
*
|
|
480
|
+
* Only ever produced for a socket that asked (`imageRefs`), so a client that has
|
|
481
|
+
* never heard of this type cannot receive one — which is why this is additive at
|
|
482
|
+
* protocol 7, the same argument {@link ToolResultBlock.truncated} makes. Unlike
|
|
483
|
+
* truncation it applies to **live events as well as replays**: the client's one
|
|
484
|
+
* render path is ref-then-fetch, so bytes on a live event would either be
|
|
485
|
+
* discarded (335 KB median, once per attached watcher) or need a second
|
|
486
|
+
* decode-from-event path pinning megabytes inside the transcript cache — the
|
|
487
|
+
* disease relocated rather than cured.
|
|
488
|
+
*/
|
|
489
|
+
type ImageRefPart = {
|
|
490
|
+
type: 'image_ref';
|
|
491
|
+
/** The stored part's own media type (`image/png`, `image/jpeg` and
|
|
492
|
+
* `image/webp` are the three observed), or `application/octet-stream` when it
|
|
493
|
+
* had none. Never the membership test — that is `image` plus a base64 source. */
|
|
494
|
+
media_type: string;
|
|
495
|
+
/** Decoded size, which a client cannot compute from an address it has not
|
|
496
|
+
* fetched yet. Not cosmetic: the placeholder spells it, and in the terminal
|
|
497
|
+
* theme a rendered string *is* a row height. */
|
|
498
|
+
bytes: number;
|
|
499
|
+
/**
|
|
500
|
+
* Index of this part in the **stored** block's content array, and the address
|
|
501
|
+
* a fetch is made with.
|
|
502
|
+
*
|
|
503
|
+
* A stamped field rather than the position it arrives at, because that
|
|
504
|
+
* position is not stable: `headOf` builds a truncated head by keeping text
|
|
505
|
+
* parts up to budget and dropping every other part, so a block that is both
|
|
506
|
+
* over the text budget and image-bearing has its parts renumbered the moment
|
|
507
|
+
* the two rules compose. Stamped, the address survives any later reshaping —
|
|
508
|
+
* and the route verifies it against the stored block rather than trusting it.
|
|
509
|
+
*/
|
|
510
|
+
part_index: number;
|
|
511
|
+
};
|
|
512
|
+
/**
|
|
513
|
+
* Project one `tool_result` content part onto its {@link ImageRefPart}, or
|
|
514
|
+
* `undefined` when the part is not a base64 image and must be delivered as it
|
|
515
|
+
* stands.
|
|
516
|
+
*
|
|
517
|
+
* The rule's **one spelling**, shared by the transform that replaces parts
|
|
518
|
+
* (core), the route that serves them back (server) and the property test that
|
|
519
|
+
* proves the fold is otherwise unchanged (react) — the same reason every other
|
|
520
|
+
* member of this family lives here rather than in whichever package applies it.
|
|
521
|
+
*
|
|
522
|
+
* Deliberately narrow. The corpus holds exactly two non-text part kinds: this
|
|
523
|
+
* one, and the CLI's `tool_reference`, of which every instance across 214
|
|
524
|
+
* sessions totals 122 KB. A "drop non-text parts" rule would sweep those in for
|
|
525
|
+
* no measurable gain, and narrowness is this family's standing habit.
|
|
526
|
+
*/
|
|
527
|
+
declare function imagePartRef(part: {
|
|
528
|
+
type?: string;
|
|
529
|
+
[key: string]: unknown;
|
|
530
|
+
}, index: number): ImageRefPart | undefined;
|
|
267
531
|
/** Forward-compatible fallback for block types this protocol version doesn't model. */
|
|
268
532
|
type UnknownBlock = {
|
|
269
533
|
type: string;
|
|
270
534
|
[key: string]: unknown;
|
|
271
535
|
};
|
|
536
|
+
/**
|
|
537
|
+
* One hunk of a file edit, in unified-diff terms.
|
|
538
|
+
*
|
|
539
|
+
* The numbers are the engine's own, not the client's: `newStart` is where this
|
|
540
|
+
* hunk begins in the file *after* the edit, which is what a reader needs to jump
|
|
541
|
+
* to the change. A client cannot compute them — it has never seen the file — so
|
|
542
|
+
* a diff rendered without this is a diff with no line numbers.
|
|
543
|
+
*/
|
|
544
|
+
type PatchHunk = {
|
|
545
|
+
oldStart: number;
|
|
546
|
+
oldLines: number;
|
|
547
|
+
newStart: number;
|
|
548
|
+
newLines: number;
|
|
549
|
+
/** Body lines, each prefixed ' ' (context), '-' (removed) or '+' (added), as
|
|
550
|
+
* unified diff spells them. The prefix is part of the string. */
|
|
551
|
+
lines: string[];
|
|
552
|
+
};
|
|
553
|
+
/**
|
|
554
|
+
* What a file-editing tool changed — the renderable half of an engine's edit
|
|
555
|
+
* output, and deliberately only that half.
|
|
556
|
+
*
|
|
557
|
+
* Both engines can say far more: the Claude SDK's `FileEditOutput` carries
|
|
558
|
+
* `originalFile`, the **entire** contents of the file before the edit. That must
|
|
559
|
+
* not travel here. This log is replayed to every attaching client and captured
|
|
560
|
+
* into parking snapshots, so a whole file on every edit is paid for again on
|
|
561
|
+
* every attach, forever — the same reason attachment bytes are references (see
|
|
562
|
+
* {@link MessageAttachment}) rather than inline base64.
|
|
563
|
+
*
|
|
564
|
+
* So the runner projects the engine's output down to the hunks, which is exactly
|
|
565
|
+
* what a diff renders and nothing more.
|
|
566
|
+
*/
|
|
567
|
+
type FilePatch = {
|
|
568
|
+
/** Absolute path the engine reported, when it named one. */path?: string; /** `create` when the file did not exist before this edit. */
|
|
569
|
+
kind?: 'create' | 'update';
|
|
570
|
+
hunks: PatchHunk[];
|
|
571
|
+
/** Hunks were dropped to keep the event small. A renderer must say so rather
|
|
572
|
+
* than present a partial diff as the whole change. */
|
|
573
|
+
truncated?: boolean;
|
|
574
|
+
};
|
|
272
575
|
type ContentBlock = TextBlock | ThinkingBlock | ToolUseBlock | ToolResultBlock | UnknownBlock;
|
|
273
576
|
/**
|
|
274
577
|
* A file the user attached to a message — a photo, a screenshot, a document.
|
|
@@ -567,6 +870,28 @@ type SessionEventBody = /** SDK init handshake: what this session actually is. *
|
|
|
567
870
|
| {
|
|
568
871
|
type: 'plan_info';
|
|
569
872
|
subscriptionType: string;
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* The engine started a **fresh conversation inside the same session** — the
|
|
876
|
+
* CLI's `/clear`, a plan-mode exit, and whatever fresh-conversation flows the
|
|
877
|
+
* SDK grows. The session id, registry row, workspace and scope are all
|
|
878
|
+
* unchanged; only the conversation is new. Clients empty the transcript and
|
|
879
|
+
* keep every session-scoped fact (models, commands, skills, produced files,
|
|
880
|
+
* rate limits, cwd, permission mode).
|
|
881
|
+
*
|
|
882
|
+
* The server's replay honours it too: an attach after a reset does not
|
|
883
|
+
* resurrect the cleared rows, because the runner skips *transcript content*
|
|
884
|
+
* below the latest reset (see {@link transcriptContent}) while still
|
|
885
|
+
* replaying every state-bearing event. `SessionInfo.activityCount` stays
|
|
886
|
+
* monotonic across a reset on purpose — it is an unread cursor, not an item
|
|
887
|
+
* count, and winding it back would kill every stored watermark above it.
|
|
888
|
+
*/
|
|
889
|
+
| {
|
|
890
|
+
type: 'conversation_reset';
|
|
891
|
+
/** The engine session id the fresh conversation runs under (the SDK's
|
|
892
|
+
* `new_conversation_id`), when the engine reported one. The follow-up
|
|
893
|
+
* `system_init` remains authoritative. */
|
|
894
|
+
sdkSessionId?: string;
|
|
570
895
|
} | {
|
|
571
896
|
type: 'assistant_message';
|
|
572
897
|
message: ApiMessage; /** Set when the message was produced inside a subagent (Task tool). */
|
|
@@ -583,6 +908,16 @@ type SessionEventBody = /** SDK init handshake: what this session actually is. *
|
|
|
583
908
|
* `message.content` carries the typed text only — the attachment bytes went
|
|
584
909
|
* to the model, not into this log. */
|
|
585
910
|
attachments?: MessageAttachment[];
|
|
911
|
+
/**
|
|
912
|
+
* What a file-editing tool changed, when this message carries that tool's
|
|
913
|
+
* result (see {@link FilePatch}). Set by the runner from the engine's own
|
|
914
|
+
* structured output — never derived by a client from the result text.
|
|
915
|
+
*
|
|
916
|
+
* Only when the message carries exactly one `tool_result` block, which is
|
|
917
|
+
* what both engines send: with two, there is nothing that says which one
|
|
918
|
+
* the patch belongs to, and guessing would attach a diff to the wrong call.
|
|
919
|
+
*/
|
|
920
|
+
patch?: FilePatch;
|
|
586
921
|
uuid?: string;
|
|
587
922
|
}
|
|
588
923
|
/** Raw Anthropic streaming event (message_start/content_block_delta/...); emitted only
|
|
@@ -924,6 +1259,37 @@ type ProfileSessionDefaults = {
|
|
|
924
1259
|
mcpServers?: string[]; /** Prepended to the session's system prompt. */
|
|
925
1260
|
instructions?: string;
|
|
926
1261
|
};
|
|
1262
|
+
/**
|
|
1263
|
+
* One rate-limit window of a profile's plan, as the *gateway* last saw it — the
|
|
1264
|
+
* newest {@link RateLimitInfo} any session on the profile reported, across every
|
|
1265
|
+
* session, live or since closed. The profile is the account boundary (one config
|
|
1266
|
+
* dir / codex home / provider key = one plan), so this is the single usage state
|
|
1267
|
+
* per account, where a session's own transcript only knows what *it* was last
|
|
1268
|
+
* told.
|
|
1269
|
+
*
|
|
1270
|
+
* Two rules a client must keep:
|
|
1271
|
+
* - An absent window (or an absent {@link ProfileInfo.usage} entirely) is
|
|
1272
|
+
* **unknown, not 0%** — render nothing, exactly as for session-level readings.
|
|
1273
|
+
* The map is in-memory and starts empty on a cold server.
|
|
1274
|
+
* - `inferredReset` marks a reading the server zeroed at serve time because the
|
|
1275
|
+
* reading's own `resetsAt` passed with nothing newer: the pre-reset number is
|
|
1276
|
+
* then provably wrong, and 0 is the truthful *floor* (the account may have
|
|
1277
|
+
* been used outside this gateway since). Distinguishable on the wire from an
|
|
1278
|
+
* engine-reported 0, which carries no flag.
|
|
1279
|
+
*/
|
|
1280
|
+
type ProfileUsageWindow = {
|
|
1281
|
+
/** The reading, exactly as the session event carried it — except after an
|
|
1282
|
+
* elapsed reset, when `utilization` is 0 and `resetsAt` is dropped (the old
|
|
1283
|
+
* one names the *previous* window; a countdown from it would be nonsense). */
|
|
1284
|
+
info: RateLimitInfo;
|
|
1285
|
+
/** Epoch ms of the event that carried the reading — honest for "Updated …"
|
|
1286
|
+
* lines even when the served utilization is inferred. */
|
|
1287
|
+
updatedAt: number; /** Present (true) only on the served-as-0 inference described above. */
|
|
1288
|
+
inferredReset?: boolean;
|
|
1289
|
+
};
|
|
1290
|
+
/** Per-window plan usage, keyed by `rateLimitType` ('five_hour', 'seven_day',
|
|
1291
|
+
* ...) — the same keying as a transcript's rate-limit state. */
|
|
1292
|
+
type ProfileUsage = Record<string, ProfileUsageWindow>;
|
|
927
1293
|
type ProfileInfo = {
|
|
928
1294
|
/** Unique name, used as {@link CreateSessionRequest.profile}. */name: string;
|
|
929
1295
|
/** Engine this profile runs on. Defaults to 'claude' when absent, so profiles
|
|
@@ -961,6 +1327,11 @@ type ProfileInfo = {
|
|
|
961
1327
|
/** Response-only: one operator-actionable line, present only when
|
|
962
1328
|
* `available === false`. */
|
|
963
1329
|
unavailableReason?: string;
|
|
1330
|
+
/** Response-only: the plan's rate-limit windows as last reported by any
|
|
1331
|
+
* session on this profile (see {@link ProfileUsageWindow}). Absent = unknown
|
|
1332
|
+
* — no session has reported yet (API-key sessions never do), or the server
|
|
1333
|
+
* restarted. **Display-only**, like `available`: never a gate. */
|
|
1334
|
+
usage?: ProfileUsage;
|
|
964
1335
|
/** Response-only, computed by the server: this profile came from the profile
|
|
965
1336
|
* store and can be edited or deleted through the API. Profiles declared in
|
|
966
1337
|
* server options are absent/false — they are code. Ignored on the way in. */
|
|
@@ -1128,6 +1499,178 @@ type CreateSessionRequest = {
|
|
|
1128
1499
|
*/
|
|
1129
1500
|
scope?: Record<string, string>;
|
|
1130
1501
|
};
|
|
1502
|
+
/**
|
|
1503
|
+
* One sub-agent (a `Task` call and the sidechain it spawned), as a *list* surface
|
|
1504
|
+
* sees it — without attaching.
|
|
1505
|
+
*
|
|
1506
|
+
* Sub-agent work is otherwise attach-only: it exists on the wire as
|
|
1507
|
+
* `parentToolUseId` on three event bodies, and is reconstructed into rows by the
|
|
1508
|
+
* react reducer and grouped per-Task by `terminalBlocks`. A sessions list never
|
|
1509
|
+
* attaches (one live attach per session, owned by the panel), so it reads
|
|
1510
|
+
* `SessionInfo` over REST and would otherwise have no way to know a session has
|
|
1511
|
+
* six agents running inside one turn.
|
|
1512
|
+
*
|
|
1513
|
+
* This is a **runner-owned rollup computed at read time**, exactly like
|
|
1514
|
+
* {@link SessionInfo.pendingPermissionCount}: it is not an event, it is not
|
|
1515
|
+
* persisted separately, and it therefore rides the REST list, the WS attach
|
|
1516
|
+
* snapshot and parking snapshots for free. Only the claude engine produces it —
|
|
1517
|
+
* codex and provider emit `parentToolUseId: null` on every event, so an empty
|
|
1518
|
+
* list there is the truth rather than a gap.
|
|
1519
|
+
*
|
|
1520
|
+
* It is deliberately **not** the input to `taskSummary`. That string is spelled
|
|
1521
|
+
* from the absorbed transcript items and must stay that way, so a transcript
|
|
1522
|
+
* replayed tomorrow spells the same line from the same items it holds today.
|
|
1523
|
+
*/
|
|
1524
|
+
type SubagentInfo = {
|
|
1525
|
+
/** The `tool_use` id of the `Task` call that spawned it — the same id its
|
|
1526
|
+
* nested events carry as `parentToolUseId`, and therefore the handle a client
|
|
1527
|
+
* uses to jump to that Task's row. */
|
|
1528
|
+
toolUseId: string; /** The Task input's `subagent_type` (e.g. "Explore"), when it named one. */
|
|
1529
|
+
agentType?: string;
|
|
1530
|
+
/** The Task input's short `description`, clipped by the runner. Together with
|
|
1531
|
+
* `agentType` this is what makes two parallel sub-agents tell apart in a list;
|
|
1532
|
+
* a row reading only `Task` answers nothing. */
|
|
1533
|
+
description?: string;
|
|
1534
|
+
/**
|
|
1535
|
+
* `running` until the Task's own `tool_result` arrives, then `done`/`failed`
|
|
1536
|
+
* from that result's `is_error`. A turn that ends without that result — an
|
|
1537
|
+
* interrupt, a session error, a turn or budget cap — settles what is still
|
|
1538
|
+
* running as `failed`: the report never came, which is the one thing `done`
|
|
1539
|
+
* could have claimed, and a `running` badge on an idle session would be a
|
|
1540
|
+
* lie a list re-renders at every poll.
|
|
1541
|
+
*
|
|
1542
|
+
* Deliberately **narrower than `taskFailed`** in `@workerdeck/ui`'s
|
|
1543
|
+
* `tool-run.ts`, which reddens a Task row when *any child call* failed. That is
|
|
1544
|
+
* right for a transcript row the reader can expand — the failure is one press
|
|
1545
|
+
* away and hiding it would be worse. It is wrong for a list: a grep that
|
|
1546
|
+
* matched nothing inside an otherwise successful Explore agent would put
|
|
1547
|
+
* `failed` beside the session's name with nothing to open. So this reports the
|
|
1548
|
+
* sub-agent's own outcome. If you are here to "fix" the inconsistency, this is
|
|
1549
|
+
* the reason it exists.
|
|
1550
|
+
*/
|
|
1551
|
+
status: 'running' | 'done' | 'failed'; /** Epoch ms the `Task` call was emitted. */
|
|
1552
|
+
startedAt: number;
|
|
1553
|
+
/** Tool calls the sub-agent has made so far — its progress reading while
|
|
1554
|
+
* running, counted from nested `tool_use` blocks. */
|
|
1555
|
+
toolCount: number;
|
|
1556
|
+
};
|
|
1557
|
+
/**
|
|
1558
|
+
* How many *settled* sub-agents {@link SessionInfo.subagents} keeps behind the
|
|
1559
|
+
* running ones. Small on purpose: the point of the tail is that a list row does
|
|
1560
|
+
* not go blank the instant a run finishes, not that it is a history.
|
|
1561
|
+
*/
|
|
1562
|
+
declare const SUBAGENT_HISTORY = 8;
|
|
1563
|
+
/**
|
|
1564
|
+
* A project's icon, as declared by its `.workerdeck.json` — either a named
|
|
1565
|
+
* glyph or a reference to an image the gateway serves.
|
|
1566
|
+
*
|
|
1567
|
+
* A discriminated union rather than one stringly field, because the two arms
|
|
1568
|
+
* have opposite render paths: a glyph is looked up in the client's own icon
|
|
1569
|
+
* set with no I/O, an image is a fetch. Collapsing them would put "is this a
|
|
1570
|
+
* name or an address" back on every renderer, which is the inference this
|
|
1571
|
+
* family keeps refusing (`ImageRefPart` is a new part type, never a
|
|
1572
|
+
* hollowed-out `image`, for the same reason).
|
|
1573
|
+
*
|
|
1574
|
+
* `glyph.name` is a lucide icon name, validated by the gateway for *shape*
|
|
1575
|
+
* only (lowercase kebab-case): the gateway has no lucide catalog and must not
|
|
1576
|
+
* grow one — icon sets version independently of this protocol. A client whose
|
|
1577
|
+
* set lacks the name renders its no-project fallback rather than erroring;
|
|
1578
|
+
* an unknown name is a stale row, never withheld state.
|
|
1579
|
+
*
|
|
1580
|
+
* `image` carries an **address, never bytes** — the attachment-bytes rule.
|
|
1581
|
+
* `SessionInfo` rides every row of `GET /sessions`, which clients poll at 1.2s
|
|
1582
|
+
* while anything is working, so an inlined base64 icon would be paid for on
|
|
1583
|
+
* every poll of every session forever (the same argument that keeps
|
|
1584
|
+
* `originalFile` off {@link FilePatch} and message bytes off events). The
|
|
1585
|
+
* bytes come from `GET {basePath}/sessions/:id/project/icon` — session-scoped
|
|
1586
|
+
* on purpose, so the fetch rides the same `canSee` gate as every other
|
|
1587
|
+
* `/sessions/:id/*` route and a scoped principal's miss is the uniform 404. A
|
|
1588
|
+
* project-keyed route would need the project root in the URL, and a route
|
|
1589
|
+
* addressed by host paths is an existence oracle for the gateway's
|
|
1590
|
+
* filesystem. `hash` (sha256 hex of the bytes) is the cross-session cache
|
|
1591
|
+
* key: two sessions in one project serve identical bytes, so a client caches
|
|
1592
|
+
* by hash rather than by URL and fetches once per project, not once per
|
|
1593
|
+
* session. The route answers with `ETag: "<hash>"` and honors
|
|
1594
|
+
* `If-None-Match`.
|
|
1595
|
+
*/
|
|
1596
|
+
type ProjectIcon = {
|
|
1597
|
+
type: 'glyph';
|
|
1598
|
+
name: string;
|
|
1599
|
+
} | {
|
|
1600
|
+
type: 'image';
|
|
1601
|
+
mediaType: 'image/png' | 'image/svg+xml';
|
|
1602
|
+
hash: string;
|
|
1603
|
+
};
|
|
1604
|
+
/**
|
|
1605
|
+
* Project identity for a session — what a `.workerdeck.json` in the session's
|
|
1606
|
+
* ancestry declares, resolved by the **gateway** and shipped on
|
|
1607
|
+
* {@link SessionInfo.project}.
|
|
1608
|
+
*
|
|
1609
|
+
* The gateway reads the file, not each client: the iOS app and a browser
|
|
1610
|
+
* pointed at a remote gateway have no access to that filesystem, so a
|
|
1611
|
+
* per-client reader would make the feature exist on exactly one client.
|
|
1612
|
+
* Discovery is an ancestor walk from the session's `cwd` upward — nearest
|
|
1613
|
+
* `.workerdeck.json` wins, so a session started in `packages/ui` still says
|
|
1614
|
+
* "WorkerDeck" — over the *realpath'd* cwd, which is what makes `root`
|
|
1615
|
+
* canonical below.
|
|
1616
|
+
*
|
|
1617
|
+
* The file's schema, stated here because this type is its wire projection
|
|
1618
|
+
* (clients never read the file; the gateway is its only parser):
|
|
1619
|
+
*
|
|
1620
|
+
* ```json
|
|
1621
|
+
* { "name": "WorkerDeck", "icon": "layers" }
|
|
1622
|
+
* { "name": "WorkerDeck", "icon": "./docs/assets/icon.png" }
|
|
1623
|
+
* ```
|
|
1624
|
+
*
|
|
1625
|
+
* Both keys optional, unknown keys ignored (forward compatibility). An empty
|
|
1626
|
+
* `{}` still marks its directory as the project root — grouping is the point,
|
|
1627
|
+
* and the name falls back to the root's basename. `icon` is one string with a
|
|
1628
|
+
* total classification rule: a value ending in `.png`/`.svg`
|
|
1629
|
+
* (case-insensitive) is a repo-relative image path — relative only, since the
|
|
1630
|
+
* file is checked into a repo that clones onto other machines, where an
|
|
1631
|
+
* absolute path is wrong by construction — and anything else must be a
|
|
1632
|
+
* lucide-shaped glyph name (`^[a-z0-9]+(-[a-z0-9]+)*$`) or it is ignored. The
|
|
1633
|
+
* two shapes cannot collide (a glyph name contains no dot), so the rule is a
|
|
1634
|
+
* classification, not a guess. Every degradation degrades *fieldwise and
|
|
1635
|
+
* silently*: a malformed or oversized file is skipped and the walk continues
|
|
1636
|
+
* to an ancestor (a broken nested file must not shadow the repo root's valid
|
|
1637
|
+
* one), a junk name falls back to the basename, a junk or escaping icon is
|
|
1638
|
+
* dropped — a session must never fail, or even warn, because of a display
|
|
1639
|
+
* declaration.
|
|
1640
|
+
*
|
|
1641
|
+
* `root` — the canonical (realpath'd) absolute directory holding the file, on
|
|
1642
|
+
* the **gateway's** filesystem — is the grouping key: two sessions are in the
|
|
1643
|
+
* same project iff same root *on the same gateway* (a remote gateway's
|
|
1644
|
+
* identical-looking path is another machine's directory — the `ScopeRoot`
|
|
1645
|
+
* argument). A *name* is not a key: two repos can both be called "api".
|
|
1646
|
+
* Canonicalizing at discovery is what makes two differently-spelled cwds of
|
|
1647
|
+
* one project agree on it.
|
|
1648
|
+
*
|
|
1649
|
+
* Resolved at **serve time** from a TTL cache, never persisted — the same
|
|
1650
|
+
* placement argument as the profile tracker's 0%-after-reset inference: it is
|
|
1651
|
+
* a function of the gateway's current filesystem, and a copy captured into a
|
|
1652
|
+
* parking record would replay a stale name forever. Editing the file shows up
|
|
1653
|
+
* on every session in the project within the TTL, with no migration and no
|
|
1654
|
+
* event.
|
|
1655
|
+
*
|
|
1656
|
+
* Additive at protocol **7**: an optional field on `SessionInfo`, where
|
|
1657
|
+
* absent means exactly what today's wire means (no project declared — render
|
|
1658
|
+
* the folder basename), an old client ignores it, and a new client against an
|
|
1659
|
+
* old gateway sees absent. The icon route is likewise unreachable by
|
|
1660
|
+
* accident: a client only fetches it when this gateway told it an image
|
|
1661
|
+
* exists.
|
|
1662
|
+
*/
|
|
1663
|
+
type ProjectInfo = {
|
|
1664
|
+
/** Display name — the file's `name`, else the root's basename. Never empty. */name: string;
|
|
1665
|
+
/** Canonical absolute path of the directory holding `.workerdeck.json`, on
|
|
1666
|
+
* the gateway's filesystem. The grouping key (per gateway); an opaque string
|
|
1667
|
+
* to clients beyond equality and display. */
|
|
1668
|
+
root: string;
|
|
1669
|
+
/** Absent = the file declared none (or declared one the gateway refused —
|
|
1670
|
+
* malformed, escaping, oversized — which a client cannot and must not
|
|
1671
|
+
* distinguish). */
|
|
1672
|
+
icon?: ProjectIcon;
|
|
1673
|
+
};
|
|
1131
1674
|
type SessionInfo = {
|
|
1132
1675
|
/** Server-assigned id (stable across SDK session forks/resumes). */id: string; /** Underlying Agent SDK session id, once known; use for `resume`. */
|
|
1133
1676
|
sdkSessionId?: string;
|
|
@@ -1158,6 +1701,22 @@ type SessionInfo = {
|
|
|
1158
1701
|
createdAt: number; /** Highest event seq emitted so far; attach with `afterSeq` to catch up. */
|
|
1159
1702
|
lastSeq: number;
|
|
1160
1703
|
pendingPermissionCount: number;
|
|
1704
|
+
/**
|
|
1705
|
+
* Sub-agents this session has running, plus a short tail of settled ones — see
|
|
1706
|
+
* {@link SubagentInfo}. Absent on an engine that has no sidechains and on an
|
|
1707
|
+
* older server; **absent and empty mean the same thing to a client**, so render
|
|
1708
|
+
* nothing rather than "0 sub-agents".
|
|
1709
|
+
*
|
|
1710
|
+
* Bounded on purpose. This rides every row of `GET /sessions`, which a busy
|
|
1711
|
+
* client polls at 1.2s, and it is captured into parking snapshots — the same
|
|
1712
|
+
* attachment-bytes rule that keeps whole files off {@link FilePatch}. Every
|
|
1713
|
+
* *running* sub-agent is always present (they are the live reading and there
|
|
1714
|
+
* are never many at once); settled ones are kept newest-first to
|
|
1715
|
+
* {@link SUBAGENT_HISTORY} and then dropped, so a day-long session with two
|
|
1716
|
+
* hundred Tasks does not grow an unbounded field. A client must therefore not
|
|
1717
|
+
* treat this as the session's full Task history — the transcript is that.
|
|
1718
|
+
*/
|
|
1719
|
+
subagents?: SubagentInfo[];
|
|
1161
1720
|
meta?: Record<string, unknown>; /** Display title: `meta.title` if the host set one, else derived (e.g. first prompt). */
|
|
1162
1721
|
title?: string; /** Cumulative cost across all turns so far (sum of turn_result totals). */
|
|
1163
1722
|
totalCostUsd?: number; /** Cumulative turn count across the session. */
|
|
@@ -1172,6 +1731,13 @@ type SessionInfo = {
|
|
|
1172
1731
|
* `lastSeq` cannot either — it counts every event, and with token streaming on
|
|
1173
1732
|
* that is hundreds per reply. Absent on an older server; a client should fall
|
|
1174
1733
|
* back to `numTurns` rather than showing nothing.
|
|
1734
|
+
*
|
|
1735
|
+
* Monotonic for the session's whole life, **including across a
|
|
1736
|
+
* `conversation_reset`**: after a `/clear` this deliberately exceeds the
|
|
1737
|
+
* number of rows a fresh attach renders. It is an unread *cursor* diffed
|
|
1738
|
+
* against stored monotonic watermarks (see `watermarks.ts`) — resetting it to
|
|
1739
|
+
* the new row count would leave every stored mark above it, and that
|
|
1740
|
+
* session's badge dead until the count caught back up.
|
|
1175
1741
|
*/
|
|
1176
1742
|
activityCount?: number; /** Epoch ms of the most recent emitted event. */
|
|
1177
1743
|
lastActivityAt?: number;
|
|
@@ -1179,6 +1745,15 @@ type SessionInfo = {
|
|
|
1179
1745
|
* {@link CreateSessionRequest.scope}. Echoed by the runner, re-stamped by the
|
|
1180
1746
|
* gateway, and never editable. */
|
|
1181
1747
|
scope?: Record<string, string>;
|
|
1748
|
+
/**
|
|
1749
|
+
* Project identity discovered from the session's `cwd` — see
|
|
1750
|
+
* {@link ProjectInfo}. Stamped by the **gateway at serve time** (runners
|
|
1751
|
+
* never set it; a runner-echoed value would be persisted into parking
|
|
1752
|
+
* records and replay a stale name forever). Absent = no `.workerdeck.json`
|
|
1753
|
+
* in the cwd's ancestry, and also = an older gateway: both mean "render the
|
|
1754
|
+
* folder basename", which is exactly today's behaviour.
|
|
1755
|
+
*/
|
|
1756
|
+
project?: ProjectInfo;
|
|
1182
1757
|
};
|
|
1183
1758
|
/**
|
|
1184
1759
|
* How many transcript rows an event materializes — the unit behind
|
|
@@ -1196,6 +1771,167 @@ type SessionInfo = {
|
|
|
1196
1771
|
* reducer's row rule changes, change this with it.
|
|
1197
1772
|
*/
|
|
1198
1773
|
declare function transcriptActivity(body: SessionEventBody): number;
|
|
1774
|
+
/**
|
|
1775
|
+
* Whether an event is **transcript content** — whether the reducer
|
|
1776
|
+
* (`@workerdeck/react`'s `transcript.ts`, and its Swift mirror) mutates
|
|
1777
|
+
* `items` when it applies it. The rule behind `conversation_reset`'s replay
|
|
1778
|
+
* semantics: the runner keeps its whole event log, but `subscribe()` skips
|
|
1779
|
+
* content below the latest reset so an attaching client does not resurrect a
|
|
1780
|
+
* cleared conversation — while every *state-bearing* event (`system_init`,
|
|
1781
|
+
* `capabilities`, `skills`, `status_changed`, usage and rate-limit readings,
|
|
1782
|
+
* `file_produced`, permission bookkeeping) still replays, because a fresh
|
|
1783
|
+
* attacher with no model list and no cwd is broken, not cleared.
|
|
1784
|
+
*
|
|
1785
|
+
* Deliberately **broader than `transcriptActivity() > 0`**: stream deltas,
|
|
1786
|
+
* tool results (synthetic user messages) and execution lifecycle events count
|
|
1787
|
+
* zero rows but still mutate items — replaying them across a reset would leave
|
|
1788
|
+
* orphaned deltas and results with no parent message.
|
|
1789
|
+
*
|
|
1790
|
+
* `conversation_reset` itself is content under this rule, and that is load-
|
|
1791
|
+
* bearing twice: a *superseded* reset (below a newer one) is skipped with the
|
|
1792
|
+
* conversation it cleared, while the latest reset always replays (the skip is
|
|
1793
|
+
* strictly-below), which is what clears a reconnecting client that still holds
|
|
1794
|
+
* pre-reset rows.
|
|
1795
|
+
*
|
|
1796
|
+
* Lives here beside {@link transcriptActivity} for the same reason: the
|
|
1797
|
+
* reducer owns the rule and the runners filter with it, and the two sides may
|
|
1798
|
+
* not import each other. If the reducer's items-mutating set changes, change
|
|
1799
|
+
* this with it. Unknown/future event types are NOT content — the safe failure
|
|
1800
|
+
* is replaying a stale row, never withholding state.
|
|
1801
|
+
*/
|
|
1802
|
+
declare function transcriptContent(body: SessionEventBody): boolean;
|
|
1803
|
+
/**
|
|
1804
|
+
* The dedupe key for an event that is **last-write-wins** on replay, or
|
|
1805
|
+
* `undefined` for one that must always be delivered.
|
|
1806
|
+
*
|
|
1807
|
+
* The problem: the runner polls context usage and the plan's rate limits after
|
|
1808
|
+
* every turn, so a fifty-turn session's log holds fifty context readings and
|
|
1809
|
+
* fifty per rate-limit window. Replaying all of them is not merely wasteful —
|
|
1810
|
+
* it is *visible*. A client applies each in turn, so opening a session shows
|
|
1811
|
+
* the usage meters counting up from the session's first reading to its last
|
|
1812
|
+
* over the length of the replay, announcing history as if it were news.
|
|
1813
|
+
*
|
|
1814
|
+
* The fix is a backwards scan over the buffered log keeping the first
|
|
1815
|
+
* occurrence of each key, which is `staleReplaySeqs` in `@workerdeck/core`.
|
|
1816
|
+
* The key is per *window* for rate limits, not per event type: the reducer
|
|
1817
|
+
* stores them keyed by window ("so five_hour and seven_day updates don't
|
|
1818
|
+
* clobber each other"), so a single key would keep only the most recently
|
|
1819
|
+
* polled window and silently drop the others.
|
|
1820
|
+
*
|
|
1821
|
+
* **This is a claim about the reducer**, which is why it lives here rather
|
|
1822
|
+
* than in core: only the server coalesces, but only `@workerdeck/react` can
|
|
1823
|
+
* prove the rule correct, and neither package may import the other. The
|
|
1824
|
+
* property that must hold is that coalescing is *unobservable* — folding the
|
|
1825
|
+
* full log and the coalesced log through `applyEvent` yields identical state.
|
|
1826
|
+
* `packages/react/test/replay-coalesce.test.ts` asserts exactly that, over
|
|
1827
|
+
* every event kind. Extend the rule only with a case that test still passes.
|
|
1828
|
+
*
|
|
1829
|
+
* Three kinds are deliberately **excluded** despite looking eligible:
|
|
1830
|
+
*
|
|
1831
|
+
* - `capabilities` — `defaultModel: event.defaultModel ?? base.defaultModel`
|
|
1832
|
+
* is a fallback *merge*, so a later event without one would erase an earlier
|
|
1833
|
+
* event's. (It is also emitted once per session, so there is nothing to win.)
|
|
1834
|
+
* - `model_changed` — `undefined` means "reset to the server default" and the
|
|
1835
|
+
* reducer *keeps* the last known model, so the last event alone is not the
|
|
1836
|
+
* same as the fold.
|
|
1837
|
+
* - `system_init` — pure replace for the reducer, but the server's
|
|
1838
|
+
* `watchAuthSource` reads the **first** one to decide an auth policy, and
|
|
1839
|
+
* parking treats each as a resume point.
|
|
1840
|
+
*
|
|
1841
|
+
* Coalescing never drops the highest-seq event, and that is load-bearing
|
|
1842
|
+
* rather than incidental: the globally-last event is by definition the last of
|
|
1843
|
+
* its own key, so it always survives. `useClaudeSession`'s replay hold waits
|
|
1844
|
+
* for `state.lastSeq` to reach the attach's `session.lastSeq`, and would hang
|
|
1845
|
+
* on a blank panel forever if a coalescer could swallow the final event.
|
|
1846
|
+
*/
|
|
1847
|
+
declare function replayCoalesceKey(body: SessionEventBody): string | undefined;
|
|
1848
|
+
/**
|
|
1849
|
+
* Does a **replay** have to deliver this event, or may it be dropped outright?
|
|
1850
|
+
*
|
|
1851
|
+
* The fifth of the family, and the closest relative of {@link snapshotRetains} —
|
|
1852
|
+
* the same claim ("no client can tell") pointed at the wire instead of at a
|
|
1853
|
+
* store. The difference from {@link replayCoalesceKey} is that this is not
|
|
1854
|
+
* last-write-wins: there is nothing to keep. These are events the reducer reads
|
|
1855
|
+
* and *discards*, so a replay that sends them is spending the reader's network
|
|
1856
|
+
* on frames whose whole effect is `return base`.
|
|
1857
|
+
*
|
|
1858
|
+
* Today that is exactly one thing, and it is the second-largest item in a real
|
|
1859
|
+
* attach: the `stream_delta`s the reducer does not model. Measured over one
|
|
1860
|
+
* 1,270-row session, the delta run was 774 KB, and **~85% of it was frames the
|
|
1861
|
+
* reducer throws away** — `input_json_delta` (a tool call's arguments, streamed
|
|
1862
|
+
* character by character, 383 KB), `signature_delta` (encrypted-thinking
|
|
1863
|
+
* signatures, 153 KB) and the `message_start`/`content_block_start`/`_stop`
|
|
1864
|
+
* scaffolding (244 KB). The reducer models two delta kinds, `text_delta` and
|
|
1865
|
+
* `thinking_delta`; everything else falls through its switch untouched.
|
|
1866
|
+
*
|
|
1867
|
+
* What is deliberately **not** dropped, though the arithmetic would allow it:
|
|
1868
|
+
*
|
|
1869
|
+
* - `thinking_delta` — the Claude SDK delivers thinking blocks whose `thinking`
|
|
1870
|
+
* is `''`, and the reducer backfills them from the accumulated streamed text
|
|
1871
|
+
* (`streamedThinking`). Dropping these erases every thought from a replayed
|
|
1872
|
+
* transcript. This is the same carve-out `snapshotRetains` documents, and it
|
|
1873
|
+
* is the reason that rule is provider-engine-only.
|
|
1874
|
+
* - `text_delta` — superseded by the `assistant_message` that follows it, which
|
|
1875
|
+
* filters the streaming id and rebuilds from the full content blocks. It could
|
|
1876
|
+
* go, but only with a lookahead proving the message arrived, and at 24 KB in
|
|
1877
|
+
* the measured session it is not worth a rule that has to be right about
|
|
1878
|
+
* supersession. A merge is likewise not worth it: a *drop* needs no synthesized
|
|
1879
|
+
* event and therefore no invented seq.
|
|
1880
|
+
*
|
|
1881
|
+
* A live event is never affected — this is about the buffered replay alone — and
|
|
1882
|
+
* the caller must never drop the log's highest-seq event whatever this says, for
|
|
1883
|
+
* the reason {@link replayCoalesceKey} gives: the replay hold waits for
|
|
1884
|
+
* `state.lastSeq` to reach the attach's `session.lastSeq` and would hang on a
|
|
1885
|
+
* blank panel forever.
|
|
1886
|
+
*
|
|
1887
|
+
* The property is the family's usual one and is a test rather than an argument:
|
|
1888
|
+
* folding the full log and the retained log through `applyEvent` yields
|
|
1889
|
+
* identical state (`packages/react/test/replay-retain.test.ts`).
|
|
1890
|
+
*/
|
|
1891
|
+
declare function replayRetains(body: SessionEventBody): boolean;
|
|
1892
|
+
/**
|
|
1893
|
+
* Does a `RunnerSnapshot` keep this event in its persisted log?
|
|
1894
|
+
*
|
|
1895
|
+
* The fourth of the same family, and the same shape of claim as
|
|
1896
|
+
* {@link replayCoalesceKey}: which events a *store* may drop without any client
|
|
1897
|
+
* being able to tell. It exists because a snapshot embeds the whole event log,
|
|
1898
|
+
* and a log is mostly stream deltas — a four-character token rides a ~180-byte
|
|
1899
|
+
* JSON envelope, so the delta run is tens of times the size of the text it
|
|
1900
|
+
* spells, sitting on disk *beside* the `assistant_message` that respells it in
|
|
1901
|
+
* full. That was affordable while a snapshot was written once, at a park. It is
|
|
1902
|
+
* not affordable written after every turn, which is what restart-survival needs.
|
|
1903
|
+
*
|
|
1904
|
+
* So: everything is retained except `stream_delta`. The reason that is safe is
|
|
1905
|
+
* not that deltas are unimportant but that they are **superseded by
|
|
1906
|
+
* construction**. The reducer upserts them under one constant id and the
|
|
1907
|
+
* following `assistant_message` filters exactly that id out and rebuilds from
|
|
1908
|
+
* the full content blocks — and a snapshot may only be taken at a rest point,
|
|
1909
|
+
* where the stream loop has exited and flushed. Both exits flush, including the
|
|
1910
|
+
* error path: an interrupted turn pushes its half-finished buffers into a
|
|
1911
|
+
* durable `assistant_message` before it emits the failed `turn_result`. There is
|
|
1912
|
+
* no rest state in which a delta is the only record of anything.
|
|
1913
|
+
*
|
|
1914
|
+
* **Provider engine only**, and this is the carve-out that must not be lost:
|
|
1915
|
+
* against a *Claude* log the rule would be wrong. The Claude SDK delivers
|
|
1916
|
+
* thinking blocks whose text is `''`, with the human-readable summary existing
|
|
1917
|
+
* only in the delta stream, and the reducer carries the streamed text over to
|
|
1918
|
+
* fill them (`transcript.ts`, the `streamedThinking` backfill). Dropping deltas
|
|
1919
|
+
* there would silently erase every thought from a restored transcript. Today
|
|
1920
|
+
* that is unreachable rather than merely avoided — only the provider engine
|
|
1921
|
+
* implements `park()`/`snapshot()` at all, and `#restore` refuses a snapshot
|
|
1922
|
+
* from another engine — but an engine that gains one inherits this obligation.
|
|
1923
|
+
*
|
|
1924
|
+
* Two properties hold it up, both of which are tests rather than arguments:
|
|
1925
|
+
* folding the full log and the retained log through `applyEvent` yields
|
|
1926
|
+
* identical state (`packages/react/test/snapshot-retain.test.ts`, the same
|
|
1927
|
+
* property `replay-coalesce.test.ts` asserts), and the retained log's last event
|
|
1928
|
+
* still carries the snapshot's own `seq`. The second matters more than it looks:
|
|
1929
|
+
* `transcriptActivity(stream_delta)` is 0, so the count `#restore` recomputes
|
|
1930
|
+
* from the log is bit-identical — a client's unread cursor cannot move — and the
|
|
1931
|
+
* replay hold waits for `state.lastSeq` to reach the attach's `lastSeq`, which a
|
|
1932
|
+
* rule that could drop the final event would hang forever.
|
|
1933
|
+
*/
|
|
1934
|
+
declare function snapshotRetains(body: SessionEventBody): boolean;
|
|
1199
1935
|
/**
|
|
1200
1936
|
* A session in an engine's on-disk store (independent of this server's registry):
|
|
1201
1937
|
* the Agent SDK's session files, or a codex profile's CODEX_HOME threads. Listed
|
|
@@ -1631,5 +2367,5 @@ type QueueStatsResponse = {
|
|
|
1631
2367
|
stats: QueueStats;
|
|
1632
2368
|
};
|
|
1633
2369
|
//#endregion
|
|
1634
|
-
export { ApiMessage, AttachedFrame, ClientFrame, ContentBlock, ContextUsage, ContextUsageCategory, CreateJobRequest, CreateJobResponse, CreateProfileRequest, CreateSessionRequest, CreateSessionResponse, DEFAULT_VIEW_CONFIG, ENGINE_CAPABILITIES, EngineCapabilities, ErrorResponse, Facet, FindHostFilesResponse, GetJobResponse, GetProfileResponse, GetSessionResponse, GroupBy, HostDirEntry, HostFileMatch, HostFileRoot, 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, PermissionDecisionSource, PermissionMode, PermissionRequest, ProfileConfigSnapshot, ProfileDefaults, ProfileEngine, ProfileInfo, ProfileSessionDefaults, ProviderConfig, QuestionBehavior, QueueServerFrame, QueueStats, QueueStatsResponse, RateLimitInfo, ReadHostFileResponse, ResolvePermissionRequest, ResolvePermissionResponse, STATE_LABELS, STATE_ORDER, SaveProfileResponse, ScopeRoot, SdkSessionSummary, ServerFrame, SessionCapability, SessionCommand, SessionEvent, SessionEventBody, SessionFileInfo, SessionGroup, SessionInfo, SessionNotification, SessionNotificationType, SessionRow, SessionState, SessionStatus, SessionWebhookConfig, SkillInfo, SlashCommandInfo, SortBy, SubmitExecutionResultRequest, SubmitExecutionResultResponse, SubsetSummary, TextBlock, ThinkingBlock, ToolCallRequestFrame, ToolExecutionBackend, ToolExecutionOutput, ToolExecutionStatus, ToolResultBlock, ToolUseBlock, UnknownBlock, UpdateProfileRequest, UpdateSessionRequest, UpdateSessionResponse, UploadAttachmentResponse, UserQuestion, UserQuestionOption, ViewConfig, Watermark, WatermarkStore, Watermarks, WebhookConfig, WorkspaceScope, WriteHostFileRequest, WriteHostFileResponse, adaptersOf, clearFilters, filterRows, groupRows, hasFacetFilter, inScope, isJobRun, scopeActive, sessionLabel, sessionState, subsetSummary, supportsPermissionMode, transcriptActivity, unseenCount, watermarkKey };
|
|
2370
|
+
export { ApiMessage, AttachedFrame, ClientFrame, ContentBlock, 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, filterRows, groupRows, hasFacetFilter, imagePartRef, inScope, isJobRun, mergeUsage, orderUsageWindows, projectKey, projectLabel, projectsOf, replayCoalesceKey, replayRetains, runningSubagents, scopeActive, sessionLabel, sessionState, snapshotRetains, subagentLabel, subsetSummary, supportsPermissionMode, transcriptActivity, transcriptContent, unseenCount, usageInfos, watermarkKey };
|
|
1635
2371
|
//# sourceMappingURL=index.d.mts.map
|