@workerdeck/protocol 0.16.0 → 0.18.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 +578 -2
- package/build/index.mjs +312 -8
- package/build/index.mjs.map +1 -1
- package/package.json +1 -1
package/build/index.mjs
CHANGED
|
@@ -13,15 +13,67 @@ const STATE_LABELS = {
|
|
|
13
13
|
};
|
|
14
14
|
function sessionState(info) {
|
|
15
15
|
if (info.pendingPermissionCount > 0 || info.status === "awaiting_approval") return "attention";
|
|
16
|
-
if (info.status === "running" || info.status === "starting") return "working";
|
|
17
16
|
if (info.status === "failed" || info.status === "closed") return "ended";
|
|
17
|
+
if (info.status === "running" || info.status === "starting") return "working";
|
|
18
|
+
if (runningSubagents(info).length > 0) return "working";
|
|
18
19
|
return "idle";
|
|
19
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* The sub-agents a list row draws as live.
|
|
23
|
+
*
|
|
24
|
+
* `sessionState` deliberately does **not** grow a `subagents` bucket — a fifth
|
|
25
|
+
* state would split `working` in two for every client that filters by it,
|
|
26
|
+
* including the ones that have not shipped this yet. Instead `working` *counts*
|
|
27
|
+
* them: a synchronous `Task` keeps the turn in flight so the status already
|
|
28
|
+
* says `working`, and a **background** agent — which outlives its turn on
|
|
29
|
+
* purpose — is the carve-out the extra arm in `sessionState` exists for.
|
|
30
|
+
* That is what makes "sub-agents are an annotation on a working row" true
|
|
31
|
+
* rather than assumed: the row is in the working bucket whichever kind is
|
|
32
|
+
* running, and this list only says more about it.
|
|
33
|
+
*/
|
|
34
|
+
function runningSubagents(info) {
|
|
35
|
+
return (info.subagents ?? []).filter((sub) => sub.status === "running");
|
|
36
|
+
}
|
|
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
|
+
function isAgentRecord(sub) {
|
|
63
|
+
return (sub.agentType?.trim() ?? "") !== "";
|
|
64
|
+
}
|
|
65
|
+
function subagentLabel(sub) {
|
|
66
|
+
const agent = sub.agentType?.trim();
|
|
67
|
+
const description = sub.description?.trim();
|
|
68
|
+
if (agent && description) return `${agent} · ${description}`;
|
|
69
|
+
return agent || description || "Sub-agent";
|
|
70
|
+
}
|
|
20
71
|
const DEFAULT_VIEW_CONFIG = {
|
|
21
72
|
search: "",
|
|
22
73
|
gateways: [],
|
|
23
74
|
adapters: [],
|
|
24
75
|
states: [],
|
|
76
|
+
projects: [],
|
|
25
77
|
scoped: true,
|
|
26
78
|
groupBy: "state",
|
|
27
79
|
sortBy: "recent"
|
|
@@ -31,10 +83,90 @@ const DEFAULT_VIEW_CONFIG = {
|
|
|
31
83
|
function adaptersOf(rows) {
|
|
32
84
|
return [...new Set(rows.map((r) => r.adapter))].sort();
|
|
33
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* The projects actually present, as `{ key, label }` for a filter control —
|
|
88
|
+
* derived like {@link adaptersOf}, and paired because the two halves differ:
|
|
89
|
+
* the *key* is what {@link ViewConfig.projects} holds (gateway-qualified root,
|
|
90
|
+
* so a rename regroups nothing) and the *label* is what a person picks by.
|
|
91
|
+
*
|
|
92
|
+
* Sorted by label, deduped by key. Two projects with the same name on two
|
|
93
|
+
* gateways therefore stay two entries wearing one word — which is honest: they
|
|
94
|
+
* really are two different directories, and the alternative is a filter that
|
|
95
|
+
* silently selects both.
|
|
96
|
+
*/
|
|
97
|
+
function projectsOf(rows) {
|
|
98
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
99
|
+
for (const row of rows) byKey.set(projectKey(row), projectLabel(row));
|
|
100
|
+
return [...byKey].map(([key, label]) => ({
|
|
101
|
+
key,
|
|
102
|
+
label
|
|
103
|
+
})).sort((a, b) => a.label.toLowerCase().localeCompare(b.label.toLowerCase()));
|
|
104
|
+
}
|
|
34
105
|
function sessionLabel(info) {
|
|
35
106
|
return info.title ?? info.id.slice(0, 8);
|
|
36
107
|
}
|
|
37
108
|
/**
|
|
109
|
+
* The project facet's grouping key: gateway id + the project root, falling
|
|
110
|
+
* back to the session's cwd when no project is declared.
|
|
111
|
+
*
|
|
112
|
+
* The root and not the name, because a name is not a key (two repos can both
|
|
113
|
+
* be called "api", and a rename must regroup nothing); qualified by gateway,
|
|
114
|
+
* because a remote gateway's identical-looking path is another machine's
|
|
115
|
+
* directory — the same rule `ScopeRoot` states. The cwd fallback is what makes
|
|
116
|
+
* grouping by project useful before anyone has written a `.workerdeck.json`:
|
|
117
|
+
* undeclared sessions group by their folder, declared ones by their root, and
|
|
118
|
+
* a session in `packages/ui` joins its repo's group the moment the file
|
|
119
|
+
* exists. Sessions with no cwd at all (a filesystem-less engine) share one
|
|
120
|
+
* per-gateway bucket — see {@link projectLabel}.
|
|
121
|
+
*/
|
|
122
|
+
function projectKey(row) {
|
|
123
|
+
return `${row.hostId}:${normalizePath(row.info.project?.root ?? row.info.cwd)}`;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* What a project group (or a row's project slot) is called: the declared name,
|
|
127
|
+
* else the cwd's basename — the exact string clients rendered before this
|
|
128
|
+
* feature existed, so an undeclared project looks like today. 'No project' is
|
|
129
|
+
* only ever the no-cwd case (a sandboxed provider session), where there is no
|
|
130
|
+
* folder to name.
|
|
131
|
+
*
|
|
132
|
+
* Takes only the `info` it reads, so a surface holding a bare `SessionInfo` —
|
|
133
|
+
* a row component, an iOS cell — can call it without inventing the rest of a
|
|
134
|
+
* `SessionRow`. That matters more than it looks: this string is what a client
|
|
135
|
+
* renders *in place of* the cwd basename it used to draw, and two spellings of
|
|
136
|
+
* it would put the list and its group headers on different names.
|
|
137
|
+
*/
|
|
138
|
+
function projectLabel(row) {
|
|
139
|
+
const name = row.info.project?.name;
|
|
140
|
+
if (name) return name;
|
|
141
|
+
const dir = normalizePath(row.info.cwd);
|
|
142
|
+
return dir.slice(dir.lastIndexOf("/") + 1) || "No project";
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Where inside its project a session actually sits — the cwd with the project
|
|
146
|
+
* root taken off the front, or `undefined` when it sits at the root, has no
|
|
147
|
+
* declared project, or has no cwd at all.
|
|
148
|
+
*
|
|
149
|
+
* The companion to {@link projectLabel}, and it exists for one situation: a list
|
|
150
|
+
* **grouped by project**. There the header has already said the project's name,
|
|
151
|
+
* so repeating it on every row spends the row's most valuable line on the one
|
|
152
|
+
* fact the reader already has. What the header cannot say is which *part* of the
|
|
153
|
+
* project a session is working in, and two sessions in the same repo are told
|
|
154
|
+
* apart by exactly that.
|
|
155
|
+
*
|
|
156
|
+
* Undefined is the honest answer for a session at the project root, and callers
|
|
157
|
+
* must render nothing rather than a `.` or a repeated name — the slot simply
|
|
158
|
+
* goes away, which is the point.
|
|
159
|
+
*/
|
|
160
|
+
function projectSubpath(row) {
|
|
161
|
+
const root = row.info.project?.root;
|
|
162
|
+
if (root === void 0 || !row.info.cwd) return void 0;
|
|
163
|
+
const base = normalizePath(root);
|
|
164
|
+
const dir = normalizePath(row.info.cwd);
|
|
165
|
+
if (dir === base) return void 0;
|
|
166
|
+
if (!dir.startsWith(`${base}/`)) return void 0;
|
|
167
|
+
return dir.slice(base.length + 1) || void 0;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
38
170
|
* This session is a job run — the queue created it, and `JobInfo.sessionId`
|
|
39
171
|
* points at it.
|
|
40
172
|
*
|
|
@@ -49,7 +181,7 @@ function isJobRun(info) {
|
|
|
49
181
|
}
|
|
50
182
|
function matchesSearch(row, needle) {
|
|
51
183
|
if (!needle) return true;
|
|
52
|
-
return sessionLabel(row.info).toLowerCase().includes(needle) || row.info.cwd.toLowerCase().includes(needle) || row.hostName.toLowerCase().includes(needle) || row.adapter.toLowerCase().includes(needle) || row.info.id.startsWith(needle);
|
|
184
|
+
return sessionLabel(row.info).toLowerCase().includes(needle) || row.info.cwd.toLowerCase().includes(needle) || (row.info.project?.name.toLowerCase().includes(needle) ?? false) || row.hostName.toLowerCase().includes(needle) || row.adapter.toLowerCase().includes(needle) || row.info.id.startsWith(needle);
|
|
53
185
|
}
|
|
54
186
|
/** Trailing separators dropped and separators unified, so containment is a
|
|
55
187
|
* plain prefix test on both a posix and a Windows gateway. */
|
|
@@ -78,13 +210,13 @@ function scopeActive(config, scope) {
|
|
|
78
210
|
function filterRows(rows, config, scope) {
|
|
79
211
|
const needle = config.search.trim().toLowerCase();
|
|
80
212
|
const scoping = scopeActive(config, scope) ? scope : void 0;
|
|
81
|
-
return rows.filter((row) => (config.gateways.length === 0 || config.gateways.includes(row.hostId)) && (config.adapters.length === 0 || config.adapters.includes(row.adapter)) && (config.states.length === 0 || config.states.includes(row.state)) && (!scoping || inScope(row, scoping)) && matchesSearch(row, needle));
|
|
213
|
+
return rows.filter((row) => (config.gateways.length === 0 || config.gateways.includes(row.hostId)) && (config.adapters.length === 0 || config.adapters.includes(row.adapter)) && (config.states.length === 0 || config.states.includes(row.state)) && (!config.projects?.length || config.projects.includes(projectKey(row))) && (!scoping || inScope(row, scoping)) && matchesSearch(row, needle));
|
|
82
214
|
}
|
|
83
215
|
function facetKey(row, facet) {
|
|
84
|
-
return facet === "gateway" ? row.hostId : facet === "adapter" ? row.adapter : row.state;
|
|
216
|
+
return facet === "gateway" ? row.hostId : facet === "adapter" ? row.adapter : facet === "project" ? projectKey(row) : row.state;
|
|
85
217
|
}
|
|
86
218
|
function facetLabel(row, facet) {
|
|
87
|
-
return facet === "gateway" ? row.hostName : facet === "adapter" ? row.adapter : STATE_LABELS[row.state];
|
|
219
|
+
return facet === "gateway" ? row.hostName : facet === "adapter" ? row.adapter : facet === "project" ? projectLabel(row) : STATE_LABELS[row.state];
|
|
88
220
|
}
|
|
89
221
|
/** Comparable rank for a facet: states run worst-first (attention before ended),
|
|
90
222
|
* the rest alphabetically by their visible label. */
|
|
@@ -129,7 +261,7 @@ function subsetSummary(config, scope, shown, total) {
|
|
|
129
261
|
if (shown >= total) return void 0;
|
|
130
262
|
const causes = [];
|
|
131
263
|
if (scope && scopeActive(config, scope)) causes.push(scope.label);
|
|
132
|
-
const facets = (config.gateways.length ? 1 : 0) + (config.adapters.length ? 1 : 0) + (config.states.length ? 1 : 0);
|
|
264
|
+
const facets = (config.gateways.length ? 1 : 0) + (config.adapters.length ? 1 : 0) + (config.states.length ? 1 : 0) + (config.projects?.length ? 1 : 0);
|
|
133
265
|
if (facets > 0) causes.push(`${facets} filter${facets === 1 ? "" : "s"}`);
|
|
134
266
|
if (config.search.trim()) causes.push("search");
|
|
135
267
|
return {
|
|
@@ -147,7 +279,7 @@ function subsetSummary(config, scope, shown, total) {
|
|
|
147
279
|
* someone made.
|
|
148
280
|
*/
|
|
149
281
|
function hasFacetFilter(config) {
|
|
150
|
-
return config.search.trim().length > 0 || config.gateways.length > 0 || config.adapters.length > 0 || config.states.length > 0;
|
|
282
|
+
return config.search.trim().length > 0 || config.gateways.length > 0 || config.adapters.length > 0 || config.states.length > 0 || (config.projects?.length ?? 0) > 0;
|
|
151
283
|
}
|
|
152
284
|
/** "Show me everything": every filter off, including scope. The group/sort
|
|
153
285
|
* choices are a layout preference and survive. */
|
|
@@ -318,6 +450,55 @@ function unseenCount(mark, info) {
|
|
|
318
450
|
/** Bumped on any breaking change to events, commands, or REST shapes. */
|
|
319
451
|
const PROTOCOL_VERSION = 7;
|
|
320
452
|
/**
|
|
453
|
+
* How much of a tool result a truncating replay keeps.
|
|
454
|
+
*
|
|
455
|
+
* Chosen against the two clients' *own* budgets, and the relationship is the
|
|
456
|
+
* whole point: the terminal theme shows ~400 characters collapsed and ~2,000
|
|
457
|
+
* open, so at 8,000 the collapsed and open states are **byte-identical to an
|
|
458
|
+
* untruncated attach** and only the uncapped "show everything" press ever
|
|
459
|
+
* fetches. That collapses the entire feature to one press, and it is asserted
|
|
460
|
+
* in a test rather than trusted — lowered below the open budget, this would
|
|
461
|
+
* silently clip the open state with no marker, which is the one failure this
|
|
462
|
+
* design must not have.
|
|
463
|
+
*
|
|
464
|
+
* Measured justification: on one 1,270-row session three `tool_result` frames
|
|
465
|
+
* were 641 / 463 / 396 KB, 68% of a 3.1 MB attach. The cut is *structural* —
|
|
466
|
+
* proportional to the thing that is actually large, wherever in the log it sits
|
|
467
|
+
* — which a row window is not.
|
|
468
|
+
*/
|
|
469
|
+
const TOOL_RESULT_HEAD_CHARS = 8e3;
|
|
470
|
+
/** How many bytes a base64 payload decodes to, without decoding it. */
|
|
471
|
+
function base64Bytes(data) {
|
|
472
|
+
const padding = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
|
|
473
|
+
return Math.max(0, Math.floor(data.length * 3 / 4) - padding);
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Project one `tool_result` content part onto its {@link ImageRefPart}, or
|
|
477
|
+
* `undefined` when the part is not a base64 image and must be delivered as it
|
|
478
|
+
* stands.
|
|
479
|
+
*
|
|
480
|
+
* The rule's **one spelling**, shared by the transform that replaces parts
|
|
481
|
+
* (core), the route that serves them back (server) and the property test that
|
|
482
|
+
* proves the fold is otherwise unchanged (react) — the same reason every other
|
|
483
|
+
* member of this family lives here rather than in whichever package applies it.
|
|
484
|
+
*
|
|
485
|
+
* Deliberately narrow. The corpus holds exactly two non-text part kinds: this
|
|
486
|
+
* one, and the CLI's `tool_reference`, of which every instance across 214
|
|
487
|
+
* sessions totals 122 KB. A "drop non-text parts" rule would sweep those in for
|
|
488
|
+
* no measurable gain, and narrowness is this family's standing habit.
|
|
489
|
+
*/
|
|
490
|
+
function imagePartRef(part, index) {
|
|
491
|
+
if (part.type !== "image") return void 0;
|
|
492
|
+
const source = part.source;
|
|
493
|
+
if (!source || source.type !== "base64" || typeof source.data !== "string") return void 0;
|
|
494
|
+
return {
|
|
495
|
+
type: "image_ref",
|
|
496
|
+
media_type: typeof source.media_type === "string" ? source.media_type : "application/octet-stream",
|
|
497
|
+
bytes: base64Bytes(source.data),
|
|
498
|
+
part_index: index
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
321
502
|
* The static capability record of each engine — the browser-safe default for
|
|
322
503
|
* `ProfileInfo.capabilities` / `SessionInfo.capabilities`, and the single place
|
|
323
504
|
* the values are written down. Core's adapters *reference* this record and a
|
|
@@ -442,6 +623,33 @@ function supportsPermissionMode(engine, mode) {
|
|
|
442
623
|
return ENGINE_CAPABILITIES[engine ?? "claude"].permissionModes.includes(mode);
|
|
443
624
|
}
|
|
444
625
|
/**
|
|
626
|
+
* How many *settled* sub-agents {@link SessionInfo.subagents} keeps behind the
|
|
627
|
+
* running ones. Small on purpose: the point of the tail is that a list row does
|
|
628
|
+
* not go blank the instant a run finishes, not that it is a history.
|
|
629
|
+
*/
|
|
630
|
+
const SUBAGENT_HISTORY = 8;
|
|
631
|
+
/**
|
|
632
|
+
* The list-sized context reading an event carries, or `undefined` for the events
|
|
633
|
+
* that carry none — the rule behind {@link SessionInfo.contextUsage}.
|
|
634
|
+
*
|
|
635
|
+
* Here rather than in each runner for the same reason {@link transcriptActivity}
|
|
636
|
+
* is: it is one rule both sides have to agree on, and three copies of "which
|
|
637
|
+
* events move the reading" is three chances to disagree. Runners fold it in
|
|
638
|
+
* their emit path; **clearing on `conversation_reset` is the caller's half** —
|
|
639
|
+
* this function answers "what does this event say the reading is", and a reset
|
|
640
|
+
* says nothing about the window, it retires the conversation the window
|
|
641
|
+
* described.
|
|
642
|
+
*/
|
|
643
|
+
function contextReading(body) {
|
|
644
|
+
if (body.type !== "context_usage") return void 0;
|
|
645
|
+
const { totalTokens, maxTokens, percentage } = body.usage;
|
|
646
|
+
return {
|
|
647
|
+
totalTokens,
|
|
648
|
+
maxTokens,
|
|
649
|
+
percentage
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
445
653
|
* How many transcript rows an event materializes — the unit behind
|
|
446
654
|
* {@link SessionInfo.activityCount}.
|
|
447
655
|
*
|
|
@@ -457,6 +665,7 @@ function supportsPermissionMode(engine, mode) {
|
|
|
457
665
|
* reducer's row rule changes, change this with it.
|
|
458
666
|
*/
|
|
459
667
|
function transcriptActivity(body) {
|
|
668
|
+
if ("parentToolUseId" in body && body.parentToolUseId != null) return 0;
|
|
460
669
|
switch (body.type) {
|
|
461
670
|
case "assistant_message": {
|
|
462
671
|
const content = body.message.content;
|
|
@@ -563,10 +772,105 @@ function replayCoalesceKey(body) {
|
|
|
563
772
|
case "context_usage": return "context_usage";
|
|
564
773
|
case "rate_limit": return body.info.rateLimitType ? `rate_limit:${body.info.rateLimitType}` : void 0;
|
|
565
774
|
case "status_changed": return "status_changed";
|
|
775
|
+
case "sdk_event": return body.payload.type === "system" && body.payload.subtype === "status" ? "sdk_event:system:status" : void 0;
|
|
566
776
|
default: return;
|
|
567
777
|
}
|
|
568
778
|
}
|
|
779
|
+
/**
|
|
780
|
+
* Does a **replay** have to deliver this event, or may it be dropped outright?
|
|
781
|
+
*
|
|
782
|
+
* The fifth of the family, and the closest relative of {@link snapshotRetains} —
|
|
783
|
+
* the same claim ("no client can tell") pointed at the wire instead of at a
|
|
784
|
+
* store. The difference from {@link replayCoalesceKey} is that this is not
|
|
785
|
+
* last-write-wins: there is nothing to keep. These are events the reducer reads
|
|
786
|
+
* and *discards*, so a replay that sends them is spending the reader's network
|
|
787
|
+
* on frames whose whole effect is `return base`.
|
|
788
|
+
*
|
|
789
|
+
* Today that is exactly one thing, and it is the second-largest item in a real
|
|
790
|
+
* attach: the `stream_delta`s the reducer does not model. Measured over one
|
|
791
|
+
* 1,270-row session, the delta run was 774 KB, and **~85% of it was frames the
|
|
792
|
+
* reducer throws away** — `input_json_delta` (a tool call's arguments, streamed
|
|
793
|
+
* character by character, 383 KB), `signature_delta` (encrypted-thinking
|
|
794
|
+
* signatures, 153 KB) and the `message_start`/`content_block_start`/`_stop`
|
|
795
|
+
* scaffolding (244 KB). The reducer models two delta kinds, `text_delta` and
|
|
796
|
+
* `thinking_delta`; everything else falls through its switch untouched.
|
|
797
|
+
*
|
|
798
|
+
* What is deliberately **not** dropped, though the arithmetic would allow it:
|
|
799
|
+
*
|
|
800
|
+
* - `thinking_delta` — the Claude SDK delivers thinking blocks whose `thinking`
|
|
801
|
+
* is `''`, and the reducer backfills them from the accumulated streamed text
|
|
802
|
+
* (`streamedThinking`). Dropping these erases every thought from a replayed
|
|
803
|
+
* transcript. This is the same carve-out `snapshotRetains` documents, and it
|
|
804
|
+
* is the reason that rule is provider-engine-only.
|
|
805
|
+
* - `text_delta` — superseded by the `assistant_message` that follows it, which
|
|
806
|
+
* filters the streaming id and rebuilds from the full content blocks. It could
|
|
807
|
+
* go, but only with a lookahead proving the message arrived, and at 24 KB in
|
|
808
|
+
* the measured session it is not worth a rule that has to be right about
|
|
809
|
+
* supersession. A merge is likewise not worth it: a *drop* needs no synthesized
|
|
810
|
+
* event and therefore no invented seq.
|
|
811
|
+
*
|
|
812
|
+
* A live event is never affected — this is about the buffered replay alone — and
|
|
813
|
+
* the caller must never drop the log's highest-seq event whatever this says, for
|
|
814
|
+
* the reason {@link replayCoalesceKey} gives: the replay hold waits for
|
|
815
|
+
* `state.lastSeq` to reach the attach's `session.lastSeq` and would hang on a
|
|
816
|
+
* blank panel forever.
|
|
817
|
+
*
|
|
818
|
+
* The property is the family's usual one and is a test rather than an argument:
|
|
819
|
+
* folding the full log and the retained log through `applyEvent` yields
|
|
820
|
+
* identical state (`packages/react/test/replay-retain.test.ts`).
|
|
821
|
+
*/
|
|
822
|
+
function replayRetains(body) {
|
|
823
|
+
if (body.type !== "stream_delta") return true;
|
|
824
|
+
const delta = body.event;
|
|
825
|
+
if (delta.type !== "content_block_delta") return false;
|
|
826
|
+
return delta.delta?.type === "text_delta" || delta.delta?.type === "thinking_delta";
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Does a `RunnerSnapshot` keep this event in its persisted log?
|
|
830
|
+
*
|
|
831
|
+
* The fourth of the same family, and the same shape of claim as
|
|
832
|
+
* {@link replayCoalesceKey}: which events a *store* may drop without any client
|
|
833
|
+
* being able to tell. It exists because a snapshot embeds the whole event log,
|
|
834
|
+
* and a log is mostly stream deltas — a four-character token rides a ~180-byte
|
|
835
|
+
* JSON envelope, so the delta run is tens of times the size of the text it
|
|
836
|
+
* spells, sitting on disk *beside* the `assistant_message` that respells it in
|
|
837
|
+
* full. That was affordable while a snapshot was written once, at a park. It is
|
|
838
|
+
* not affordable written after every turn, which is what restart-survival needs.
|
|
839
|
+
*
|
|
840
|
+
* So: everything is retained except `stream_delta`. The reason that is safe is
|
|
841
|
+
* not that deltas are unimportant but that they are **superseded by
|
|
842
|
+
* construction**. The reducer upserts them under one constant id and the
|
|
843
|
+
* following `assistant_message` filters exactly that id out and rebuilds from
|
|
844
|
+
* the full content blocks — and a snapshot may only be taken at a rest point,
|
|
845
|
+
* where the stream loop has exited and flushed. Both exits flush, including the
|
|
846
|
+
* error path: an interrupted turn pushes its half-finished buffers into a
|
|
847
|
+
* durable `assistant_message` before it emits the failed `turn_result`. There is
|
|
848
|
+
* no rest state in which a delta is the only record of anything.
|
|
849
|
+
*
|
|
850
|
+
* **Provider engine only**, and this is the carve-out that must not be lost:
|
|
851
|
+
* against a *Claude* log the rule would be wrong. The Claude SDK delivers
|
|
852
|
+
* thinking blocks whose text is `''`, with the human-readable summary existing
|
|
853
|
+
* only in the delta stream, and the reducer carries the streamed text over to
|
|
854
|
+
* fill them (`transcript.ts`, the `streamedThinking` backfill). Dropping deltas
|
|
855
|
+
* there would silently erase every thought from a restored transcript. Today
|
|
856
|
+
* that is unreachable rather than merely avoided — only the provider engine
|
|
857
|
+
* implements `park()`/`snapshot()` at all, and `#restore` refuses a snapshot
|
|
858
|
+
* from another engine — but an engine that gains one inherits this obligation.
|
|
859
|
+
*
|
|
860
|
+
* Two properties hold it up, both of which are tests rather than arguments:
|
|
861
|
+
* folding the full log and the retained log through `applyEvent` yields
|
|
862
|
+
* identical state (`packages/react/test/snapshot-retain.test.ts`, the same
|
|
863
|
+
* property `replay-coalesce.test.ts` asserts), and the retained log's last event
|
|
864
|
+
* still carries the snapshot's own `seq`. The second matters more than it looks:
|
|
865
|
+
* `transcriptActivity(stream_delta)` is 0, so the count `#restore` recomputes
|
|
866
|
+
* from the log is bit-identical — a client's unread cursor cannot move — and the
|
|
867
|
+
* replay hold waits for `state.lastSeq` to reach the attach's `lastSeq`, which a
|
|
868
|
+
* rule that could drop the final event would hang forever.
|
|
869
|
+
*/
|
|
870
|
+
function snapshotRetains(body) {
|
|
871
|
+
return body.type !== "stream_delta";
|
|
872
|
+
}
|
|
569
873
|
//#endregion
|
|
570
|
-
export { DEFAULT_VIEW_CONFIG, ENGINE_CAPABILITIES, PROTOCOL_VERSION, PROVIDER_PERMISSION_MODES, STATE_LABELS, STATE_ORDER, Watermarks, adaptersOf, clearFilters, filterRows, groupRows, hasFacetFilter, inScope, isJobRun, mergeUsage, orderUsageWindows, replayCoalesceKey, scopeActive, sessionLabel, sessionState, subsetSummary, supportsPermissionMode, transcriptActivity, transcriptContent, unseenCount, usageInfos, watermarkKey };
|
|
874
|
+
export { DEFAULT_VIEW_CONFIG, ENGINE_CAPABILITIES, PROTOCOL_VERSION, PROVIDER_PERMISSION_MODES, STATE_LABELS, STATE_ORDER, SUBAGENT_HISTORY, TOOL_RESULT_HEAD_CHARS, Watermarks, 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 };
|
|
571
875
|
|
|
572
876
|
//# sourceMappingURL=index.mjs.map
|