@iasiv5/dsh-skip-browser-auth 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,4836 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@iasiv5/dsh-skip-browser-auth",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ //#region lib/types/client/connection.js
8
+ const CONNECTION_DEFAULTS = {
9
+ backoffBaseMs: 500,
10
+ backoffFactor: 2,
11
+ backoffMaxMs: 1e4,
12
+ generationReadyTimeoutMs: 3e3
13
+ };
14
+ const MANUAL_RECONNECT = /* @__PURE__ */ new Error("connection: manual reconnect requested");
15
+ const NETWORK_STATE_CHANGED = /* @__PURE__ */ new Error("connection: browser network state changed");
16
+ function sleep(ms, signal) {
17
+ return new Promise((resolve) => {
18
+ const t = setTimeout(done, ms);
19
+ signal.addEventListener("abort", done, { once: true });
20
+ function done() {
21
+ clearTimeout(t);
22
+ signal.removeEventListener("abort", done);
23
+ resolve();
24
+ }
25
+ });
26
+ }
27
+ function waitForAbort(signal) {
28
+ if (signal.aborted) return Promise.resolve();
29
+ return new Promise((resolve) => {
30
+ signal.addEventListener("abort", () => {
31
+ resolve();
32
+ }, { once: true });
33
+ });
34
+ }
35
+ /**
36
+ * Opens the registered generation source, reconnecting with exponential backoff on loss.
37
+ * State (generation/attempt) is instance-private, never in the store.
38
+ * Sink exceptions do not kill the generation loop.
39
+ */
40
+ var ConnectionController = class {
41
+ source;
42
+ sinks;
43
+ generation = 0;
44
+ attempt = 0;
45
+ current = null;
46
+ retryDelay = null;
47
+ running = false;
48
+ immediateRetry = false;
49
+ networkAvailable = true;
50
+ lastState;
51
+ config;
52
+ constructor(source, sinks = {}, config = {}) {
53
+ this.source = source;
54
+ this.sinks = sinks;
55
+ this.config = {
56
+ ...CONNECTION_DEFAULTS,
57
+ ...config
58
+ };
59
+ }
60
+ /** Idempotent: begin the connect/pump/reconnect loop. */
61
+ start() {
62
+ if (this.running) return;
63
+ this.running = true;
64
+ this.loop();
65
+ }
66
+ /** Stop the loop and abort the current generation source. */
67
+ stop() {
68
+ this.running = false;
69
+ this.current?.abort();
70
+ this.current = null;
71
+ this.retryDelay?.abort();
72
+ this.retryDelay = null;
73
+ }
74
+ /** Reset the retry sequence and replace the current generation or retry delay immediately. */
75
+ reconnect() {
76
+ if (!this.running) return;
77
+ this.attempt = 0;
78
+ this.immediateRetry = true;
79
+ this.emitState("connecting");
80
+ if (!this.isRunning()) return;
81
+ this.current?.abort(MANUAL_RECONNECT);
82
+ this.retryDelay?.abort(MANUAL_RECONNECT);
83
+ }
84
+ /**
85
+ * Suspend automatic retries while offline and restart backoff when the network returns.
86
+ * @param available - whether the browser reports network access.
87
+ */
88
+ setNetworkAvailable(available) {
89
+ if (this.networkAvailable === available) return;
90
+ this.networkAvailable = available;
91
+ this.attempt = 0;
92
+ this.immediateRetry = false;
93
+ if (!this.running) return;
94
+ this.emitState(available ? "connecting" : "disconnected");
95
+ if (!this.isRunning()) return;
96
+ this.current?.abort(NETWORK_STATE_CHANGED);
97
+ this.retryDelay?.abort(NETWORK_STATE_CHANGED);
98
+ }
99
+ backoffCap(attempt) {
100
+ const { backoffBaseMs, backoffFactor, backoffMaxMs } = this.config;
101
+ return Math.min(backoffMaxMs, backoffBaseMs * backoffFactor ** Math.max(0, attempt - 1));
102
+ }
103
+ backoffDelay(attempt) {
104
+ const cap = this.backoffCap(attempt);
105
+ return cap / 2 + Math.random() * (cap / 2);
106
+ }
107
+ isFinalBackoffTier(attempt) {
108
+ const cap = this.backoffCap(attempt);
109
+ const nextCap = this.backoffCap(attempt + 1);
110
+ return cap >= this.config.backoffMaxMs || !Number.isFinite(nextCap) || nextCap <= cap;
111
+ }
112
+ /** Read through a method: stop() flips the flag across awaits, so narrowing from the loop condition must not stick. */
113
+ isRunning() {
114
+ return this.running;
115
+ }
116
+ /** Re-read both mutable liveness guards after a potentially reentrant sink. */
117
+ isGenerationActive(controller) {
118
+ return this.isRunning() && !controller.signal.aborted;
119
+ }
120
+ async loop() {
121
+ let retry = false;
122
+ while (this.running) {
123
+ if (!this.networkAvailable && !this.immediateRetry) {
124
+ const retryDelay = new AbortController();
125
+ this.retryDelay = retryDelay;
126
+ this.emitState("disconnected");
127
+ await waitForAbort(retryDelay.signal);
128
+ if (this.retryDelay === retryDelay) this.retryDelay = null;
129
+ if (!this.isRunning()) return;
130
+ retry = true;
131
+ continue;
132
+ }
133
+ let manualAttempt = false;
134
+ if (retry) {
135
+ const immediate = this.immediateRetry;
136
+ this.immediateRetry = false;
137
+ if (immediate) this.attempt = 0;
138
+ manualAttempt = immediate;
139
+ if (!immediate && this.attempt > 0 && this.isFinalBackoffTier(this.attempt)) {
140
+ const retryDelay = new AbortController();
141
+ this.retryDelay = retryDelay;
142
+ this.emitState("disconnected");
143
+ await waitForAbort(retryDelay.signal);
144
+ if (this.retryDelay === retryDelay) this.retryDelay = null;
145
+ continue;
146
+ }
147
+ const attempt = ++this.attempt;
148
+ this.emitState("connecting");
149
+ if (!this.isRunning()) return;
150
+ if (!immediate) {
151
+ const retryDelay = new AbortController();
152
+ this.retryDelay = retryDelay;
153
+ await sleep(this.backoffDelay(attempt), retryDelay.signal);
154
+ if (this.retryDelay === retryDelay) this.retryDelay = null;
155
+ if (!this.isRunning()) return;
156
+ if (retryDelay.signal.aborted) continue;
157
+ }
158
+ console.warn(`[connection] connection lost, retry #${String(attempt)}`);
159
+ this.callSink(() => {
160
+ this.sinks.onReconnectRequested?.();
161
+ });
162
+ if (!this.isRunning()) return;
163
+ }
164
+ const gen = ++this.generation;
165
+ const ac = new AbortController();
166
+ this.current = ac;
167
+ let sourceReady = false;
168
+ let resolveReady;
169
+ let rejectReady;
170
+ let rejectSourceLost;
171
+ const ready = new Promise((resolve, reject) => {
172
+ resolveReady = resolve;
173
+ rejectReady = reject;
174
+ });
175
+ const sourceLost = new Promise((_resolve, reject) => {
176
+ rejectSourceLost = reject;
177
+ });
178
+ const reportReady = (host) => {
179
+ if (sourceReady) return;
180
+ sourceReady = true;
181
+ resolveReady(host);
182
+ };
183
+ const failed = new Promise((resolve) => {
184
+ const settle = () => {
185
+ if (gen === this.generation && !ac.signal.aborted) ac.abort();
186
+ resolve();
187
+ };
188
+ Promise.resolve().then(() => this.source(ac.signal, reportReady)).then(() => {
189
+ const error = /* @__PURE__ */ new Error("connection generation ended");
190
+ if (!sourceReady) rejectReady(error);
191
+ rejectSourceLost(error);
192
+ settle();
193
+ }, (error) => {
194
+ const failure = error instanceof Error ? error : new Error("connection generation failed", { cause: error });
195
+ if (!sourceReady) rejectReady(failure);
196
+ rejectSourceLost(failure);
197
+ settle();
198
+ });
199
+ });
200
+ try {
201
+ const host = await Promise.race([waitForReady(ready, this.config.generationReadyTimeoutMs, ac.signal), sourceLost]);
202
+ if (ac.signal.aborted) throw new Error("generation aborted during readiness handshake");
203
+ this.attempt = 0;
204
+ this.emitState("connected");
205
+ if (this.isGenerationActive(ac)) this.callSink(() => {
206
+ this.sinks.onConnected?.(host);
207
+ });
208
+ } catch {}
209
+ await failed;
210
+ if (!this.isRunning()) return;
211
+ if (manualAttempt) this.attempt = 0;
212
+ retry = true;
213
+ }
214
+ }
215
+ /** Deduplicated state emission (sink isolation applies). */
216
+ emitState(state) {
217
+ if (this.lastState === state) return;
218
+ this.lastState = state;
219
+ this.callSink(() => this.sinks.onStateChange?.(state));
220
+ }
221
+ /** Sink exception isolation: a business-layer throw is logged only, never affecting pump or reconnect semantics. */
222
+ callSink(fn) {
223
+ try {
224
+ fn();
225
+ } catch (error) {
226
+ console.error("[connection] connection sink threw:", error);
227
+ }
228
+ }
229
+ };
230
+ /** Await source readiness while reporting, but not cancelling, a slow Host. */
231
+ function waitForReady(ready, timeoutMs, signal) {
232
+ return new Promise((resolve, reject) => {
233
+ let settled = false;
234
+ const timeout = setTimeout(() => {
235
+ console.warn(`[connection] generation is still not ready after ${String(timeoutMs)}ms`);
236
+ }, timeoutMs);
237
+ const aborted = () => {
238
+ finish({ error: new Error("connection generation aborted", { cause: signal.reason }) });
239
+ };
240
+ const finish = (outcome) => {
241
+ if (settled) return;
242
+ settled = true;
243
+ clearTimeout(timeout);
244
+ signal.removeEventListener("abort", aborted);
245
+ if ("error" in outcome) reject(outcome.error);
246
+ else resolve(outcome.value);
247
+ };
248
+ signal.addEventListener("abort", aborted, { once: true });
249
+ ready.then((value) => {
250
+ finish({ value });
251
+ }, (error) => {
252
+ finish({ error });
253
+ });
254
+ });
255
+ }
256
+ //#endregion
257
+ //#region ../../util/crypto/lib/index.js
258
+ /**
259
+ * Random v4 UUID, minted from `crypto.getRandomValues`.
260
+ * @returns the UUID string.
261
+ */
262
+ function randomUUID() {
263
+ const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
264
+ const hex = Array.from(bytes, (byte, index) => {
265
+ return (index === 6 ? byte & 15 | 64 : index === 8 ? byte & 63 | 128 : byte).toString(16).padStart(2, "0");
266
+ }).join("");
267
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
268
+ }
269
+ //#endregion
270
+ //#region ../../util/brand/lib/index.js
271
+ /**
272
+ * Duplicate-install-safe nominal primitive helpers.
273
+ *
274
+ * A brand makes structurally identical strings or numbers non-interchangeable
275
+ * at the type level: a `SessionId` cannot be passed where a `ToolCallId` is
276
+ * expected, and an event sequence cannot be passed as a log offset. Comparison,
277
+ * logging, and serialization retain the underlying primitive behavior.
278
+ *
279
+ * This package owns no concrete domain value and keeps no runtime identity or mutable
280
+ * state, so independently installed copies produce interchangeable values.
281
+ *
282
+ * @module @deepseek-ai/dsh-brand
283
+ */
284
+ /**
285
+ * Apply a compile-time string brand without changing the value.
286
+ * @param value - string admitted by the domain that owns the target brand.
287
+ * @returns the same string with the requested compile-time brand.
288
+ */
289
+ function brandString(value) {
290
+ return value;
291
+ }
292
+ /**
293
+ * Apply a compile-time number brand without changing the value.
294
+ * @param value - number admitted by the domain that owns the target brand.
295
+ * @returns the same number with the requested compile-time brand.
296
+ */
297
+ function brandNumber(value) {
298
+ return value;
299
+ }
300
+ //#endregion
301
+ //#region ../../util/values/lib/index.js
302
+ /**
303
+ * Deep-freeze an object graph in place while leaving live AbortSignal objects mutable.
304
+ * @param value - value to freeze.
305
+ * @returns the same value after every reachable enumerable child is frozen.
306
+ */
307
+ function deepFreeze(value) {
308
+ const seen = /* @__PURE__ */ new WeakSet();
309
+ const pending = [{
310
+ kind: "visit",
311
+ node: value
312
+ }];
313
+ while (pending.length > 0) {
314
+ const task = pending.pop();
315
+ /* v8 ignore next -- the loop condition guarantees one pending task. */
316
+ if (task === void 0) continue;
317
+ if (task.kind === "property") {
318
+ pending.push({
319
+ kind: "visit",
320
+ node: task.source[task.key]
321
+ });
322
+ continue;
323
+ }
324
+ const node = task.node;
325
+ if (node === null || typeof node !== "object") continue;
326
+ if (node instanceof AbortSignal) continue;
327
+ if (seen.has(node)) continue;
328
+ seen.add(node);
329
+ Object.freeze(node);
330
+ const keys = Object.keys(node);
331
+ for (let index = keys.length - 1; index >= 0; index--) {
332
+ const key = keys[index];
333
+ /* v8 ignore next -- the loop is bounded by the captured key count. */
334
+ if (key === void 0) continue;
335
+ pending.push({
336
+ kind: "property",
337
+ source: node,
338
+ key
339
+ });
340
+ }
341
+ }
342
+ return value;
343
+ }
344
+ //#endregion
345
+ //#region ../../llm/llm/lib/types/message.js
346
+ /** Message value types, identity, and immutable construction helpers. */
347
+ /**
348
+ * Detach and deep-freeze a message whose identity already exists.
349
+ * @param message - complete message, including its stable identity.
350
+ * @returns an immutable snapshot that preserves the identity.
351
+ */
352
+ function freezeMessage(message) {
353
+ return deepFreeze(structuredClone(message));
354
+ }
355
+ /**
356
+ * Create one identified message and freeze it before publication.
357
+ * @param input - complete role, content, and source for a new message.
358
+ * @returns an immutable message with a fresh stable identity.
359
+ */
360
+ function createMessage(input) {
361
+ return freezeMessage({
362
+ ...input,
363
+ id: brandString(randomUUID())
364
+ });
365
+ }
366
+ /**
367
+ * Create one identified user-role message and freeze it before publication.
368
+ * @param input - complete content and source for a new user message.
369
+ * @returns an immutable user message with a fresh stable identity.
370
+ */
371
+ function createUserMessage(input) {
372
+ return createMessage({
373
+ ...input,
374
+ role: "user"
375
+ });
376
+ }
377
+ /**
378
+ * Create one identified model-produced assistant message and freeze it before publication.
379
+ * @param input - complete content plus the provider, model, and optional replay state for a new assistant message.
380
+ * @returns an immutable assistant message with fixed role/source tags and a fresh stable identity.
381
+ */
382
+ function createAssistantMessage(input) {
383
+ return createMessage({
384
+ role: "assistant",
385
+ content: input.content,
386
+ source: {
387
+ kind: "model",
388
+ ...input.source
389
+ }
390
+ });
391
+ }
392
+ /**
393
+ * Create and freeze one identified tool-result message.
394
+ * @param input - call identity, raw result blocks, and outcome.
395
+ * @returns an immutable user-role tool-result message.
396
+ */
397
+ function createToolResultMessage(input) {
398
+ return createUserMessage({
399
+ source: {
400
+ kind: "tool",
401
+ callId: input.callId
402
+ },
403
+ content: [{
404
+ type: "tool-result",
405
+ toolCallId: input.callId,
406
+ content: input.content,
407
+ isError: input.isError
408
+ }]
409
+ });
410
+ }
411
+ //#endregion
412
+ //#region ../../core/session/lib/types/types.js
413
+ /**
414
+ * Admit a numeric value as an existing Session event position.
415
+ * @param value - non-negative safe integer admitted by the owning log operation.
416
+ * @returns the same number with the Session-sequence brand.
417
+ */
418
+ function SessionSeq(value) {
419
+ if (!Number.isSafeInteger(value) || value < 0 || Object.is(value, -0)) throw new TypeError(`SessionSeq must be a non-negative safe integer, got ${String(value)}`);
420
+ return brandNumber(value);
421
+ }
422
+ /**
423
+ * Admit a numeric value as a Session log offset.
424
+ * @param value - non-negative safe integer used as a gap or prefix length.
425
+ * @returns the same number with the Session-log-offset brand.
426
+ */
427
+ function SessionLogOffset(value) {
428
+ if (!Number.isSafeInteger(value) || value < 0 || Object.is(value, -0)) throw new TypeError(`SessionLogOffset must be a non-negative safe integer, got ${String(value)}`);
429
+ return brandNumber(value);
430
+ }
431
+ //#endregion
432
+ //#region ../../core/session/lib/types/chunk-rows.js
433
+ /**
434
+ * Lossless row packing for `assistant/chunk` delta runs. Providers stream
435
+ * token-sized deltas, so a log stores hundreds of near-identical event lines
436
+ * whose JSON envelopes dwarf their payloads (~56× measured on a real DeepSeek
437
+ * session). This module packs each run of consecutive same-block delta chunks
438
+ * into ONE storage row — `text-chunks`, `reasoning-chunks`, or
439
+ * `tool-call-chunks` — and expands rows back to the exact original events.
440
+ *
441
+ * Packed rows are an encoding vocabulary, NOT session events: they never enter
442
+ * `Session.snapshotEvents()`, have no `SessionEventMap` entry, and use bare (slash-less)
443
+ * type tags so a reader cannot confuse them with the event taxonomy
444
+ * (precedent: the JSONL header line's `session` tag). Persistence and bounded
445
+ * history transport both use the codec. The encoder whitelists exact shapes —
446
+ * anything it does not fully recognize stays verbatim, so unknown fields or
447
+ * future chunk variants lose compression, never data. The decoder validates
448
+ * before expanding and fails loud on a malformed row-tagged value instead of
449
+ * silently dropping a whole run.
450
+ *
451
+ * @module @deepseek-ai/dsh-session/chunk-rows
452
+ */
453
+ /**
454
+ * Test whether an encoded record is a packed chunk row rather than a Session event.
455
+ * @param record - one persistence or bounded-history encoding record.
456
+ * @returns Whether the record is a packed chunk row.
457
+ */
458
+ function isChunkRow(record) {
459
+ return record.type === "text-chunks" || record.type === "reasoning-chunks" || record.type === "tool-call-chunks";
460
+ }
461
+ /**
462
+ * Minimum members before a run packs. Below it a row's envelope rivals the
463
+ * event lines it replaces. A format constant, not a tunable: both layouts
464
+ * decode identically, so changing it never invalidates stored logs.
465
+ */
466
+ const MIN_RUN = 3;
467
+ function isRecord$1(value) {
468
+ return typeof value === "object" && value !== null;
469
+ }
470
+ /** Exact-key check: `value` has every key in `keys` and nothing else. */
471
+ function hasExactKeys(value, keys) {
472
+ return Object.keys(value).length === keys.length && keys.every((k) => Object.hasOwn(value, k));
473
+ }
474
+ /**
475
+ * Classify an event for packing: its delta kind when the ENTIRE shape
476
+ * (envelope, data, chunk — exact keys, primitive types, integer seq/time) is
477
+ * whitelisted, else `undefined` (store verbatim). Inputs come from live typed
478
+ * appends AND parsed fixture files, so the checks are structural, not
479
+ * type-trusted. Integer times keep gap encoding exact: a fractional time would
480
+ * reconstruct through float subtraction/addition, which need not round-trip.
481
+ */
482
+ function classify(event) {
483
+ if (event.type !== "assistant/chunk") return void 0;
484
+ if (!hasExactKeys(event, [
485
+ "type",
486
+ "seq",
487
+ "time",
488
+ "data"
489
+ ])) return void 0;
490
+ if (!Number.isSafeInteger(event.seq) || event.seq < 0 || Object.is(event.seq, -0) || !Number.isSafeInteger(event.time)) return void 0;
491
+ const data = event.data;
492
+ if (!isRecord$1(data) || !hasExactKeys(data, [
493
+ "turn",
494
+ "step",
495
+ "chunk"
496
+ ])) return void 0;
497
+ if (typeof data.turn !== "number" || typeof data.step !== "number") return void 0;
498
+ const chunk = data.chunk;
499
+ if (!isRecord$1(chunk) || typeof chunk.index !== "number") return void 0;
500
+ switch (chunk.type) {
501
+ case "text-delta":
502
+ case "reasoning-delta": return hasExactKeys(chunk, [
503
+ "type",
504
+ "index",
505
+ "text"
506
+ ]) && typeof chunk.text === "string" ? chunk.type : void 0;
507
+ case "tool-call-delta": return (hasExactKeys(chunk, [
508
+ "type",
509
+ "index",
510
+ "id",
511
+ "argumentsDelta"
512
+ ]) || hasExactKeys(chunk, [
513
+ "type",
514
+ "index",
515
+ "id",
516
+ "name",
517
+ "argumentsDelta"
518
+ ]) && typeof chunk.name === "string") && typeof chunk.id === "string" && typeof chunk.argumentsDelta === "string" ? chunk.type : void 0;
519
+ default: return;
520
+ }
521
+ }
522
+ /** The tool-call fields of a whitelisted delta chunk (only after {@link classify} returned `'tool-call-delta'`). */
523
+ function toolCallOf(event) {
524
+ return event.data.chunk;
525
+ }
526
+ /** The block index of a whitelisted delta chunk (not every {@link StreamChunk} variant carries one). */
527
+ function indexOf(event) {
528
+ return event.data.chunk.index;
529
+ }
530
+ /** Whether `next` extends a run ending in `prev` (same kind already checked by the caller). */
531
+ function continues(prev, next, kind) {
532
+ if (next.seq !== prev.seq + 1) return false;
533
+ if (!Number.isSafeInteger(next.time - prev.time)) return false;
534
+ if (next.data.turn !== prev.data.turn || next.data.step !== prev.data.step) return false;
535
+ if (indexOf(next) !== indexOf(prev)) return false;
536
+ if (kind !== "tool-call-delta") return true;
537
+ const a = toolCallOf(prev);
538
+ const b = toolCallOf(next);
539
+ return a.id === b.id && Object.hasOwn(a, "name") === Object.hasOwn(b, "name") && a.name === b.name;
540
+ }
541
+ /** Build the row for a completed run (`run.length >= MIN_RUN`, uniform per {@link continues}). */
542
+ function buildRow(kind, run) {
543
+ const first = run[0];
544
+ const base = {
545
+ turn: first.data.turn,
546
+ step: first.data.step,
547
+ index: indexOf(first),
548
+ dt: run.slice(1).map((event, i) => event.time - run[i].time)
549
+ };
550
+ const envelope = {
551
+ seq0: first.seq,
552
+ time0: first.time
553
+ };
554
+ if (kind === "tool-call-delta") {
555
+ const call = toolCallOf(first);
556
+ return {
557
+ type: "tool-call-chunks",
558
+ ...envelope,
559
+ data: {
560
+ ...base,
561
+ id: brandString(call.id),
562
+ ...Object.hasOwn(call, "name") ? { name: call.name } : {},
563
+ args: run.map((event) => event.data.chunk.argumentsDelta)
564
+ }
565
+ };
566
+ }
567
+ const data = {
568
+ ...base,
569
+ texts: run.map((event) => event.data.chunk.text)
570
+ };
571
+ return kind === "text-delta" ? {
572
+ type: "text-chunks",
573
+ ...envelope,
574
+ data
575
+ } : {
576
+ type: "reasoning-chunks",
577
+ ...envelope,
578
+ data
579
+ };
580
+ }
581
+ /**
582
+ * Pack an event batch for storage: each run of at least {@link MIN_RUN}
583
+ * consecutive whitelisted same-kind, same-block delta chunk events becomes one
584
+ * {@link ChunkRow}; every other event passes through verbatim, in order.
585
+ * Pure and stateless — safe over any array, including a batch whose runs were
586
+ * split by flush boundaries (the split runs simply pack per batch).
587
+ *
588
+ * @param events - the batch to encode, in log order.
589
+ * @returns the storage records to write, one JSONL line each.
590
+ */
591
+ function packChunkRuns(events) {
592
+ const out = [];
593
+ let kind;
594
+ let run = [];
595
+ const flush = () => {
596
+ if (kind !== void 0 && run.length >= MIN_RUN) out.push(buildRow(kind, run));
597
+ else out.push(...run);
598
+ kind = void 0;
599
+ run = [];
600
+ };
601
+ for (const event of events) {
602
+ const k = classify(event);
603
+ if (k === void 0) {
604
+ flush();
605
+ out.push(event);
606
+ continue;
607
+ }
608
+ const delta = event;
609
+ const last = run[run.length - 1];
610
+ if (k === kind && last !== void 0 && continues(last, delta, k)) {
611
+ run.push(delta);
612
+ continue;
613
+ }
614
+ flush();
615
+ kind = k;
616
+ run = [delta];
617
+ }
618
+ flush();
619
+ return out;
620
+ }
621
+ //#endregion
622
+ //#region ../../core/session/lib/types/surface.js
623
+ /**
624
+ * Surface layer on top of the session event log: an ordered view of events
625
+ * that produce LLM messages. The append-only log remains the source of truth.
626
+ *
627
+ * Browser-safe: web clients consume this subpath export, so it must stay free
628
+ * of `node:` imports (they break the vite bundle).
629
+ *
630
+ * @module @deepseek-ai/dsh-session/surface
631
+ */
632
+ /** Runtime counterpart of the message-producing event union. */
633
+ const SURFACE_EVENT_TYPES = new Set([
634
+ "user/message",
635
+ "assistant/message",
636
+ "tool/result"
637
+ ]);
638
+ /**
639
+ * Whether an event type can join the model-visible surface.
640
+ * @param type - event type to test.
641
+ * @returns true for one of the three message-producing event types.
642
+ */
643
+ function isSurfaceEligibleType(type) {
644
+ return SURFACE_EVENT_TYPES.has(type);
645
+ }
646
+ /**
647
+ * Project a single event into the LLM message it derives to, or null when it
648
+ * produces none — a non-surface event (chunk, boundary, log-only record) or an
649
+ * empty-content assistant/message (which exists only to host usage). This is
650
+ * THE per-node projection rule: `Session.deriveMessages` folds it over the
651
+ * live surface, external reconstructors and pure projections fold the same
652
+ * function over a log prefix's surface to rebuild the exact messages any
653
+ * request was built from. The returned message is the already frozen message
654
+ * nested in the event wrapper and shared by delivery, durable history, and
655
+ * model requests.
656
+ * @param event - the event to project.
657
+ * @returns the derived message, or null when the event produces none.
658
+ */
659
+ function deriveEventMessage(event) {
660
+ switch (event.type) {
661
+ case "user/message": return event.data;
662
+ case "assistant/message":
663
+ if (event.data.message.content.length === 0) return null;
664
+ return event.data.message;
665
+ case "tool/result": return event.data.message;
666
+ default: return null;
667
+ }
668
+ }
669
+ /** Create an empty surface fold state. */
670
+ function createFoldState() {
671
+ return {
672
+ nodes: [],
673
+ replaceGeneration: 0
674
+ };
675
+ }
676
+ /** Whether a runtime value is a non-negative safe event sequence. */
677
+ function isEventSeq(value) {
678
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && !Object.is(value, -0);
679
+ }
680
+ /** Whether a runtime value is the exact positional-replacement shape. */
681
+ function isReplaceOp(value) {
682
+ const op = value;
683
+ return Object.keys(op).length === 3 && Object.hasOwn(op, "op") && Object.hasOwn(op, "start") && Object.hasOwn(op, "end") && op["op"] === "replace" && isEventSeq(op["start"]) && isEventSeq(op["end"]);
684
+ }
685
+ /** Validate event-local surface eligibility and return its operation. */
686
+ function surfaceOpOf(event) {
687
+ const raw = event;
688
+ if (!isSurfaceEligibleType(event.type)) {
689
+ if (raw.surfaceOp !== void 0) throw new Error(`session event "${event.type}" is not surface-eligible and cannot carry surfaceOp`);
690
+ if (raw.sourceEventSeqs !== void 0) throw new Error(`session event "${event.type}" is not surface-eligible and cannot carry sourceEventSeqs`);
691
+ return;
692
+ }
693
+ const op = raw.surfaceOp;
694
+ if (op === void 0) throw new Error(`session event "${event.type}" is surface-eligible and requires a surfaceOp marker`);
695
+ if (op === "append") return op;
696
+ if (op === null || typeof op !== "object" || Array.isArray(op)) throw new Error(`session event "${event.type}" carries an invalid surfaceOp`);
697
+ if (!isReplaceOp(op)) throw new Error(`session event "${event.type}" carries an invalid replace surfaceOp`);
698
+ return op;
699
+ }
700
+ /** Validate cited source-event seqs against prior log entries and the replacement range. */
701
+ function assertProvenance(event, shadowedSeqs) {
702
+ const raw = event.sourceEventSeqs;
703
+ const sources = /* @__PURE__ */ new Set();
704
+ if (raw !== void 0) {
705
+ if (!Array.isArray(raw)) throw new Error(`sourceEventSeqs on event at seq ${event.seq} must be an array when present`);
706
+ if (raw.length === 0 && event.type !== "assistant/message") throw new Error("sourceEventSeqs must not be empty except on assistant/message");
707
+ let nonEarlierSource;
708
+ for (const source of raw) {
709
+ if (!isEventSeq(source)) throw new Error(`session event "${event.type}" sourceEventSeqs must densely contain non-negative safe integers`);
710
+ sources.add(source);
711
+ if (nonEarlierSource === void 0 && source >= event.seq) nonEarlierSource = source;
712
+ }
713
+ if (sources.size !== raw.length) throw new Error("sourceEventSeqs must not contain duplicates");
714
+ if (nonEarlierSource !== void 0) throw new Error(`sourceEventSeqs must reference earlier events: ${nonEarlierSource} >= current seq ${event.seq}`);
715
+ }
716
+ const missing = shadowedSeqs.filter((seq) => !sources.has(seq));
717
+ if (missing.length > 0) throw new Error(`surface replace: sourceEventSeqs must include every shadowed surface node; missing ${missing.join(", ")}`);
718
+ }
719
+ /** Locate one replacement range without mutating the current fold state. */
720
+ function replacementRange(state, op) {
721
+ const startIdx = state.nodes.indexOf(op.start);
722
+ if (startIdx === -1) throw new Error(`surface replace: start seq ${op.start} not found in surface`);
723
+ const endIdx = state.nodes.indexOf(op.end);
724
+ if (endIdx === -1) throw new Error(`surface replace: end seq ${op.end} not found in surface`);
725
+ if (startIdx > endIdx) throw new Error(`surface replace: start seq ${op.start} (index ${startIdx}) is after end seq ${op.end} (index ${endIdx})`);
726
+ return {
727
+ startIdx,
728
+ endIdx,
729
+ shadowedSeqs: state.nodes.slice(startIdx, endIdx + 1)
730
+ };
731
+ }
732
+ /**
733
+ * Deep structural equality over the session-event JSON value domain
734
+ * (null/boolean/number/string, arrays, plain objects). Replaces
735
+ * `node:util`'s isDeepStrictEqual to keep this module browser-safe.
736
+ */
737
+ function isDeepEqualJson(a, b) {
738
+ if (a === b) return true;
739
+ if (Array.isArray(a) || Array.isArray(b)) {
740
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
741
+ return a.every((item, i) => isDeepEqualJson(item, b[i]));
742
+ }
743
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
744
+ const aKeys = Object.keys(a);
745
+ const bRecord = b;
746
+ if (aKeys.length !== Object.keys(b).length) return false;
747
+ return aKeys.every((key) => Object.hasOwn(b, key) && isDeepEqualJson(a[key], bRecord[key]));
748
+ }
749
+ /** Restrict a tool-result replacement to one current result's content. */
750
+ function assertToolResultRewrite(event, shadowedSeqs, events, baseSeq) {
751
+ if (event.type !== "tool/result") return;
752
+ if (shadowedSeqs.length !== 1) throw new Error("tool/result surface replacement must rewrite exactly one current node");
753
+ for (const originalSeq of shadowedSeqs) {
754
+ const original = events[originalSeq - baseSeq];
755
+ if (original?.type !== "tool/result") throw new Error("tool/result surface replacement must target a current tool/result");
756
+ const originalRest = { ...original.data };
757
+ const replacementRest = { ...event.data };
758
+ const originalResult = original.data.message.content[0];
759
+ const replacementResult = event.data.message.content[0];
760
+ originalRest["message"] = {
761
+ ...original.data.message,
762
+ content: [{
763
+ ...originalResult,
764
+ content: null
765
+ }]
766
+ };
767
+ replacementRest["message"] = {
768
+ ...event.data.message,
769
+ content: [{
770
+ ...replacementResult,
771
+ content: null
772
+ }]
773
+ };
774
+ if (!isDeepEqualJson(originalRest, replacementRest)) throw new Error("tool/result surface replacement may change only content");
775
+ }
776
+ }
777
+ /** Validate one event at its replay boundary and prepare its atomic fold transition. */
778
+ function planSurfaceEvent(state, event, expectedSeq, events, baseSeq) {
779
+ if (event.seq !== expectedSeq) throw new Error(`session event seq ${event.seq} is not contiguous; expected ${expectedSeq}`);
780
+ const surfaceOp = surfaceOpOf(event);
781
+ if (surfaceOp === void 0) return;
782
+ if (surfaceOp === "append") {
783
+ assertProvenance(event, []);
784
+ return {
785
+ kind: "append",
786
+ seq: event.seq
787
+ };
788
+ }
789
+ const range = replacementRange(state, surfaceOp);
790
+ assertProvenance(event, range.shadowedSeqs);
791
+ assertToolResultRewrite(event, range.shadowedSeqs, events, baseSeq);
792
+ return {
793
+ kind: "replace",
794
+ seq: event.seq,
795
+ start: surfaceOp.start,
796
+ end: surfaceOp.end,
797
+ ...range
798
+ };
799
+ }
800
+ /** Apply one event and return replacement metadata only when one occurred. */
801
+ function applySurfaceEvent(state, event, expectedSeq, events, baseSeq) {
802
+ return applySurfacePlan(state, planSurfaceEvent(state, event, expectedSeq, events, baseSeq));
803
+ }
804
+ /** Commit one previously validated surface transition. */
805
+ function applySurfacePlan(state, plan) {
806
+ if (plan?.kind === "append") state.nodes.push(plan.seq);
807
+ else if (plan?.kind === "replace") {
808
+ state.nodes.splice(plan.startIdx, plan.endIdx - plan.startIdx + 1, plan.seq);
809
+ state.replaceGeneration += 1;
810
+ }
811
+ if (plan?.kind !== "replace") return;
812
+ return {
813
+ seq: plan.seq,
814
+ start: plan.start,
815
+ end: plan.end,
816
+ shadowedSeqs: plan.shadowedSeqs
817
+ };
818
+ }
819
+ /**
820
+ * Replay a complete session log through the canonical surface fold.
821
+ * @param events - session events in contiguous seq order.
822
+ * @returns detached current sequences and replacement history.
823
+ * @throws when an event violates surface metadata, source-event references, range, or tool-result rewrite rules.
824
+ */
825
+ function foldSurface(events) {
826
+ const state = createFoldState();
827
+ const replacements = [];
828
+ for (const [index, event] of events.entries()) {
829
+ const replacement = applySurfaceEvent(state, event, SessionSeq(index), events, SessionLogOffset(0));
830
+ if (replacement !== void 0) replacements.push(replacement);
831
+ }
832
+ return {
833
+ nodes: [...state.nodes],
834
+ replacements
835
+ };
836
+ }
837
+ //#endregion
838
+ //#region lib/types/client/random-uuid.js
839
+ /** Browser-safe UUID generation for client-side wire correlation. */
840
+ /**
841
+ * Generate an RFC 4122 version 4 UUID without requiring a secure context.
842
+ * @returns a UUID backed by `crypto.getRandomValues()`, which browsers expose on insecure origins.
843
+ */
844
+ function randomUuid() {
845
+ const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
846
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
847
+ view.setUint8(6, view.getUint8(6) & 15 | 64);
848
+ view.setUint8(8, view.getUint8(8) & 63 | 128);
849
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
850
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
851
+ }
852
+ //#endregion
853
+ //#region lib/types/client/fixture.js
854
+ const FIXTURE_SESSION_SEARCH_RESULT_LIMIT = 20;
855
+ function isFixtureTokenDelta(chunk) {
856
+ switch (chunk.type) {
857
+ case "text-delta":
858
+ case "reasoning-delta": return chunk.text !== "";
859
+ case "tool-call-delta": return chunk.argumentsDelta !== "" || chunk.name !== void 0;
860
+ default: return false;
861
+ }
862
+ }
863
+ function text(t) {
864
+ return [{
865
+ type: "text",
866
+ text: t
867
+ }];
868
+ }
869
+ function userMessage(content, source = { kind: "user" }) {
870
+ return createUserMessage({
871
+ content,
872
+ source
873
+ });
874
+ }
875
+ function assistantMessage(content, model = "fx-1") {
876
+ return createAssistantMessage({
877
+ content,
878
+ source: {
879
+ provider: "fixture",
880
+ model
881
+ }
882
+ });
883
+ }
884
+ function toolResultMessage(callId, content, isError) {
885
+ return createToolResultMessage({
886
+ callId: brandString(callId),
887
+ content,
888
+ isError
889
+ });
890
+ }
891
+ const MARKDOWN_FIXTURE = [
892
+ "# Markdown fixture",
893
+ "",
894
+ "Assistant output renders **strong text**, *emphasis*, and `inline code`.",
895
+ "",
896
+ "- first item",
897
+ " - nested item",
898
+ "",
899
+ "| Area | State |",
900
+ "| --- | --- |",
901
+ "| history | rendered |",
902
+ "| streaming | stable |",
903
+ "",
904
+ "[DeepSeek](https://www.deepseek.com)",
905
+ "",
906
+ "```ts",
907
+ "const markdown = true",
908
+ "```"
909
+ ].join("\n");
910
+ const USER_MARKDOWN_LITERAL = "用户字面量:# 不渲染 `code` [link](https://example.com)";
911
+ /**
912
+ * SGR wrapper for the terminal output sample below: authoring the escapes as
913
+ * `\u001b` keeps literal control bytes out of this source file.
914
+ * @param code - the SGR parameter (an ANSI color or attribute number).
915
+ * @param body - the text the attribute applies to.
916
+ * @returns the body wrapped in the attribute and a reset.
917
+ */
918
+ function sgr(code, body) {
919
+ return `\u001b[${code}m${body}\u001b[0m`;
920
+ }
921
+ /**
922
+ * Terminal output sample for fixture turn 66, authored to carry every feature
923
+ * the terminal card draws that turn 60's two prompt rows cannot reach:
924
+ * basic-16 SGR foreground runs (green, red, bright-black) that must resolve to
925
+ * `--dsw-*` tokens, a bold run, column-aligned table rows that must scroll
926
+ * rather than fold, more than DEFAULT_TERMINAL_MAX_LINES (16) lines so the
927
+ * height cap collapses the middle. This constant is the visible body; the call
928
+ * site appends the shell result's `[exit code: N]` marker so Client derivation
929
+ * can consume it into the terminal status pill.
930
+ */
931
+ const TERMINAL_OUTPUT_FIXTURE = [
932
+ sgr(1, "Running 4 checks"),
933
+ `${sgr(32, "✓")} typecheck 1.82s`,
934
+ `${sgr(32, "✓")} lint 0.94s`,
935
+ `${sgr(32, "✓")} duplication 2.10s`,
936
+ `${sgr(31, "✗")} unit 8.41s`,
937
+ "",
938
+ sgr(90, "packages/client/ui-primitives/tests/terminal-block.client.spec.tsx"),
939
+ ` ${sgr(31, "FAIL")} caps output at the configured line budget`,
940
+ " expected 16 lines, received 24",
941
+ "",
942
+ "NAME LINES BRANCHES FUNCTIONS UNCOVERED",
943
+ "TerminalBlock.tsx 100% 100% 100% -",
944
+ "ansi.ts 100% 100% 100% -",
945
+ "clipboard.ts 100% 100% 100% -",
946
+ "CodeBlock.tsx 98.4% 96.2% 100% 41-43",
947
+ "highlight.ts 100% 100% 100% -",
948
+ "Pill.tsx 100% 100% 100% -",
949
+ "StateDot.tsx 100% 100% 100% -",
950
+ "markdown/Markdown.tsx 100% 100% 100% -",
951
+ "",
952
+ sgr(31, "1 of 4 checks failed")
953
+ ].join("\n");
954
+ /**
955
+ * Structured grep metadata for the search sample (turn 67). `truncated` with a
956
+ * larger `total` than the retained match count exercises the search card's
957
+ * capped indicator; the file with more than CHAT_SEARCH_MAX_LINES rows
958
+ * exercises its head/tail height cap.
959
+ */
960
+ const SEARCH_MATCHES_FIXTURE = [
961
+ {
962
+ path: "packages/client/ui-primitives/src/SearchBlock.tsx",
963
+ matches: [
964
+ {
965
+ lineNumber: 16,
966
+ line: "export const DEFAULT_SEARCH_MAX_LINES = 16"
967
+ },
968
+ {
969
+ lineNumber: 138,
970
+ line: "export function SearchBlock(props: SearchBlockProps) {"
971
+ },
972
+ {
973
+ lineNumber: 141,
974
+ line: " const [collapsed, setCollapsed] = useState<ReadonlySet<number>>(() => new Set())"
975
+ }
976
+ ]
977
+ },
978
+ {
979
+ path: "packages/client/ui-tool/src/client/tool/models/search-card-model.ts",
980
+ matches: [{
981
+ lineNumber: 45,
982
+ line: "export const CHAT_SEARCH_MAX_LINES = 8"
983
+ }, {
984
+ lineNumber: 130,
985
+ line: "export function searchCardModel(block: ToolCallBlock): SearchCardModel | null {"
986
+ }]
987
+ },
988
+ {
989
+ path: "packages/client/ui-tool/src/client/tool/toolviews/search-row.tsx",
990
+ matches: [
991
+ {
992
+ lineNumber: 34,
993
+ line: "export function SearchRow({ toolName, block, inspect, t }: SearchRowProps) {"
994
+ },
995
+ {
996
+ lineNumber: 36,
997
+ line: " const search = searchCardModel(block)"
998
+ },
999
+ {
1000
+ lineNumber: 56,
1001
+ line: " search={search}"
1002
+ },
1003
+ {
1004
+ lineNumber: 78,
1005
+ line: " yield ctx.slots.register({ name: 'tool.call.toolview', key: 'grep', locale: NS }, SearchRow)"
1006
+ }
1007
+ ]
1008
+ }
1009
+ ];
1010
+ const SEARCH_MATCHES_TEXT = [
1011
+ "Found 9 of 42 matches",
1012
+ "",
1013
+ ...SEARCH_MATCHES_FIXTURE.map((file) => [file.path, ...file.matches.map((m) => `Line ${m.lineNumber}: ${m.line}`)].join("\n")),
1014
+ "",
1015
+ "(Full grep result stored at: fixture://spill/grep-66. Read it to see every match.)"
1016
+ ].join("\n");
1017
+ const SEARCH_PATHS_FIXTURE = [
1018
+ "packages/client/ui-primitives/src/SearchBlock.tsx",
1019
+ "packages/client/ui-primitives/src/SearchBlock.module.css",
1020
+ "packages/client/ui-tool/src/client/tool/models/search-card-model.ts",
1021
+ "packages/client/ui-tool/src/client/tool/toolviews/search-row.tsx",
1022
+ "packages/client/ui-tool/tests/search-card.client.spec.tsx"
1023
+ ];
1024
+ const SEARCH_PATHS_TEXT = [
1025
+ ...SEARCH_PATHS_FIXTURE,
1026
+ "",
1027
+ "(Showing 5 of 23 paths. Full sorted result stored at: fixture://spill/glob-67. Read it to see every path.)"
1028
+ ].join("\n");
1029
+ const READ_SAMPLE_FIRST_LINE = 41;
1030
+ const READ_SAMPLE_SOURCE = [
1031
+ "export interface ReadBlockProps {",
1032
+ " label?: string | undefined",
1033
+ " lines: readonly ReadBlockLine[]",
1034
+ " totalLines: number",
1035
+ " lang?: string | undefined",
1036
+ " maxLines?: number | undefined",
1037
+ " className?: string | undefined",
1038
+ "}",
1039
+ "",
1040
+ "// A windowed read keeps the file line numbers in the gutter.",
1041
+ "const marker = \"fixture read sample\""
1042
+ ];
1043
+ const READ_SAMPLE_LINES = READ_SAMPLE_SOURCE.map((text, index) => ({
1044
+ number: READ_SAMPLE_FIRST_LINE + index,
1045
+ text
1046
+ }));
1047
+ const READ_SAMPLE_PATH = "packages/client/ui-primitives/src/ReadBlock.tsx";
1048
+ const READ_SAMPLE_TOTAL = 180;
1049
+ const READ_SAMPLE_LAST_LINE = READ_SAMPLE_FIRST_LINE + READ_SAMPLE_SOURCE.length - 1;
1050
+ const READ_SAMPLE_TEXT = [
1051
+ `<path>${READ_SAMPLE_PATH}</path>`,
1052
+ "<type>file</type>",
1053
+ "<content>",
1054
+ ...READ_SAMPLE_SOURCE.map((text, index) => `${READ_SAMPLE_FIRST_LINE + index}: ${text}`),
1055
+ "",
1056
+ `(Showing lines ${READ_SAMPLE_FIRST_LINE}-${READ_SAMPLE_LAST_LINE} of ${READ_SAMPLE_TOTAL}. Use offset=${READ_SAMPLE_LAST_LINE + 1} to continue.)`,
1057
+ "</content>"
1058
+ ].join("\n");
1059
+ /**
1060
+ * The `web_search` result metadata for the web-search turn. The sources cover a
1061
+ * titled source with a snippet and date, a hostname-label fallback, and a
1062
+ * titled source without a snippet; `truncated` exercises the capped indicator.
1063
+ */
1064
+ const WEB_SEARCH_META = {
1065
+ answer: "DeepSeek Harness is a plugin-based agent harness on vendored Cordis where **every capability is a plugin**.",
1066
+ sources: [
1067
+ {
1068
+ url: "https://github.com/deepseek-ai/deepseek-harness",
1069
+ title: "DeepSeek Harness — plugin-based agent harness",
1070
+ snippet: "Everything is a plugin: session, tools, agent-loop, and LLM adapters all mount on the same Cordis context.",
1071
+ publishedAt: "2026-07-01"
1072
+ },
1073
+ {
1074
+ url: "https://www.deepseek.com/blog/harness-architecture",
1075
+ snippet: "The capability-seam pattern splits each capability into interface, implementation, and consumer packages."
1076
+ },
1077
+ {
1078
+ url: "https://docs.deepseek.com/harness/plugins",
1079
+ title: "Writing a harness plugin",
1080
+ publishedAt: "2026-06-15"
1081
+ }
1082
+ ],
1083
+ truncated: true
1084
+ };
1085
+ /** The `web_fetch` result metadata for the web-fetch turn. */
1086
+ const WEB_FETCH_META = {
1087
+ url: "https://www.deepseek.com/blog/harness-architecture",
1088
+ statusCode: 200,
1089
+ truncated: false
1090
+ };
1091
+ const DEEPSEEK_REASONING = {
1092
+ efforts: [
1093
+ {
1094
+ id: "off",
1095
+ name: "Off"
1096
+ },
1097
+ {
1098
+ id: "high",
1099
+ name: "High"
1100
+ },
1101
+ {
1102
+ id: "max",
1103
+ name: "Max"
1104
+ }
1105
+ ],
1106
+ defaultEffort: "high"
1107
+ };
1108
+ const OPENAI_REASONING = {
1109
+ efforts: [
1110
+ {
1111
+ id: "off",
1112
+ name: "Off"
1113
+ },
1114
+ {
1115
+ id: "medium",
1116
+ name: "Medium"
1117
+ },
1118
+ {
1119
+ id: "high",
1120
+ name: "High"
1121
+ },
1122
+ {
1123
+ id: "max",
1124
+ name: "Max"
1125
+ }
1126
+ ],
1127
+ defaultEffort: "medium"
1128
+ };
1129
+ /** Catalog served by `session/modelCatalog` (fresh copies per call). */
1130
+ function fixtureModelGroups() {
1131
+ return [{
1132
+ id: "deepseek-official",
1133
+ name: "DeepSeek",
1134
+ models: [{
1135
+ id: "deepseek-v4-flash",
1136
+ name: "DeepSeek-V4-Flash",
1137
+ description: "快速响应",
1138
+ reasoning: DEEPSEEK_REASONING
1139
+ }, {
1140
+ id: "deepseek-v4-pro",
1141
+ name: "DeepSeek-V4-Pro",
1142
+ description: "复杂任务",
1143
+ reasoning: DEEPSEEK_REASONING
1144
+ }]
1145
+ }, {
1146
+ id: "openai",
1147
+ name: "OpenAI",
1148
+ models: [{
1149
+ id: "gpt-5",
1150
+ name: "GPT-5",
1151
+ reasoning: OPENAI_REASONING
1152
+ }]
1153
+ }];
1154
+ }
1155
+ function sid(id) {
1156
+ return id;
1157
+ }
1158
+ const FIXTURE_IMAGE_DATA = "iVBORw0KGgoAAAANSUhEUgAAAKAAAABaCAYAAAA/xl1SAAAAvklEQVR42u3SMQ0AAAjAMIyhELM4AAe8PD1qYFlk9cCXEAEDYkAwIAYEA2JAMCAGBANiQDAgBgQDYkAwIAYEA2JAMCAGBANiQDAgBgQDYkAwIAYEA2JAMCAGxIBCYEAMCAbEgGBADAgGxIBgQAwIBsSAYEAMCAbEgGBADAgGxIBgQAwIBsSAYEAMCAbEgGBADAgGxIAYEAyIAcGAGBAMiAHBgBgQDIgBwYAYEAyIAcGAGBAMiAHBgBgQDIgB4bYWLb6pnOb1xAAAAABJRU5ErkJggg==";
1159
+ const FIXTURE_IMAGE_REF = {
1160
+ attachmentId: "fixture:image",
1161
+ mediaType: "image/png",
1162
+ bytes: 247,
1163
+ width: 160,
1164
+ height: 90,
1165
+ name: "fixture-image.png"
1166
+ };
1167
+ /** Deterministic provider billing attached to fixture assistant messages. */
1168
+ function fixtureUsage(turn, step) {
1169
+ return {
1170
+ inputTokens: 20 + turn % 5,
1171
+ outputTokens: 8 + step,
1172
+ cacheReadTokens: turn === 0 ? 0 : 80,
1173
+ cacheWriteTokens: turn % 10 === 0 ? 4 : 0
1174
+ };
1175
+ }
1176
+ /** fx-alpha history script: 75 turns (~150+ messages -> 4 pages at PAGE_MESSAGES=50),
1177
+ * mixing reasoning blocks / tool call+result / context. */
1178
+ function buildAlphaLog() {
1179
+ const events = [];
1180
+ let time = Date.now() - 36e5;
1181
+ const push = (e) => {
1182
+ const seq = events.length;
1183
+ const data = e["data"];
1184
+ const authored = e["type"] === "assistant/message" && data !== void 0 ? {
1185
+ ...e,
1186
+ data: {
1187
+ ...data,
1188
+ usage: fixtureUsage(data["turn"], data["step"])
1189
+ }
1190
+ } : e;
1191
+ events.push({
1192
+ seq,
1193
+ time: time += 800,
1194
+ ...authored
1195
+ });
1196
+ return seq;
1197
+ };
1198
+ push({
1199
+ type: "request/context",
1200
+ data: {
1201
+ provider: "deepseek-official",
1202
+ model: "deepseek-v4-flash",
1203
+ contextWindow: 128e3
1204
+ }
1205
+ });
1206
+ for (let turn = 0; turn < 60; turn++) {
1207
+ push({
1208
+ type: "turn/start",
1209
+ data: { turn }
1210
+ });
1211
+ const userSeq = push({
1212
+ type: "user/message",
1213
+ surfaceOp: "append",
1214
+ data: userMessage(text(turn === 59 ? USER_MARKDOWN_LITERAL : `问题 ${turn}:fixture 历史消息,用于翻页与渲染验收。`))
1215
+ });
1216
+ if (turn === 0) push({
1217
+ type: "session/title",
1218
+ data: {
1219
+ title: "Fixture 历史会话",
1220
+ messageSeqs: [userSeq],
1221
+ source: { kind: "fallback" }
1222
+ }
1223
+ });
1224
+ if (turn % 9 === 4) push({
1225
+ type: "user/message",
1226
+ surfaceOp: "append",
1227
+ data: userMessage(text(`[fixture] 上下文注入(turn ${turn})`), {
1228
+ kind: "plugin",
1229
+ plugin: "fixture"
1230
+ })
1231
+ });
1232
+ push({
1233
+ type: "step/start",
1234
+ data: {
1235
+ turn,
1236
+ step: 0
1237
+ }
1238
+ });
1239
+ const withTool = turn % 5 === 2;
1240
+ const withReasoning = turn % 3 === 1;
1241
+ const blocks = [];
1242
+ if (withReasoning) blocks.push({
1243
+ type: "reasoning",
1244
+ text: `思考过程 ${turn}:这是一段可折叠的 reasoning 内容。`
1245
+ });
1246
+ blocks.push({
1247
+ type: "text",
1248
+ text: turn === 59 ? MARKDOWN_FIXTURE : `回答 ${turn}:这是 fixture 生成的历史回复正文。`
1249
+ });
1250
+ if (withTool) {
1251
+ const callId = `fx-call-${turn}`;
1252
+ blocks.push({
1253
+ type: "tool-call",
1254
+ id: callId,
1255
+ name: "echo",
1256
+ arguments: `{"text":"turn ${turn}"}`
1257
+ });
1258
+ push({
1259
+ type: "assistant/message",
1260
+ surfaceOp: "append",
1261
+ data: {
1262
+ turn,
1263
+ step: 0,
1264
+ message: assistantMessage(blocks)
1265
+ }
1266
+ });
1267
+ push({
1268
+ type: "tool/call",
1269
+ data: {
1270
+ turn,
1271
+ step: 0,
1272
+ callId,
1273
+ name: "echo",
1274
+ arguments: `{"text":"turn ${turn}"}`
1275
+ }
1276
+ });
1277
+ push({
1278
+ type: "tool/result",
1279
+ surfaceOp: "append",
1280
+ data: {
1281
+ turn,
1282
+ step: 0,
1283
+ message: toolResultMessage(callId, text(`ECHO: TURN ${turn}`), turn % 25 === 12)
1284
+ }
1285
+ });
1286
+ push({
1287
+ type: "step/end",
1288
+ data: {
1289
+ turn,
1290
+ step: 0
1291
+ }
1292
+ });
1293
+ push({
1294
+ type: "step/start",
1295
+ data: {
1296
+ turn,
1297
+ step: 1
1298
+ }
1299
+ });
1300
+ push({
1301
+ type: "assistant/message",
1302
+ surfaceOp: "append",
1303
+ data: {
1304
+ turn,
1305
+ step: 1,
1306
+ message: assistantMessage(text(`工具结果已消化(turn ${turn})。`))
1307
+ }
1308
+ });
1309
+ push({
1310
+ type: "step/end",
1311
+ data: {
1312
+ turn,
1313
+ step: 1
1314
+ }
1315
+ });
1316
+ } else {
1317
+ push({
1318
+ type: "assistant/message",
1319
+ surfaceOp: "append",
1320
+ data: {
1321
+ turn,
1322
+ step: 0,
1323
+ message: assistantMessage(blocks)
1324
+ }
1325
+ });
1326
+ push({
1327
+ type: "step/end",
1328
+ data: {
1329
+ turn,
1330
+ step: 0
1331
+ }
1332
+ });
1333
+ }
1334
+ push({
1335
+ type: "turn/end",
1336
+ data: {
1337
+ turn,
1338
+ reason: { kind: "completed" }
1339
+ }
1340
+ });
1341
+ }
1342
+ const toolTurn = (turn, name, args, resultText, resultMeta) => {
1343
+ const callId = `fx-call-${turn}`;
1344
+ push({
1345
+ type: "turn/start",
1346
+ data: { turn }
1347
+ });
1348
+ push({
1349
+ type: "user/message",
1350
+ surfaceOp: "append",
1351
+ data: userMessage(text(`问题 ${turn}:${name} 样本。`))
1352
+ });
1353
+ push({
1354
+ type: "step/start",
1355
+ data: {
1356
+ turn,
1357
+ step: 0
1358
+ }
1359
+ });
1360
+ push({
1361
+ type: "assistant/message",
1362
+ surfaceOp: "append",
1363
+ data: {
1364
+ turn,
1365
+ step: 0,
1366
+ message: assistantMessage([{
1367
+ type: "tool-call",
1368
+ id: callId,
1369
+ name,
1370
+ arguments: args
1371
+ }])
1372
+ }
1373
+ });
1374
+ push({
1375
+ type: "tool/call",
1376
+ data: {
1377
+ turn,
1378
+ step: 0,
1379
+ callId,
1380
+ name,
1381
+ arguments: args
1382
+ }
1383
+ });
1384
+ push({
1385
+ type: "tool/result",
1386
+ surfaceOp: "append",
1387
+ data: {
1388
+ turn,
1389
+ step: 0,
1390
+ message: toolResultMessage(callId, text(resultText), false),
1391
+ ...resultMeta === void 0 ? {} : { meta: resultMeta }
1392
+ }
1393
+ });
1394
+ push({
1395
+ type: "step/end",
1396
+ data: {
1397
+ turn,
1398
+ step: 0
1399
+ }
1400
+ });
1401
+ push({
1402
+ type: "turn/end",
1403
+ data: {
1404
+ turn,
1405
+ reason: { kind: "completed" }
1406
+ }
1407
+ });
1408
+ };
1409
+ toolTurn(60, "bash", "{\"command\":\"ls -la\\necho done\",\"description\":\"fixture 终端样本\",\"workdir\":\"/tmp/fixture\"}", "total 2\ndrwxr-xr-x fixture\n-rw-r--r-- demo.txt");
1410
+ toolTurn(61, "write", "{\"file_path\":\"notes/demo.txt\",\"content\":\"hello fixture\\n\"}", "wrote notes/demo.txt", { diffs: [{
1411
+ path: "notes/demo.txt",
1412
+ oldText: null,
1413
+ newText: "hello fixture\n"
1414
+ }] });
1415
+ toolTurn(62, "edit", "{\"file_path\":\"notes/demo.txt\",\"old_string\":\"hello\",\"new_string\":\"hello fixture\"}", "已编辑", { diffs: [{
1416
+ path: "notes/demo.txt",
1417
+ oldText: "hello",
1418
+ newText: "hello fixture"
1419
+ }] });
1420
+ toolTurn(63, "write", "{\"file_path\":\"notes/new-demo.txt\",\"content\":\"hello fixture\\n\"}", "已写入", { diffs: [{
1421
+ path: "notes/new-demo.txt",
1422
+ oldText: null,
1423
+ newText: "hello fixture\n"
1424
+ }] });
1425
+ toolTurn(64, "edit", "{\"file_path\":\"src/config.ts\",\"old_string\":\"const timeout = 30\",\"new_string\":\"const timeout = 60\"}", "已编辑", { diffs: [{
1426
+ path: "src/config.ts",
1427
+ oldText: "const timeout = 30",
1428
+ newText: "const timeout = 60"
1429
+ }, {
1430
+ path: "src/config.ts",
1431
+ oldText: "retries: 1",
1432
+ newText: "retries: 3"
1433
+ }] });
1434
+ {
1435
+ const turn = 65;
1436
+ const callId = `fx-call-${turn}`;
1437
+ const args = JSON.stringify({
1438
+ code: "const listing = await tools.bash({ command: \"ls notes\", description: \"List notes\" })\nconst demo = await tools.read({ file_path: \"notes/demo.txt\" })\nawait tools.read({ file_path: \"notes/missing.txt\" }).catch(() => \"tolerated\")\nreturn { listing, demo }",
1439
+ description: "Read the notes files and summarize"
1440
+ });
1441
+ push({
1442
+ type: "turn/start",
1443
+ data: { turn }
1444
+ });
1445
+ push({
1446
+ type: "user/message",
1447
+ surfaceOp: "append",
1448
+ data: userMessage(text(`问题 ${turn}:run_code 样本。`))
1449
+ });
1450
+ push({
1451
+ type: "step/start",
1452
+ data: {
1453
+ turn,
1454
+ step: 0
1455
+ }
1456
+ });
1457
+ push({
1458
+ type: "assistant/message",
1459
+ surfaceOp: "append",
1460
+ data: {
1461
+ turn,
1462
+ step: 0,
1463
+ message: assistantMessage([{
1464
+ type: "tool-call",
1465
+ id: callId,
1466
+ name: "run_code",
1467
+ arguments: args
1468
+ }])
1469
+ }
1470
+ });
1471
+ push({
1472
+ type: "tool/call",
1473
+ data: {
1474
+ turn,
1475
+ step: 0,
1476
+ callId,
1477
+ name: "run_code",
1478
+ arguments: args
1479
+ }
1480
+ });
1481
+ const dispatchPair = (n, name, dispatchArgs, resultText, isError = false) => {
1482
+ push({
1483
+ type: "tool/code-dispatch-start",
1484
+ data: {
1485
+ rootCallId: callId,
1486
+ parentCallId: callId,
1487
+ subCallId: `${callId}:code:${n}`,
1488
+ name,
1489
+ arguments: dispatchArgs
1490
+ }
1491
+ });
1492
+ push({
1493
+ type: "tool/code-dispatch",
1494
+ data: {
1495
+ rootCallId: callId,
1496
+ parentCallId: callId,
1497
+ subCallId: `${callId}:code:${n}`,
1498
+ name,
1499
+ arguments: dispatchArgs,
1500
+ isError,
1501
+ content: [{
1502
+ type: "text",
1503
+ text: resultText
1504
+ }]
1505
+ }
1506
+ });
1507
+ };
1508
+ dispatchPair(1, "bash", {
1509
+ command: "ls notes",
1510
+ description: "List notes"
1511
+ }, "demo.txt\nnew-demo.txt");
1512
+ dispatchPair(2, "read", { file_path: "notes/demo.txt" }, "hello fixture\n");
1513
+ dispatchPair(3, "read", { file_path: "notes/missing.txt" }, "Error: ENOENT: notes/missing.txt not found", true);
1514
+ push({
1515
+ type: "tool/result",
1516
+ surfaceOp: "append",
1517
+ data: {
1518
+ turn,
1519
+ step: 0,
1520
+ message: toolResultMessage(callId, text("{\"listing\":\"demo.txt\\nnew-demo.txt\",\"demo\":\"hello fixture\\n\"}"), false)
1521
+ }
1522
+ });
1523
+ push({
1524
+ type: "step/end",
1525
+ data: {
1526
+ turn,
1527
+ step: 0
1528
+ }
1529
+ });
1530
+ push({
1531
+ type: "turn/end",
1532
+ data: {
1533
+ turn,
1534
+ reason: { kind: "completed" }
1535
+ }
1536
+ });
1537
+ }
1538
+ const fixtureTodos = [
1539
+ {
1540
+ content: "梳理需求",
1541
+ status: "completed"
1542
+ },
1543
+ {
1544
+ content: "实现 fixture 样本",
1545
+ status: "in_progress"
1546
+ },
1547
+ {
1548
+ content: "跑后台构建",
1549
+ status: "in_progress"
1550
+ },
1551
+ {
1552
+ content: "浏览器验收",
1553
+ status: "pending"
1554
+ }
1555
+ ];
1556
+ toolTurn(66, "bash", "{\"command\":\"pnpm run check\",\"description\":\"fixture 终端样本\",\"workdir\":\"/tmp/fixture/deep/nested\"}", `${TERMINAL_OUTPUT_FIXTURE}\n[exit code: 1]`);
1557
+ toolTurn(67, "grep", "{\"pattern\":\"SEARCH_MAX_LINES\",\"path\":\"packages/client\"}", SEARCH_MATCHES_TEXT, {
1558
+ shape: "matches",
1559
+ files: SEARCH_MATCHES_FIXTURE,
1560
+ truncated: true,
1561
+ total: 42
1562
+ });
1563
+ toolTurn(68, "glob", "{\"pattern\":\"**/SearchBlock*\",\"path\":\"packages/client\"}", SEARCH_PATHS_TEXT, {
1564
+ shape: "paths",
1565
+ paths: SEARCH_PATHS_FIXTURE,
1566
+ truncated: true,
1567
+ total: 23
1568
+ });
1569
+ toolTurn(69, "read", `{"file_path":${JSON.stringify(READ_SAMPLE_PATH)},"offset":${READ_SAMPLE_FIRST_LINE}}`, READ_SAMPLE_TEXT, {
1570
+ path: READ_SAMPLE_PATH,
1571
+ offset: READ_SAMPLE_FIRST_LINE,
1572
+ lines: READ_SAMPLE_LINES,
1573
+ totalLines: READ_SAMPLE_TOTAL,
1574
+ lang: "ts"
1575
+ });
1576
+ toolTurn(70, "web_search", "{\"queries\":[\"deepseek harness architecture\"]}", "Search results for deepseek harness architecture.", WEB_SEARCH_META);
1577
+ toolTurn(71, "web_fetch", "{\"url\":\"https://www.deepseek.com/blog/harness-architecture\"}", "# Harness architecture\n\nEverything is a plugin.", WEB_FETCH_META);
1578
+ push({
1579
+ type: "turn/start",
1580
+ data: { turn: 72 }
1581
+ });
1582
+ push({
1583
+ type: "user/message",
1584
+ surfaceOp: "append",
1585
+ data: userMessage(text("问题 72:请完整列出全部一百条条目。"))
1586
+ });
1587
+ push({
1588
+ type: "step/start",
1589
+ data: {
1590
+ turn: 72,
1591
+ step: 0
1592
+ }
1593
+ });
1594
+ push({
1595
+ type: "assistant/message",
1596
+ surfaceOp: "append",
1597
+ data: {
1598
+ turn: 72,
1599
+ step: 0,
1600
+ message: assistantMessage(text("条目 1:第一条。条目 2:第二条。条目 3:这一条写到一半被"))
1601
+ }
1602
+ });
1603
+ push({
1604
+ type: "step/end",
1605
+ data: {
1606
+ turn: 72,
1607
+ step: 0
1608
+ }
1609
+ });
1610
+ push({
1611
+ type: "turn/end",
1612
+ data: {
1613
+ turn: 72,
1614
+ reason: { kind: "max-tokens" }
1615
+ }
1616
+ });
1617
+ push({
1618
+ type: "turn/start",
1619
+ data: { turn: 73 }
1620
+ });
1621
+ push({
1622
+ type: "user/message",
1623
+ surfaceOp: "append",
1624
+ data: userMessage([{
1625
+ type: "image",
1626
+ attachment: FIXTURE_IMAGE_REF
1627
+ }, ...text("历史用户图片")])
1628
+ });
1629
+ push({
1630
+ type: "step/start",
1631
+ data: {
1632
+ turn: 73,
1633
+ step: 0
1634
+ }
1635
+ });
1636
+ push({
1637
+ type: "assistant/message",
1638
+ surfaceOp: "append",
1639
+ data: {
1640
+ turn: 73,
1641
+ step: 0,
1642
+ message: assistantMessage([...text("结构化模型图片:"), {
1643
+ type: "image",
1644
+ attachment: FIXTURE_IMAGE_REF
1645
+ }], "fx-vision")
1646
+ }
1647
+ });
1648
+ push({
1649
+ type: "step/end",
1650
+ data: {
1651
+ turn: 73,
1652
+ step: 0
1653
+ }
1654
+ });
1655
+ push({
1656
+ type: "turn/end",
1657
+ data: {
1658
+ turn: 73,
1659
+ reason: { kind: "completed" }
1660
+ }
1661
+ });
1662
+ toolTurn(74, "todo_write", JSON.stringify({ todos: fixtureTodos }), "Updated todo list: 1 pending, 2 in progress, 1 completed.");
1663
+ const callIndex = events.length - 4;
1664
+ const callTime = events[callIndex]?.time;
1665
+ events.splice(callIndex + 1, 0, {
1666
+ type: "todo/write",
1667
+ time: callTime + 400,
1668
+ data: { todos: fixtureTodos }
1669
+ });
1670
+ events.forEach((e, i) => {
1671
+ e.seq = i;
1672
+ });
1673
+ return events;
1674
+ }
1675
+ /**
1676
+ * Fixture parallel of the plan unit's lifecycle fold. The paired
1677
+ * `command/done` retains successful plan selections and drops failures;
1678
+ * `plan/mode` commits one. `wanted` is exposed for the prompt boundary (the
1679
+ * fixture's step/start parallel).
1680
+ */
1681
+ function foldPlan(log) {
1682
+ let active = false;
1683
+ let wanted = null;
1684
+ let running = null;
1685
+ for (const event of log) {
1686
+ const item = event;
1687
+ if (item.type === "command/run" && item.data?.["name"] === "plan") {
1688
+ const args = item.data["args"];
1689
+ if (typeof args !== "string") continue;
1690
+ running = {
1691
+ commandId: item.data["commandId"],
1692
+ wanted: args.trim() !== "off"
1693
+ };
1694
+ } else if (item.type === "command/done" && item.data !== void 0 && running !== null && item.data["commandId"] === running.commandId) {
1695
+ wanted = item.data["kind"] === "success" && running.wanted !== active ? running.wanted : null;
1696
+ running = null;
1697
+ } else if (item.type === "plan/mode") {
1698
+ active = item.data?.["active"] === true;
1699
+ wanted = null;
1700
+ }
1701
+ }
1702
+ const selected = running?.wanted ?? wanted;
1703
+ return {
1704
+ active,
1705
+ pending: selected !== null && selected !== active,
1706
+ wanted: selected
1707
+ };
1708
+ }
1709
+ /** The plan projection's wire view over the full log. */
1710
+ function planViewOf(log) {
1711
+ const plan = foldPlan(log);
1712
+ return {
1713
+ active: plan.active,
1714
+ pending: plan.pending
1715
+ };
1716
+ }
1717
+ /** Fixture parallel of the host's projection units: whole current values per key over the full log. */
1718
+ /** Fixture preset table (the host PermissionPresetService defaults). */
1719
+ const PERMISSION_PRESETS = {
1720
+ "workspace-write": {
1721
+ sandbox: "workspace-write",
1722
+ approval: "ask",
1723
+ description: "Write inside the workspace and permitted temporary directories; wider retries require approval."
1724
+ },
1725
+ "danger-full-access": {
1726
+ sandbox: "danger-full-access",
1727
+ approval: "never",
1728
+ description: "Full file access without approval prompts."
1729
+ }
1730
+ };
1731
+ /** Host permissions-unit parallel: fold the three knob events, derive the select over the fixture defaults. */
1732
+ function permissionSelectOf(log) {
1733
+ let preset = null;
1734
+ let sandbox = "workspace-write";
1735
+ let approval = "ask";
1736
+ for (const event of log) {
1737
+ const item = event;
1738
+ if (item.type === "permission/preset") preset = item.data["preset"];
1739
+ else if (item.type === "sandbox/mode") sandbox = item.data["mode"];
1740
+ else if (item.type === "approval/policy") approval = item.data["policy"];
1741
+ }
1742
+ const matches = (spec) => spec.sandbox === sandbox && spec.approval === approval;
1743
+ let currentValue = "custom";
1744
+ const folded = preset === null ? void 0 : PERMISSION_PRESETS[preset];
1745
+ if (preset !== null && folded !== void 0 && matches(folded)) currentValue = preset;
1746
+ else for (const [name, spec] of Object.entries(PERMISSION_PRESETS)) if (matches(spec)) {
1747
+ currentValue = name;
1748
+ break;
1749
+ }
1750
+ return {
1751
+ options: [...Object.entries(PERMISSION_PRESETS).map(([value, spec]) => ({
1752
+ value,
1753
+ name: value,
1754
+ description: spec.description
1755
+ })), ...currentValue === "custom" ? [{
1756
+ value: "custom",
1757
+ name: "Custom",
1758
+ description: "Current sandbox and approval settings do not match a preset."
1759
+ }] : []],
1760
+ currentValue
1761
+ };
1762
+ }
1763
+ /** Read one provider usage sample from either durable carrier. */
1764
+ function usageSampleOf(event) {
1765
+ const item = event;
1766
+ const usage = item.type === "assistant/chunk" && item.data.chunk?.type === "usage" ? item.data.chunk.usage : item.type === "assistant/message" ? item.data.usage : void 0;
1767
+ return usage === void 0 || item.data.turn === void 0 || item.data.step === void 0 ? void 0 : {
1768
+ turn: item.data.turn,
1769
+ step: item.data.step,
1770
+ usage
1771
+ };
1772
+ }
1773
+ /** Fixture parallel of token-meter's last-sample-replacing usage projection. */
1774
+ function tokenUsageOf(log) {
1775
+ const totals = {
1776
+ uncachedInputTokens: 0,
1777
+ outputTokens: 0,
1778
+ cacheReadTokens: 0,
1779
+ cacheWriteTokens: 0
1780
+ };
1781
+ let last = null;
1782
+ for (const event of log) {
1783
+ const sample = usageSampleOf(event);
1784
+ if (sample === void 0) continue;
1785
+ const buckets = {
1786
+ uncachedInputTokens: sample.usage.inputTokens,
1787
+ outputTokens: sample.usage.outputTokens,
1788
+ cacheReadTokens: sample.usage.cacheReadTokens ?? 0,
1789
+ cacheWriteTokens: sample.usage.cacheWriteTokens ?? 0
1790
+ };
1791
+ const previous = last?.turn === sample.turn && last.step === sample.step ? last.buckets : void 0;
1792
+ totals.uncachedInputTokens += buckets.uncachedInputTokens - (previous?.uncachedInputTokens ?? 0);
1793
+ totals.outputTokens += buckets.outputTokens - (previous?.outputTokens ?? 0);
1794
+ totals.cacheReadTokens += buckets.cacheReadTokens - (previous?.cacheReadTokens ?? 0);
1795
+ totals.cacheWriteTokens += buckets.cacheWriteTokens - (previous?.cacheWriteTokens ?? 0);
1796
+ last = {
1797
+ turn: sample.turn,
1798
+ step: sample.step,
1799
+ buckets
1800
+ };
1801
+ }
1802
+ return totals;
1803
+ }
1804
+ /** Fixture parallel of session-stats' whole-log counting and wall-time fold. */
1805
+ function sessionStatsOf(log) {
1806
+ const value = {
1807
+ turns: 0,
1808
+ steps: 0,
1809
+ llmMs: 0,
1810
+ toolMs: 0,
1811
+ ttftMs: 0,
1812
+ ttftSteps: 0,
1813
+ decodeMs: 0,
1814
+ decodeTokens: 0
1815
+ };
1816
+ let lastTurn = null;
1817
+ let openStep = null;
1818
+ const pendingCalls = /* @__PURE__ */ new Map();
1819
+ for (const event of log) switch (event.type) {
1820
+ case "step/start":
1821
+ openStep = {
1822
+ turn: event.data.turn,
1823
+ step: event.data.step,
1824
+ startTime: event.time,
1825
+ firstTokenTime: null
1826
+ };
1827
+ break;
1828
+ case "assistant/chunk":
1829
+ if (openStep !== null && openStep.turn === event.data.turn && openStep.step === event.data.step && openStep.firstTokenTime === null && isFixtureTokenDelta(event.data.chunk)) openStep.firstTokenTime = event.time;
1830
+ break;
1831
+ case "assistant/message":
1832
+ if (openStep === null || openStep.turn !== event.data.turn || openStep.step !== event.data.step) break;
1833
+ value.llmMs += Math.max(0, event.time - openStep.startTime);
1834
+ if (openStep.firstTokenTime !== null) {
1835
+ value.ttftMs += Math.max(0, openStep.firstTokenTime - openStep.startTime);
1836
+ value.ttftSteps += 1;
1837
+ const outputTokens = event.data.usage?.outputTokens;
1838
+ if (typeof outputTokens === "number" && Number.isFinite(outputTokens) && outputTokens >= 0) {
1839
+ value.decodeMs += Math.max(0, event.time - openStep.firstTokenTime);
1840
+ value.decodeTokens += outputTokens;
1841
+ }
1842
+ }
1843
+ openStep = null;
1844
+ break;
1845
+ case "tool/call":
1846
+ pendingCalls.set(event.data.callId, event.time);
1847
+ break;
1848
+ case "tool/result": {
1849
+ const callId = event.data.message.source.callId;
1850
+ const dispatched = pendingCalls.get(callId);
1851
+ if (dispatched === void 0) break;
1852
+ pendingCalls.delete(callId);
1853
+ value.toolMs += Math.max(0, event.time - dispatched);
1854
+ break;
1855
+ }
1856
+ case "step/end":
1857
+ if (event.data.turn !== lastTurn) {
1858
+ value.turns += 1;
1859
+ lastTurn = event.data.turn;
1860
+ }
1861
+ value.steps += 1;
1862
+ openStep = null;
1863
+ break;
1864
+ case "turn/end":
1865
+ pendingCalls.clear();
1866
+ break;
1867
+ default: break;
1868
+ }
1869
+ return value;
1870
+ }
1871
+ /** Fixed token-meter heuristic constants mirrored by this client-only fixture. */
1872
+ const CHARS_PER_TOKEN = 4;
1873
+ const BLOCK_OVERHEAD = 4;
1874
+ const ROLE_OVERHEAD = 4;
1875
+ /** Price fixture content with token-meter's fixed-density heuristic. */
1876
+ function estimateFixtureContent(blocks) {
1877
+ const densityPrice = (value) => Math.ceil(value.length / CHARS_PER_TOKEN);
1878
+ return blocks.reduce((tokens, block) => {
1879
+ if (block.type === "text" || block.type === "reasoning") return tokens + densityPrice(block.text) + BLOCK_OVERHEAD;
1880
+ if (block.type === "tool-call") return tokens + densityPrice(block.name) + densityPrice(block.arguments) + BLOCK_OVERHEAD;
1881
+ if (block.type === "tool-result") return tokens + estimateFixtureContent(block.content) + BLOCK_OVERHEAD;
1882
+ return tokens + densityPrice(JSON.stringify(block)) + BLOCK_OVERHEAD;
1883
+ }, 0);
1884
+ }
1885
+ /** Fixture parallel of token-meter's heuristic context-composition projection. */
1886
+ function contextBreakdownOf(log) {
1887
+ const headerEvent = log.findLast((event) => event.type === "request/header");
1888
+ const header = headerEvent === void 0 ? void 0 : headerEvent.data.header;
1889
+ let messageTokens = 0;
1890
+ for (const seq of foldSurface(log).nodes) {
1891
+ const event = log[seq];
1892
+ if (event === void 0) continue;
1893
+ const message = deriveEventMessage(event);
1894
+ if (message !== null) messageTokens += estimateFixtureContent(message.content) + ROLE_OVERHEAD;
1895
+ }
1896
+ return {
1897
+ systemTokens: header?.system === void 0 ? 0 : Math.ceil(header.system.length / CHARS_PER_TOKEN) + ROLE_OVERHEAD,
1898
+ toolsTokens: header?.tools === void 0 || header.tools.length === 0 ? 0 : Math.ceil(JSON.stringify(header.tools).length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD,
1899
+ messageTokens
1900
+ };
1901
+ }
1902
+ /** Latest log-only route context, or undefined before any request ran. */
1903
+ function lastRequestContext(log) {
1904
+ const event = log.findLast((item) => item.type === "request/context");
1905
+ return event === void 0 ? void 0 : event.data;
1906
+ }
1907
+ /**
1908
+ * Fixture parallel of token-meter's request-pressure projection: the last
1909
+ * provider-reported prompt size paired with the last recorded capacity. The
1910
+ * two need not come from one request — see the token-meter README. The host's
1911
+ * `projectedTokens` is deliberately absent: reproducing it would mean
1912
+ * reimplementing the estimator client-side, and every consumer falls back to
1913
+ * the bare sample, so a fixture-driven view simply lags a compaction the way
1914
+ * the projection did before that field existed.
1915
+ */
1916
+ function contextPressureOf(log) {
1917
+ let pressureTokens;
1918
+ for (const event of log) {
1919
+ const sample = usageSampleOf(event);
1920
+ if (sample === void 0) continue;
1921
+ pressureTokens = sample.usage.inputTokens + (sample.usage.cacheReadTokens ?? 0) + (sample.usage.cacheWriteTokens ?? 0);
1922
+ }
1923
+ const contextWindow = lastRequestContext(log)?.contextWindow;
1924
+ return {
1925
+ ...pressureTokens === void 0 ? {} : { pressureTokens },
1926
+ ...contextWindow === void 0 ? {} : { contextWindow }
1927
+ };
1928
+ }
1929
+ function projectionValuesOf(log) {
1930
+ const values = {};
1931
+ values["modelSelection"] = modelSelectionProjectionOf(log);
1932
+ const titleEvent = log.findLast((item) => item.type === "session/title");
1933
+ if (titleEvent !== void 0) values["title"] = titleEvent.data.title;
1934
+ values["todos"] = backscanTodos(log) ?? null;
1935
+ values["permissions"] = permissionSelectOf(log);
1936
+ values["plan"] = planViewOf(log);
1937
+ values["goal"] = backscanGoal(log);
1938
+ values["tokenUsage"] = tokenUsageOf(log);
1939
+ values["contextPressure"] = contextPressureOf(log);
1940
+ values["contextBreakdown"] = contextBreakdownOf(log);
1941
+ values["sessionStats"] = sessionStatsOf(log);
1942
+ values["imageLimits"] = {
1943
+ maxImageBytes: 5 * 1024 * 1024,
1944
+ maxImagesPerMessage: 20,
1945
+ maxMessageImageBytes: 100 * 1024 * 1024,
1946
+ maxImagePixels: 4e7,
1947
+ maxImageDimension: 2e3,
1948
+ mediaTypes: [
1949
+ "image/png",
1950
+ "image/jpeg",
1951
+ "image/webp",
1952
+ "image/gif"
1953
+ ]
1954
+ };
1955
+ return values;
1956
+ }
1957
+ function modelSelectionProjectionOf(log) {
1958
+ let lastUsed = null;
1959
+ let pending = null;
1960
+ for (const event of log) {
1961
+ if (event.type === "model/selection") {
1962
+ pending = event.data;
1963
+ continue;
1964
+ }
1965
+ if (event.type !== "request/header") continue;
1966
+ lastUsed = {
1967
+ provider: event.data.header.config.provider,
1968
+ model: event.data.header.config.model,
1969
+ ...event.data.header.config.reasoningEffort === void 0 ? {} : { reasoningEffort: event.data.header.config.reasoningEffort }
1970
+ };
1971
+ if (sameModelSelection(pending, lastUsed)) pending = null;
1972
+ }
1973
+ return {
1974
+ lastUsed,
1975
+ next: pending ?? lastUsed
1976
+ };
1977
+ }
1978
+ function sameModelSelection(left, right) {
1979
+ return left === right || left !== null && right !== null && left.provider === right.provider && left.model === right.model && left.reasoningEffort === right.reasoningEffort;
1980
+ }
1981
+ /** Host parallel: emit one Session control projection frame per key advanced by the event. */
1982
+ function projectionFramesOf(id, log, event) {
1983
+ const type = event.type;
1984
+ const frames = [];
1985
+ if (type === "model/selection" || type === "request/header") frames.push({
1986
+ type: "projection",
1987
+ sessionId: id,
1988
+ key: "modelSelection",
1989
+ value: modelSelectionProjectionOf(log),
1990
+ seq: event.seq
1991
+ });
1992
+ if (usageSampleOf(event) !== void 0) frames.push({
1993
+ type: "projection",
1994
+ sessionId: id,
1995
+ key: "tokenUsage",
1996
+ value: tokenUsageOf(log),
1997
+ seq: event.seq
1998
+ }, {
1999
+ type: "projection",
2000
+ sessionId: id,
2001
+ key: "contextPressure",
2002
+ value: contextPressureOf(log),
2003
+ seq: event.seq
2004
+ });
2005
+ if (type === "request/context") frames.push({
2006
+ type: "projection",
2007
+ sessionId: id,
2008
+ key: "contextPressure",
2009
+ value: contextPressureOf(log),
2010
+ seq: event.seq
2011
+ });
2012
+ if (type === "request/header" || type === "user/message" || type === "assistant/message" || type === "tool/result") frames.push({
2013
+ type: "projection",
2014
+ sessionId: id,
2015
+ key: "contextBreakdown",
2016
+ value: contextBreakdownOf(log),
2017
+ seq: event.seq
2018
+ });
2019
+ if (type === "assistant/message" || type === "tool/result" || type === "step/end") frames.push({
2020
+ type: "projection",
2021
+ sessionId: id,
2022
+ key: "sessionStats",
2023
+ value: sessionStatsOf(log),
2024
+ seq: event.seq
2025
+ });
2026
+ if (frames.length > 0) return frames;
2027
+ if (type === "session/title") {
2028
+ const values = projectionValuesOf(log);
2029
+ /* v8 ignore next -- the advancing title event is in the log, so the key is present. */
2030
+ if (!Object.hasOwn(values, "title")) return [];
2031
+ return [{
2032
+ type: "projection",
2033
+ sessionId: id,
2034
+ key: "title",
2035
+ value: values["title"],
2036
+ seq: event.seq
2037
+ }];
2038
+ }
2039
+ if (type === "goal/change") return [{
2040
+ type: "projection",
2041
+ sessionId: id,
2042
+ key: "goal",
2043
+ value: backscanGoal(log),
2044
+ seq: event.seq
2045
+ }];
2046
+ if (type === "todo/write" || type === "turn/start") return [{
2047
+ type: "projection",
2048
+ sessionId: id,
2049
+ key: "todos",
2050
+ value: backscanTodos(log) ?? null,
2051
+ seq: event.seq
2052
+ }];
2053
+ if (type === "permission/preset" || type === "sandbox/mode" || type === "approval/policy") return [{
2054
+ type: "projection",
2055
+ sessionId: id,
2056
+ key: "permissions",
2057
+ value: permissionSelectOf(log),
2058
+ seq: event.seq
2059
+ }];
2060
+ const commandData = event;
2061
+ if (type === "plan/mode" || type === "command/run" && commandData.data.name === "plan" && typeof commandData.data.args === "string") return [{
2062
+ type: "projection",
2063
+ sessionId: id,
2064
+ key: "plan",
2065
+ value: planViewOf(log),
2066
+ seq: event.seq
2067
+ }];
2068
+ return [];
2069
+ }
2070
+ /**
2071
+ * Message-boundary paging mirrors the Host contract: count `maxMessages`
2072
+ * backwards from the end and cut at a turn/start boundary.
2073
+ */
2074
+ function pageOf(log, beforeSeq, maxMessages) {
2075
+ const end = beforeSeq === void 0 ? log.length : Math.max(0, Math.min(beforeSeq, log.length));
2076
+ let start = 0;
2077
+ let messages = 0;
2078
+ for (let i = end - 1; i >= 0; i--) {
2079
+ const event = log[i];
2080
+ /* v8 ignore next -- dense-array guard: log seqs are array indexes, i stays within [0, end). */
2081
+ if (event === void 0) break;
2082
+ if (event.type === "user/message" || event.type === "assistant/message") messages++;
2083
+ if (event.type === "turn/start" && messages >= maxMessages) {
2084
+ start = i;
2085
+ break;
2086
+ }
2087
+ }
2088
+ return {
2089
+ records: packChunkRuns(log.slice(start, end)).map((record) => {
2090
+ if (!isChunkRow(record)) return {
2091
+ type: "event",
2092
+ event: record
2093
+ };
2094
+ switch (record.type) {
2095
+ case "text-chunks": return {
2096
+ type: "chunks",
2097
+ event: {
2098
+ type: "chunkrow/text-chunks",
2099
+ seq: record.seq0,
2100
+ time: record.time0,
2101
+ data: record.data
2102
+ }
2103
+ };
2104
+ case "reasoning-chunks": return {
2105
+ type: "chunks",
2106
+ event: {
2107
+ type: "chunkrow/reasoning-chunks",
2108
+ seq: record.seq0,
2109
+ time: record.time0,
2110
+ data: record.data
2111
+ }
2112
+ };
2113
+ case "tool-call-chunks": return {
2114
+ type: "chunks",
2115
+ event: {
2116
+ type: "chunkrow/tool-call-chunks",
2117
+ seq: record.seq0,
2118
+ time: record.time0,
2119
+ data: record.data
2120
+ }
2121
+ };
2122
+ }
2123
+ }),
2124
+ hasMore: start > 0
2125
+ };
2126
+ }
2127
+ /** Fixture mirror of host session-scoped attachment authorization. */
2128
+ function logReferencesAttachment(log, attachmentId) {
2129
+ const visit = (value) => {
2130
+ if (Array.isArray(value)) return value.some(visit);
2131
+ if (typeof value !== "object" || value === null) return false;
2132
+ const record = value;
2133
+ if (record.attachmentId === attachmentId) return true;
2134
+ return Object.values(record).some(visit);
2135
+ };
2136
+ return log.some((event) => visit(event.data));
2137
+ }
2138
+ /** Fixture mirror of first-party message extraction used by session-query. */
2139
+ function searchBlockText(block) {
2140
+ switch (block.type) {
2141
+ case "text": return [block.text];
2142
+ case "reasoning": return [];
2143
+ case "tool-call": return [block.name, block.arguments];
2144
+ case "tool-result": return block.content.flatMap(searchBlockText);
2145
+ default: return [];
2146
+ }
2147
+ }
2148
+ /** One current-surface user/assistant document, if searchable. */
2149
+ function searchEventText(event) {
2150
+ const content = event.type === "user/message" ? event.data.content : event.type === "assistant/message" ? event.data.message.content : void 0;
2151
+ if (content === void 0) return "";
2152
+ return content.flatMap(searchBlockText).map((part) => part.trim()).filter(Boolean).join("\n");
2153
+ }
2154
+ /**
2155
+ * Browser-safe approximation of SQLite FTS5 unicode61 token boundaries.
2156
+ * Keeping phrase matching token-based prevents the development fixture from
2157
+ * promising arbitrary within-token substring behavior that production lacks.
2158
+ */
2159
+ function searchTokenSpans(value) {
2160
+ const text = value.replace(/\s+/gu, " ").trim();
2161
+ const characters = Array.from(text);
2162
+ const tokens = [];
2163
+ let start;
2164
+ let raw = "";
2165
+ const flush = (end) => {
2166
+ if (start !== void 0) {
2167
+ const folded = raw.normalize("NFD").replace(/\p{M}+/gu, "").toLowerCase();
2168
+ if (folded !== "") tokens.push({
2169
+ value: folded,
2170
+ start,
2171
+ end
2172
+ });
2173
+ }
2174
+ start = void 0;
2175
+ raw = "";
2176
+ };
2177
+ for (let index = 0; index < characters.length; index++) {
2178
+ const character = characters[index];
2179
+ const tokenBase = character.normalize("NFD").replace(/\p{M}+/gu, "");
2180
+ if (tokenBase === "") {
2181
+ if (start !== void 0) raw += character;
2182
+ continue;
2183
+ }
2184
+ if (/^[\p{L}\p{N}\p{Co}]+$/u.test(tokenBase)) {
2185
+ start ??= index;
2186
+ raw += character;
2187
+ } else flush(index);
2188
+ }
2189
+ flush(characters.length);
2190
+ return {
2191
+ text,
2192
+ tokens
2193
+ };
2194
+ }
2195
+ /** Count exact contiguous token-phrase occurrences and retain the first display span. */
2196
+ function phraseMatch(document, phrase) {
2197
+ if (phrase.length === 0 || phrase.length > document.length) return {
2198
+ count: 0,
2199
+ start: 0,
2200
+ end: 0
2201
+ };
2202
+ let count = 0;
2203
+ let firstStart = 0;
2204
+ let firstEnd = 0;
2205
+ for (let start = 0; start <= document.length - phrase.length; start++) {
2206
+ if (!phrase.every((token, offset) => document[start + offset]?.value === token)) continue;
2207
+ count++;
2208
+ if (count === 1) {
2209
+ firstStart = document[start]?.start ?? 0;
2210
+ firstEnd = document[start + phrase.length - 1]?.end ?? firstStart;
2211
+ }
2212
+ }
2213
+ return {
2214
+ count,
2215
+ start: firstStart,
2216
+ end: firstEnd
2217
+ };
2218
+ }
2219
+ /** Match-centered fixture excerpt, bounded by Unicode code points for the sidebar. */
2220
+ function searchSnippet(value, matchStart, matchEnd) {
2221
+ const characters = Array.from(value);
2222
+ if (characters.length <= 120) return value;
2223
+ const boundedStart = Math.min(Math.max(0, matchStart), characters.length - 1);
2224
+ const boundedEnd = Math.min(characters.length, Math.max(boundedStart + 1, matchEnd));
2225
+ const center = Math.floor((boundedStart + boundedEnd) / 2);
2226
+ let start = Math.min(characters.length - 118, Math.max(0, center - Math.floor(118 / 2)));
2227
+ let end = start + 118;
2228
+ if (start === 0) end = 119;
2229
+ else if (end === characters.length) start = characters.length - 119;
2230
+ return `${start > 0 ? "…" : ""}${characters.slice(start, end).join("")}${end < characters.length ? "…" : ""}`;
2231
+ }
2232
+ /** Mirrors `packages/session-query/session-query-sqlite/src/index.ts`; update both together. */
2233
+ function compareSearchCandidates(a, b) {
2234
+ if (a.matchCount !== b.matchCount) return b.matchCount - a.matchCount;
2235
+ if (a.documentLength !== b.documentLength) return a.documentLength - b.documentLength;
2236
+ if (a.time !== b.time) return b.time - a.time;
2237
+ if (a.sessionId !== b.sessionId) return a.sessionId < b.sessionId ? -1 : 1;
2238
+ return b.seq - a.seq;
2239
+ }
2240
+ /**
2241
+ * Current plan projection over the full log (host parallel: latest todo/write
2242
+ * with no later turn/start; a new turn retires the previous plan).
2243
+ */
2244
+ function backscanTodos(log) {
2245
+ for (let i = log.length - 1; i >= 0; i--) {
2246
+ const event = log[i];
2247
+ if (event === void 0) continue;
2248
+ if (event.type === "turn/start") return void 0;
2249
+ if (event.type === "todo/write") return event.data.todos;
2250
+ }
2251
+ }
2252
+ /**
2253
+ * Current goal projection over the full log (host parallel: the GoalService
2254
+ * unit's last-wins fold of goal/change whole values; clear returns null).
2255
+ */
2256
+ function backscanGoal(log) {
2257
+ for (let i = log.length - 1; i >= 0; i--) {
2258
+ const event = log[i];
2259
+ if (event === void 0 || event.type !== "goal/change" || event.data === void 0) continue;
2260
+ const change = event.data;
2261
+ if (change.operation === "clear") return null;
2262
+ return {
2263
+ goal: change.goal,
2264
+ roundsStarted: change.roundsStarted,
2265
+ createdAt: change.createdAt,
2266
+ updatedAt: change.updatedAt
2267
+ };
2268
+ }
2269
+ return null;
2270
+ }
2271
+ /** Inbox pump shared by both stream generators (FrameQueue pattern: ONE abort listener hung
2272
+ * outside the loop — a per-iteration {once:true} listener never fires for non-final rounds and
2273
+ * piles up for the stream's lifetime). breakNow force-ends the stream without the
2274
+ * client's signal (timing hook: simulated connection loss). */
2275
+ var FxInbox = class {
2276
+ inbox = [];
2277
+ wake = null;
2278
+ broken = false;
2279
+ push(value) {
2280
+ this.inbox.push(value);
2281
+ this.wake?.();
2282
+ }
2283
+ breakNow() {
2284
+ this.broken = true;
2285
+ this.wake?.();
2286
+ }
2287
+ /** Read through a method: breakNow()/abort flip state across yields, so narrowing from the loop condition must not stick. */
2288
+ isLive(signal) {
2289
+ return !signal.aborted && !this.broken;
2290
+ }
2291
+ async *drain(signal) {
2292
+ const onAbort = () => this.wake?.();
2293
+ signal.addEventListener("abort", onAbort);
2294
+ try {
2295
+ while (this.isLive(signal)) {
2296
+ while (this.inbox.length > 0) yield this.inbox.shift();
2297
+ if (!this.isLive(signal)) break;
2298
+ await new Promise((resolve) => {
2299
+ this.wake = resolve;
2300
+ });
2301
+ this.wake = null;
2302
+ }
2303
+ } finally {
2304
+ signal.removeEventListener("abort", onAbort);
2305
+ }
2306
+ }
2307
+ };
2308
+ /** Build the fixture's Remote RPC face over one state graph. */
2309
+ function createFixtureWorld(options) {
2310
+ const sessions = options.empty ? [] : [
2311
+ {
2312
+ sessionId: sid("fx-alpha"),
2313
+ updatedAt: Date.now(),
2314
+ running: true,
2315
+ blank: false,
2316
+ cwd: "/tmp/fixture"
2317
+ },
2318
+ {
2319
+ sessionId: sid("fx-beta"),
2320
+ updatedAt: Date.now() - 6e4,
2321
+ running: false,
2322
+ blank: false,
2323
+ parentSessionId: sid("fx-alpha"),
2324
+ cwd: "/tmp/fixture"
2325
+ },
2326
+ {
2327
+ sessionId: sid("fx-gamma"),
2328
+ updatedAt: Date.now() - 12e4,
2329
+ running: false,
2330
+ blank: false,
2331
+ cwd: "/tmp/fixture"
2332
+ }
2333
+ ];
2334
+ const logs = new Map([[sid("fx-alpha"), buildAlphaLog()]]);
2335
+ const modelSelections = new Map(sessions.map((session) => [session.sessionId, {
2336
+ provider: "deepseek-official",
2337
+ model: "deepseek-v4-flash"
2338
+ }]));
2339
+ const attachments = new Map([[String(FIXTURE_IMAGE_REF.attachmentId), {
2340
+ attachment: FIXTURE_IMAGE_REF,
2341
+ data: FIXTURE_IMAGE_DATA
2342
+ }]]);
2343
+ /** Credential store double: set/unset flip the describe badge, values never read back. */
2344
+ const fixtureCredentials = new Map([["DEEPSEEK_API_KEY", true]]);
2345
+ /** Canonical fixture implementation of the generated Settings Remote contract. */
2346
+ const settingsRemotes = {
2347
+ describe() {
2348
+ return {
2349
+ ok: true,
2350
+ value: {
2351
+ writable: true,
2352
+ hasDocument: true,
2353
+ namespaces: [{
2354
+ ns: "llm-deepseek",
2355
+ schema: {},
2356
+ value: { apiKeyEnv: "DEEPSEEK_API_KEY" },
2357
+ applies: "live",
2358
+ secrets: [{
2359
+ path: ["apiKey"],
2360
+ set: false
2361
+ }],
2362
+ revision: 0
2363
+ }]
2364
+ }
2365
+ };
2366
+ },
2367
+ update(ns) {
2368
+ return {
2369
+ ok: false,
2370
+ error: {
2371
+ code: "settings/rejected",
2372
+ message: "fixture: the minimal readiness settings descriptor is read-only",
2373
+ details: { ns }
2374
+ }
2375
+ };
2376
+ },
2377
+ replace(ns) {
2378
+ return {
2379
+ ok: false,
2380
+ error: {
2381
+ code: "settings/rejected",
2382
+ message: "fixture: the minimal readiness settings descriptor is read-only",
2383
+ details: { ns }
2384
+ }
2385
+ };
2386
+ },
2387
+ mutate(ns) {
2388
+ return {
2389
+ ok: false,
2390
+ error: {
2391
+ code: "settings/rejected",
2392
+ message: "fixture: no settings namespaces are registered",
2393
+ details: { ns }
2394
+ }
2395
+ };
2396
+ },
2397
+ openSettingsDocument() {
2398
+ return {
2399
+ ok: true,
2400
+ value: { opened: true }
2401
+ };
2402
+ },
2403
+ openAgentPresetDirectory(agentPreset) {
2404
+ const existing = fixturePresets.get(agentPreset);
2405
+ if (existing === void 0 || existing.trust === "system") return {
2406
+ ok: false,
2407
+ error: {
2408
+ code: "agent-preset/read-only",
2409
+ message: `agent preset "${agentPreset}" ships with the deployment`,
2410
+ details: {
2411
+ agentPreset,
2412
+ reason: "it ships with the deployment"
2413
+ }
2414
+ }
2415
+ };
2416
+ return {
2417
+ ok: true,
2418
+ value: { opened: true }
2419
+ };
2420
+ }
2421
+ };
2422
+ const credentialRemotes = {
2423
+ describe(refs) {
2424
+ return {
2425
+ ok: true,
2426
+ value: Object.fromEntries(refs.map((ref) => [ref, {
2427
+ configured: fixtureCredentials.has(ref),
2428
+ ...fixtureCredentials.has(ref) ? { source: "file" } : {},
2429
+ writable: true
2430
+ }]))
2431
+ };
2432
+ },
2433
+ set(ref) {
2434
+ fixtureCredentials.set(ref, true);
2435
+ return {
2436
+ ok: true,
2437
+ value: void 0
2438
+ };
2439
+ },
2440
+ unset(ref) {
2441
+ fixtureCredentials.delete(ref);
2442
+ return {
2443
+ ok: true,
2444
+ value: void 0
2445
+ };
2446
+ }
2447
+ };
2448
+ /**
2449
+ * Preset compositions the fixture serves. Held as state rather than
2450
+ * constants so the settings editor's save and delete are exercisable: the
2451
+ * roster a GUI journey sees after writing is the text it wrote.
2452
+ */
2453
+ const fixturePresets = new Map([
2454
+ ["standard", {
2455
+ trust: "system",
2456
+ content: "- id: tool-bash\n name: '@deepseek-ai/dsh-tool-bash'\n"
2457
+ }],
2458
+ ["minimal", {
2459
+ trust: "system",
2460
+ content: "- id: tool-web-search\n name: '@deepseek-ai/dsh-tool-web-search'\n"
2461
+ }],
2462
+ ["my-agent", {
2463
+ trust: "user",
2464
+ content: "- id: tool-read\n name: '@deepseek-ai/dsh-tool-read'\n"
2465
+ }]
2466
+ ]);
2467
+ let fixtureDefaultPreset = "standard";
2468
+ const nextTurn = new Map([[sid("fx-alpha"), 75]]);
2469
+ let nextSession = 1;
2470
+ const wid = (raw) => raw;
2471
+ const fixtureEpoch = (/* @__PURE__ */ new Date(Date.now() - 3e5)).toISOString();
2472
+ const FIXTURE_HOME = "/home/fixture";
2473
+ const workspaces = options.empty ? [] : [{
2474
+ workspaceId: wid("fx-ws-fixture"),
2475
+ path: "/tmp/fixture",
2476
+ title: "fixture",
2477
+ sessionIds: [
2478
+ sid("fx-alpha"),
2479
+ sid("fx-beta"),
2480
+ sid("fx-gamma")
2481
+ ],
2482
+ createdAt: fixtureEpoch,
2483
+ updatedAt: fixtureEpoch
2484
+ }, {
2485
+ workspaceId: wid("fx-ws-home"),
2486
+ path: `${FIXTURE_HOME}/Documents/project`,
2487
+ title: "project",
2488
+ sessionIds: [],
2489
+ createdAt: fixtureEpoch,
2490
+ updatedAt: fixtureEpoch
2491
+ }];
2492
+ let nextWorkspace = 1;
2493
+ const archivedSessionIds = [];
2494
+ const workspaceSnapshot = (workspace) => ({
2495
+ ...workspace,
2496
+ sessionIds: [...workspace.sessionIds]
2497
+ });
2498
+ const workspaceBaseline = () => ({
2499
+ type: "baseline",
2500
+ value: {
2501
+ items: workspaces.map(workspaceSnapshot),
2502
+ archivedSessionIds: [...archivedSessionIds]
2503
+ }
2504
+ });
2505
+ const directoryTree = new Map([
2506
+ ["/", ["home"]],
2507
+ ["/home", ["fixture"]],
2508
+ [FIXTURE_HOME, [
2509
+ "Documents",
2510
+ "Downloads",
2511
+ ".config"
2512
+ ]],
2513
+ [`${FIXTURE_HOME}/Documents`, [
2514
+ "project",
2515
+ "deepseek-iOS",
2516
+ "deepseek-android",
2517
+ "deepseek-platform",
2518
+ "deepseek-web",
2519
+ "deepseek-harness",
2520
+ "deepseek-app",
2521
+ "deepseek-landing-blog"
2522
+ ]]
2523
+ ]);
2524
+ const childrenOf = (path) => {
2525
+ const known = directoryTree.get(path);
2526
+ if (known !== void 0) return known;
2527
+ const parent = path.slice(0, path.lastIndexOf("/")) || "/";
2528
+ const name = path.slice(path.lastIndexOf("/") + 1);
2529
+ return directoryTree.get(parent)?.includes(name) === true ? [] : void 0;
2530
+ };
2531
+ const crumbsOf = (path) => {
2532
+ const crumbs = [{
2533
+ name: "/",
2534
+ path: "/",
2535
+ hidden: false
2536
+ }];
2537
+ let acc = "";
2538
+ for (const segment of path.split("/").filter(Boolean)) {
2539
+ acc += `/${segment}`;
2540
+ crumbs.push({
2541
+ name: segment,
2542
+ path: acc,
2543
+ hidden: false
2544
+ });
2545
+ }
2546
+ return crumbs;
2547
+ };
2548
+ /** Resident waterfalls retain their event ids across Remote Event generations. */
2549
+ const pendingApprovalEventId = "fx-interaction-approval";
2550
+ let approvalPending = !options.empty;
2551
+ const pendingQuestionEventId = "fx-interaction-question";
2552
+ let questionPending = !options.empty;
2553
+ const fixtureQuestions = [
2554
+ {
2555
+ id: "harness-profile",
2556
+ header: "偏好",
2557
+ question: "你现在更想招哪类 Agent/Harness 候选人?",
2558
+ options: [
2559
+ {
2560
+ label: "工程落地型 (Recommended)",
2561
+ description: "更看重能直接做 runtime、tool executor、sandbox、trace 和线上问题排查。"
2562
+ },
2563
+ {
2564
+ label: "研究潜力型",
2565
+ description: "更看重 Agent 理解、训练评测思路和长期成长空间。"
2566
+ },
2567
+ {
2568
+ label: "均衡型",
2569
+ description: "同时要求工程能力和 Agent 认知,但可能筛选门槛更高。"
2570
+ }
2571
+ ]
2572
+ },
2573
+ {
2574
+ id: "work-mode",
2575
+ header: "方式",
2576
+ question: "你希望候选人优先展示哪种工作方式?",
2577
+ options: [{
2578
+ label: "先做小型原型 (Recommended)",
2579
+ description: "用可运行结果尽快验证关键假设。"
2580
+ }, {
2581
+ label: "先写完整设计",
2582
+ description: "先收敛边界、协议和风险,再开始实现。"
2583
+ }]
2584
+ },
2585
+ {
2586
+ id: "signals",
2587
+ header: "信号",
2588
+ question: "哪些面试信号最重要?",
2589
+ detail: "按当前招聘目标选择;跳过则视为不设偏好。",
2590
+ multiSelect: true,
2591
+ options: [
2592
+ { label: "系统设计" },
2593
+ { label: "代码质量" },
2594
+ { label: "Agent 产品判断" }
2595
+ ]
2596
+ }
2597
+ ];
2598
+ const controlConns = /* @__PURE__ */ new Set();
2599
+ const followConns = /* @__PURE__ */ new Map();
2600
+ const workspaceConns = /* @__PURE__ */ new Set();
2601
+ const remoteEventConns = /* @__PURE__ */ new Map();
2602
+ const emitControl = (frame) => {
2603
+ for (const conn of controlConns) conn.push(frame);
2604
+ };
2605
+ const emitWorkspace = (frame) => {
2606
+ for (const conn of workspaceConns) conn.push(frame);
2607
+ };
2608
+ const emitRemote = (event, args) => {
2609
+ for (const conn of remoteEventConns.values()) conn.push({
2610
+ type: "emit",
2611
+ event,
2612
+ args
2613
+ });
2614
+ };
2615
+ const emitRemoteFrame = (frame) => {
2616
+ for (const conn of remoteEventConns.values()) conn.push(frame);
2617
+ };
2618
+ const emitFollow = (sessionId, entry) => {
2619
+ for (const conn of followConns.get(sessionId) ?? []) conn.push(entry);
2620
+ };
2621
+ function sessionOk(value) {
2622
+ return Promise.resolve({
2623
+ ok: true,
2624
+ value
2625
+ });
2626
+ }
2627
+ function sessionErr(error) {
2628
+ return Promise.resolve({
2629
+ ok: false,
2630
+ error
2631
+ });
2632
+ }
2633
+ const summaryOf = (id) => sessions.find((s) => s.sessionId === id);
2634
+ const requireRemoteSession = (request) => {
2635
+ if (summaryOf(request.sessionId) !== void 0) return void 0;
2636
+ return sessionErr({
2637
+ code: "session/not-found",
2638
+ message: `no session ${request.sessionId}`,
2639
+ details: { sessionId: request.sessionId }
2640
+ });
2641
+ };
2642
+ const setRunning = (id, running) => {
2643
+ const summary = summaryOf(id);
2644
+ if (summary === void 0 || summary.running === running) return;
2645
+ summary.running = running;
2646
+ emitRemote("api-session/status", [id, running]);
2647
+ };
2648
+ const logOf = (id) => {
2649
+ let log = logs.get(id);
2650
+ if (log === void 0) {
2651
+ log = [];
2652
+ logs.set(id, log);
2653
+ }
2654
+ return log;
2655
+ };
2656
+ const append = (id, e) => {
2657
+ const log = logOf(id);
2658
+ const event = {
2659
+ seq: SessionSeq(log.length),
2660
+ time: Date.now(),
2661
+ ...e
2662
+ };
2663
+ log.push(event);
2664
+ emitFollow(id, {
2665
+ type: "event",
2666
+ event
2667
+ });
2668
+ for (const frame of projectionFramesOf(id, log, event)) emitControl(frame);
2669
+ if (event.type === "user/message" && event.data.source.kind === "user") {
2670
+ const summary = summaryOf(id);
2671
+ if (summary !== void 0) summary.updatedAt = event.time;
2672
+ emitRemote("api-session/activity", [id, event.time]);
2673
+ }
2674
+ };
2675
+ /** Append one durable goal/change (host GoalService parallel). */
2676
+ const appendGoalChange = (id, change) => {
2677
+ const log = logOf(id);
2678
+ append(id, {
2679
+ type: "goal/change",
2680
+ data: change
2681
+ });
2682
+ return backscanGoal(log);
2683
+ };
2684
+ const goalFailure = (message) => ({
2685
+ ok: false,
2686
+ error: {
2687
+ code: "gateway/internal",
2688
+ message,
2689
+ details: {}
2690
+ }
2691
+ });
2692
+ const requireGoalSession = (id) => summaryOf(id) === void 0 ? {
2693
+ ok: false,
2694
+ error: {
2695
+ code: "session/not-found",
2696
+ message: `no session ${id}`,
2697
+ details: { sessionId: id }
2698
+ }
2699
+ } : void 0;
2700
+ /** Canonical fixture implementation of the generated Commands Remote contract. */
2701
+ const commandRemotes = {
2702
+ list(id) {
2703
+ const missing = requireGoalSession(id);
2704
+ if (missing !== void 0) return missing;
2705
+ return {
2706
+ ok: true,
2707
+ value: [
2708
+ {
2709
+ name: "compact",
2710
+ description: "fixture:压缩当前会话上下文"
2711
+ },
2712
+ {
2713
+ name: "echo",
2714
+ description: "fixture:回显参数",
2715
+ input: { hint: "text to echo" }
2716
+ },
2717
+ {
2718
+ name: "goal",
2719
+ description: "set or view the goal for a long-running task",
2720
+ input: {
2721
+ hint: "<objective>",
2722
+ images: true
2723
+ }
2724
+ },
2725
+ {
2726
+ name: "permission",
2727
+ description: "Switch the permission preset (sandbox mode + approval policy)",
2728
+ input: { hint: "<preset>" }
2729
+ },
2730
+ {
2731
+ name: "plan",
2732
+ description: "Enter or leave plan mode",
2733
+ input: {
2734
+ hint: "[off|message]",
2735
+ images: true
2736
+ }
2737
+ }
2738
+ ]
2739
+ };
2740
+ },
2741
+ execute(id, line, images = []) {
2742
+ const missing = requireGoalSession(id);
2743
+ if (missing !== void 0) return missing;
2744
+ const match = /^\/(\S+)((?:\s.*)?)$/.exec(line.trim());
2745
+ const name = match?.[1];
2746
+ const args = match?.[2] ?? "";
2747
+ if (images.length > 0 && name !== void 0 && [
2748
+ "permission",
2749
+ "goal",
2750
+ "compact",
2751
+ "echo",
2752
+ "plan"
2753
+ ].includes(name)) {
2754
+ const rejection = name !== "goal" && name !== "plan" ? `/${name} does not accept image attachments` : name === "goal" && args.trim() === "" ? "Image attachments only accompany a goal objective: /goal <objective> or /goal edit <objective>." : name === "plan" && args.trim() === "off" ? "Image attachments cannot accompany /plan off." : void 0;
2755
+ if (rejection !== void 0) {
2756
+ const commandId = `fx-cmd-${logOf(id).length}`;
2757
+ append(id, {
2758
+ type: "command/run",
2759
+ data: {
2760
+ commandId,
2761
+ name,
2762
+ args,
2763
+ source: { kind: "user" }
2764
+ }
2765
+ });
2766
+ const result = {
2767
+ kind: "error",
2768
+ text: rejection
2769
+ };
2770
+ append(id, {
2771
+ type: "command/done",
2772
+ data: {
2773
+ commandId,
2774
+ ...result
2775
+ }
2776
+ });
2777
+ return {
2778
+ ok: true,
2779
+ value: {
2780
+ commandId,
2781
+ result
2782
+ }
2783
+ };
2784
+ }
2785
+ }
2786
+ if (name === "permission") {
2787
+ const preset = args.trim();
2788
+ const commandId = `fx-cmd-${logOf(id).length}`;
2789
+ append(id, {
2790
+ type: "command/run",
2791
+ data: {
2792
+ commandId,
2793
+ name,
2794
+ args,
2795
+ source: { kind: "user" }
2796
+ }
2797
+ });
2798
+ const spec = PERMISSION_PRESETS[preset];
2799
+ let result;
2800
+ if (preset === "") result = {
2801
+ kind: "success",
2802
+ text: `current preset ${permissionSelectOf(logOf(id)).currentValue} (available: ${Object.keys(PERMISSION_PRESETS).join(", ")})`
2803
+ };
2804
+ else if (spec === void 0) result = {
2805
+ kind: "error",
2806
+ text: `unknown preset "${preset}" (available: ${Object.keys(PERMISSION_PRESETS).join(", ")})`
2807
+ };
2808
+ else {
2809
+ if (permissionSelectOf(logOf(id)).currentValue !== preset) append(id, {
2810
+ type: "permission/preset",
2811
+ data: { preset }
2812
+ });
2813
+ append(id, {
2814
+ type: "sandbox/mode",
2815
+ data: { mode: spec.sandbox }
2816
+ });
2817
+ append(id, {
2818
+ type: "approval/policy",
2819
+ data: { policy: spec.approval }
2820
+ });
2821
+ result = {
2822
+ kind: "success",
2823
+ text: `preset ${preset}`
2824
+ };
2825
+ }
2826
+ append(id, {
2827
+ type: "command/done",
2828
+ data: {
2829
+ commandId,
2830
+ ...result
2831
+ }
2832
+ });
2833
+ return {
2834
+ ok: true,
2835
+ value: {
2836
+ commandId,
2837
+ result
2838
+ }
2839
+ };
2840
+ }
2841
+ if (name === "goal") {
2842
+ const commandId = `fx-cmd-${logOf(id).length}`;
2843
+ append(id, {
2844
+ type: "command/run",
2845
+ data: {
2846
+ commandId,
2847
+ name,
2848
+ args,
2849
+ source: { kind: "user" }
2850
+ }
2851
+ });
2852
+ const objective = args.trim();
2853
+ const current = backscanGoal(logOf(id));
2854
+ let text;
2855
+ if (objective === "") text = current === null ? "No goal is set. Usage: /goal <objective>" : `Current goal: ${current.goal.objective}`;
2856
+ else if (current !== null && current.goal.phase !== "complete") text = `A goal already exists (${current.goal.objective}). Clear it first.`;
2857
+ else text = `Goal created: ${appendGoalChange(id, {
2858
+ kind: "goal/change",
2859
+ version: 1,
2860
+ operation: "create",
2861
+ goal: {
2862
+ id: `fx-goal-${logOf(id).length}`,
2863
+ revision: 1,
2864
+ objective,
2865
+ phase: "active",
2866
+ maxGoalRounds: 256
2867
+ },
2868
+ roundsStarted: 0,
2869
+ createdAt: Date.now(),
2870
+ updatedAt: Date.now()
2871
+ }).goal.objective}`;
2872
+ const result = {
2873
+ kind: "success",
2874
+ text
2875
+ };
2876
+ append(id, {
2877
+ type: "command/done",
2878
+ data: {
2879
+ commandId,
2880
+ ...result
2881
+ }
2882
+ });
2883
+ return {
2884
+ ok: true,
2885
+ value: {
2886
+ commandId,
2887
+ result
2888
+ }
2889
+ };
2890
+ }
2891
+ const running = summaryOf(id)?.running === true;
2892
+ const outcomes = {
2893
+ compact: "fixture:已压缩(假动作)",
2894
+ echo: args.trim(),
2895
+ plan: args.trim() === "off" ? running ? "Leaving plan mode (applies from the next step)." : "Plan mode off." : running ? "Entering plan mode (applies from the next step). Use /plan off to leave." : "Plan mode on. Use /plan off to leave."
2896
+ };
2897
+ const text = name === void 0 ? void 0 : outcomes[name];
2898
+ if (name === void 0 || text === void 0) return {
2899
+ ok: true,
2900
+ value: void 0
2901
+ };
2902
+ const commandId = `fx-cmd-${logOf(id).length}`;
2903
+ append(id, {
2904
+ type: "command/run",
2905
+ data: {
2906
+ commandId,
2907
+ name,
2908
+ args,
2909
+ source: { kind: "user" }
2910
+ }
2911
+ });
2912
+ if (name === "plan" && !running) {
2913
+ const plan = foldPlan(logOf(id));
2914
+ if (plan.wanted !== null && plan.wanted !== plan.active) append(id, {
2915
+ type: "plan/mode",
2916
+ data: { active: plan.wanted }
2917
+ });
2918
+ }
2919
+ const result = {
2920
+ kind: "success",
2921
+ ...text === "" ? {} : { text }
2922
+ };
2923
+ append(id, {
2924
+ type: "command/done",
2925
+ data: {
2926
+ commandId,
2927
+ ...result
2928
+ }
2929
+ });
2930
+ return {
2931
+ ok: true,
2932
+ value: {
2933
+ commandId,
2934
+ result
2935
+ }
2936
+ };
2937
+ }
2938
+ };
2939
+ const goalView = (projection) => ({
2940
+ ...projection.goal,
2941
+ roundsStarted: projection.roundsStarted,
2942
+ createdAt: projection.createdAt,
2943
+ updatedAt: projection.updatedAt,
2944
+ activation: projection.goal.phase === "active" ? "armed" : "disarmed"
2945
+ });
2946
+ /** Canonical fixture implementation of the generated Goal Remote contract. */
2947
+ /** Canonical fixture implementation of the generated reference-discovery Remote contracts. */
2948
+ const referenceRemotes = {
2949
+ files(id, query) {
2950
+ const missing = requireGoalSession(id);
2951
+ if (missing !== void 0) return missing;
2952
+ const needle = query.toLocaleLowerCase();
2953
+ return {
2954
+ ok: true,
2955
+ value: [
2956
+ {
2957
+ path: "notes",
2958
+ kind: "directory"
2959
+ },
2960
+ {
2961
+ path: "README.md",
2962
+ kind: "file"
2963
+ },
2964
+ {
2965
+ path: "notes/demo.txt",
2966
+ kind: "file"
2967
+ }
2968
+ ].filter((item) => item.path.toLocaleLowerCase().includes(needle))
2969
+ };
2970
+ },
2971
+ sessions(id, query) {
2972
+ const missing = requireGoalSession(id);
2973
+ if (missing !== void 0) return missing;
2974
+ const needle = query.toLocaleLowerCase();
2975
+ return {
2976
+ ok: true,
2977
+ value: sessions.filter((item) => item.sessionId !== id).filter((item) => String(item.sessionId).toLocaleLowerCase().includes(needle) || item.cwd?.toLocaleLowerCase().includes(needle) === true).map((item) => {
2978
+ const label = item.sessionId === sid("fx-beta") ? "Fixture child session" : String(item.sessionId);
2979
+ const encoded = btoa(JSON.stringify(item.sessionId)).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
2980
+ return {
2981
+ sessionId: item.sessionId,
2982
+ label,
2983
+ ...item.cwd === void 0 ? {} : { cwd: item.cwd },
2984
+ createdAt: item.updatedAt,
2985
+ mention: `@[${label}](dsh-session:${encoded})`
2986
+ };
2987
+ })
2988
+ };
2989
+ }
2990
+ };
2991
+ /**
2992
+ * Canonical fixture implementation of the generated Directory Picker Remote
2993
+ * contract. The pick is deterministic — the keyless lanes drive the full
2994
+ * pick-then-adopt path without an OS chooser — over the same design-mock
2995
+ * tree the browse primitives serve.
2996
+ */
2997
+ const directoryPickerRemotes = {
2998
+ pick() {
2999
+ return {
3000
+ ok: true,
3001
+ value: `${FIXTURE_HOME}/Documents/project`
3002
+ };
3003
+ },
3004
+ list(path) {
3005
+ const target = path ?? FIXTURE_HOME;
3006
+ const children = childrenOf(target);
3007
+ if (children === void 0) return {
3008
+ ok: false,
3009
+ error: {
3010
+ code: "directory-picker/unreadable",
3011
+ message: `cannot list ${target}: not in the fixture tree`,
3012
+ details: { path: target }
3013
+ }
3014
+ };
3015
+ return {
3016
+ ok: true,
3017
+ value: {
3018
+ path: target,
3019
+ home: FIXTURE_HOME,
3020
+ crumbs: crumbsOf(target),
3021
+ entries: [...children].sort((a, b) => a.localeCompare(b)).map((name) => ({
3022
+ name,
3023
+ path: target === "/" ? `/${name}` : `${target}/${name}`,
3024
+ hidden: name.startsWith(".")
3025
+ })),
3026
+ truncated: false
3027
+ }
3028
+ };
3029
+ },
3030
+ createDirectory(parent, name) {
3031
+ const children = childrenOf(parent);
3032
+ if (children === void 0) return {
3033
+ ok: false,
3034
+ error: {
3035
+ code: "directory-picker/create-failed",
3036
+ message: `missing parent ${parent}`,
3037
+ details: { path: parent }
3038
+ }
3039
+ };
3040
+ const target = parent === "/" ? `/${name}` : `${parent}/${name}`;
3041
+ if (children.includes(name)) return {
3042
+ ok: false,
3043
+ error: {
3044
+ code: "directory-picker/exists",
3045
+ message: `${target} already exists`,
3046
+ details: { path: target }
3047
+ }
3048
+ };
3049
+ directoryTree.set(parent, [...children, name]);
3050
+ directoryTree.set(target, []);
3051
+ return {
3052
+ ok: true,
3053
+ value: target
3054
+ };
3055
+ }
3056
+ };
3057
+ const goalRemotes = {
3058
+ create(id, request) {
3059
+ const missing = requireGoalSession(id);
3060
+ if (missing !== void 0) return missing;
3061
+ const current = backscanGoal(logOf(id));
3062
+ if (current !== null && current.goal.phase !== "complete") return goalFailure(`goal "${current.goal.id}" already exists`);
3063
+ const now = Date.now();
3064
+ const projection = appendGoalChange(id, {
3065
+ kind: "goal/change",
3066
+ version: 1,
3067
+ operation: "create",
3068
+ goal: {
3069
+ id: `fx-goal-${logOf(id).length}`,
3070
+ revision: 1,
3071
+ objective: request.objective,
3072
+ phase: "active",
3073
+ maxGoalRounds: request.maxGoalRounds ?? 256
3074
+ },
3075
+ roundsStarted: 0,
3076
+ createdAt: now,
3077
+ updatedAt: now
3078
+ });
3079
+ return {
3080
+ ok: true,
3081
+ value: { ref: {
3082
+ id: projection.goal.id,
3083
+ revision: projection.goal.revision
3084
+ } }
3085
+ };
3086
+ },
3087
+ edit(id, ref, request) {
3088
+ return mutateGoal(id, ref, (current) => ({
3089
+ ...current.goal,
3090
+ revision: current.goal.revision + 1,
3091
+ ...request.objective === void 0 ? {} : { objective: request.objective },
3092
+ ...request.maxGoalRounds === void 0 ? {} : { maxGoalRounds: request.maxGoalRounds }
3093
+ }));
3094
+ },
3095
+ pause(id, ref) {
3096
+ return mutateGoal(id, ref, (current) => current.goal.phase === "active" ? {
3097
+ ...current.goal,
3098
+ revision: current.goal.revision + 1,
3099
+ phase: "paused"
3100
+ } : void 0);
3101
+ },
3102
+ resume(id, ref) {
3103
+ return mutateGoal(id, ref, (current) => current.goal.phase === "paused" || current.goal.phase === "blocked" || current.goal.phase === "active" ? {
3104
+ ...current.goal,
3105
+ revision: current.goal.revision + 1,
3106
+ phase: "active"
3107
+ } : void 0);
3108
+ },
3109
+ complete(id, ref) {
3110
+ return mutateGoal(id, ref, (current) => current.goal.phase === "complete" ? void 0 : {
3111
+ ...current.goal,
3112
+ revision: current.goal.revision + 1,
3113
+ phase: "complete"
3114
+ });
3115
+ },
3116
+ clear(id, ref) {
3117
+ const resolved = resolveGoal(id, ref);
3118
+ if (!resolved.ok) return resolved;
3119
+ const current = resolved.value;
3120
+ const tombstone = {
3121
+ id: current.goal.id,
3122
+ revision: current.goal.revision + 1
3123
+ };
3124
+ appendGoalChange(id, {
3125
+ kind: "goal/change",
3126
+ version: 1,
3127
+ operation: "clear",
3128
+ cleared: tombstone,
3129
+ clearedAt: Date.now()
3130
+ });
3131
+ return {
3132
+ ok: true,
3133
+ value: tombstone
3134
+ };
3135
+ }
3136
+ };
3137
+ /** Resolve one current goal revision for a canonical Remote mutation. */
3138
+ function resolveGoal(id, ref) {
3139
+ const missing = requireGoalSession(id);
3140
+ if (missing !== void 0) return missing;
3141
+ const current = backscanGoal(logOf(id));
3142
+ if (current === null || current.goal.id !== ref.id || current.goal.revision !== ref.revision) return goalFailure("stale or missing goal revision");
3143
+ return {
3144
+ ok: true,
3145
+ value: current
3146
+ };
3147
+ }
3148
+ /** Shared CAS mutation path behind the canonical Remote verbs. */
3149
+ function mutateGoal(id, ref, next) {
3150
+ const resolved = resolveGoal(id, ref);
3151
+ if (!resolved.ok) return resolved;
3152
+ const current = resolved.value;
3153
+ const goal = next(current);
3154
+ if (goal === void 0) return goalFailure(`invalid goal transition from "${current.goal.phase}"`);
3155
+ return {
3156
+ ok: true,
3157
+ value: goalView(appendGoalChange(id, {
3158
+ kind: "goal/change",
3159
+ version: 1,
3160
+ operation: goal.phase === current.goal.phase ? "edit" : goal.phase === "paused" ? "pause" : goal.phase === "active" ? "resume" : "complete",
3161
+ goal,
3162
+ roundsStarted: current.roundsStarted,
3163
+ createdAt: current.createdAt,
3164
+ updatedAt: Date.now()
3165
+ }))
3166
+ };
3167
+ }
3168
+ /** Canonical fixture implementation of the generated AgentPresets Remote contract. */
3169
+ const presetRemotes = {
3170
+ list() {
3171
+ return {
3172
+ ok: true,
3173
+ value: {
3174
+ presets: [...fixturePresets].map(([id, preset]) => ({
3175
+ id,
3176
+ trust: preset.trust,
3177
+ isDefault: id === fixtureDefaultPreset
3178
+ })),
3179
+ authorable: true
3180
+ }
3181
+ };
3182
+ },
3183
+ select(_id, agentPreset) {
3184
+ fixtureDefaultPreset = agentPreset;
3185
+ return {
3186
+ ok: true,
3187
+ value: agentPreset
3188
+ };
3189
+ },
3190
+ read(agentPreset) {
3191
+ const preset = fixturePresets.get(agentPreset);
3192
+ if (preset === void 0) return {
3193
+ ok: false,
3194
+ error: {
3195
+ code: "agent-preset/not-found",
3196
+ message: `unknown agent preset "${agentPreset}"`,
3197
+ details: {
3198
+ agentPreset,
3199
+ available: [...fixturePresets.keys()]
3200
+ }
3201
+ }
3202
+ };
3203
+ return {
3204
+ ok: true,
3205
+ value: {
3206
+ agentPreset,
3207
+ trust: preset.trust,
3208
+ content: preset.content
3209
+ }
3210
+ };
3211
+ },
3212
+ copy(from, id) {
3213
+ const source = fixturePresets.get(from);
3214
+ if (source === void 0) return {
3215
+ ok: false,
3216
+ error: {
3217
+ code: "agent-preset/not-found",
3218
+ message: `unknown agent preset "${from}"`,
3219
+ details: {
3220
+ agentPreset: from,
3221
+ available: [...fixturePresets.keys()]
3222
+ }
3223
+ }
3224
+ };
3225
+ if (fixturePresets.has(id)) return {
3226
+ ok: false,
3227
+ error: {
3228
+ code: "agent-preset/invalid",
3229
+ message: `agent preset "${id}" already exists`,
3230
+ details: {
3231
+ agentPreset: id,
3232
+ reason: "already exists"
3233
+ }
3234
+ }
3235
+ };
3236
+ fixturePresets.set(id, {
3237
+ trust: "user",
3238
+ content: source.content
3239
+ });
3240
+ return {
3241
+ ok: true,
3242
+ value: void 0
3243
+ };
3244
+ },
3245
+ deletePreset(id) {
3246
+ if (fixturePresets.get(id)?.trust === "system") return {
3247
+ ok: false,
3248
+ error: {
3249
+ code: "agent-preset/read-only",
3250
+ message: `agent preset "${id}" ships with the deployment`,
3251
+ details: {
3252
+ agentPreset: id,
3253
+ reason: "it ships with the deployment"
3254
+ }
3255
+ }
3256
+ };
3257
+ fixturePresets.delete(id);
3258
+ return {
3259
+ ok: true,
3260
+ value: void 0
3261
+ };
3262
+ }
3263
+ };
3264
+ /** At most one in-flight replay per session; cancel clears it. */
3265
+ const replays = /* @__PURE__ */ new Map();
3266
+ /** History transit delay; the page snapshot is taken at request time. */
3267
+ let historyDelayMs = 0;
3268
+ /** One-shot history failure (timing hook: a pre-disconnect history request already doomed when reconnect lands). */
3269
+ let failNextHistory = false;
3270
+ /** Force-enders for currently open stream generators (timing hook: simulated connection loss). */
3271
+ const streamBreakers = /* @__PURE__ */ new Set();
3272
+ /** Retry scenarios opened by timing hooks and completed in a later browser assertion phase. */
3273
+ const retryScenarios = /* @__PURE__ */ new Map();
3274
+ /** The single opt-in browser stress producer; normal fixture journeys never start it. */
3275
+ let activeReasoningChunkStorm = null;
3276
+ globalThis.__fxTiming = {
3277
+ setHistoryDelay(ms) {
3278
+ historyDelayMs = ms;
3279
+ },
3280
+ /** Fail the NEXT history call (after its transit delay) with a transport-level throw. */
3281
+ failNextHistory() {
3282
+ failNextHistory = true;
3283
+ },
3284
+ /** Log append plus follow-stream delivery (the normal live path). */
3285
+ appendUser(id, msg) {
3286
+ append(sid(id), {
3287
+ type: "user/message",
3288
+ surfaceOp: "append",
3289
+ data: userMessage(text(msg))
3290
+ });
3291
+ },
3292
+ /** Append a later durable title revision through the normal raw-event + control-frame path. */
3293
+ appendTitle(id, title) {
3294
+ const messageSeqs = logOf(sid(id)).filter((event) => event.type === "user/message").map((event) => event.seq);
3295
+ append(sid(id), {
3296
+ type: "session/title",
3297
+ data: {
3298
+ title,
3299
+ messageSeqs,
3300
+ source: {
3301
+ kind: "provider",
3302
+ provider: "fixture"
3303
+ }
3304
+ }
3305
+ });
3306
+ },
3307
+ /** Start an externally paced reasoning stream for the opt-in browser stress lane. */
3308
+ startReasoningChunkStorm(id, chunkCount, chunksPerInterval, intervalMs) {
3309
+ if (!Number.isSafeInteger(chunkCount) || chunkCount < 1) throw new Error("fixture: reasoning chunk count must be a positive safe integer");
3310
+ if (!Number.isSafeInteger(chunksPerInterval) || chunksPerInterval < 1) throw new Error("fixture: reasoning chunks per interval must be a positive safe integer");
3311
+ if (!Number.isSafeInteger(intervalMs) || intervalMs < 1) throw new Error("fixture: reasoning interval must be a positive safe integer");
3312
+ if (activeReasoningChunkStorm?.emitting === true) throw new Error("fixture: reasoning chunk storm already running");
3313
+ const sessionId = sid(id);
3314
+ const log = logOf(sessionId);
3315
+ let turn = nextTurn.get(sessionId) ?? 0;
3316
+ for (const event of log) {
3317
+ const candidate = event.data?.turn;
3318
+ if (typeof candidate === "number") turn = Math.max(turn, candidate + 1);
3319
+ }
3320
+ nextTurn.set(sessionId, turn + 1);
3321
+ const marker = `REASONING_STRESS_COMPLETE:${String(turn)}:${String(chunkCount)}`;
3322
+ const state = {
3323
+ sessionId: id,
3324
+ chunkCount,
3325
+ chunksPerInterval,
3326
+ intervalMs,
3327
+ emitted: 0,
3328
+ marker,
3329
+ emitting: true
3330
+ };
3331
+ activeReasoningChunkStorm = state;
3332
+ setRunning(sessionId, true);
3333
+ append(sessionId, {
3334
+ type: "turn/start",
3335
+ data: {
3336
+ turn,
3337
+ trigger: {
3338
+ kind: "message",
3339
+ source: { kind: "user" }
3340
+ }
3341
+ }
3342
+ });
3343
+ append(sessionId, {
3344
+ type: "user/message",
3345
+ surfaceOp: "append",
3346
+ data: userMessage(text(`Reasoning chunk stress: ${String(chunkCount)} chunks.`))
3347
+ });
3348
+ append(sessionId, {
3349
+ type: "step/start",
3350
+ data: {
3351
+ turn,
3352
+ step: 0
3353
+ }
3354
+ });
3355
+ append(sessionId, {
3356
+ type: "assistant/chunk",
3357
+ data: {
3358
+ turn,
3359
+ step: 0,
3360
+ chunk: {
3361
+ type: "block-start",
3362
+ index: 0,
3363
+ blockType: "reasoning"
3364
+ }
3365
+ }
3366
+ });
3367
+ const startedAt = Date.now();
3368
+ const pump = () => {
3369
+ const elapsedIntervals = Math.floor((Date.now() - startedAt) / intervalMs) + 1;
3370
+ const due = Math.max(state.emitted + chunksPerInterval, elapsedIntervals * chunksPerInterval);
3371
+ const end = Math.min(due, chunkCount);
3372
+ for (let index = state.emitted; index < end; index++) {
3373
+ const chunkText = index === chunkCount - 1 ? `\n${marker}` : index % 64 === 63 ? "推理\n" : "推理";
3374
+ append(sessionId, {
3375
+ type: "assistant/chunk",
3376
+ data: {
3377
+ turn,
3378
+ step: 0,
3379
+ chunk: {
3380
+ type: "reasoning-delta",
3381
+ index: 0,
3382
+ text: chunkText
3383
+ }
3384
+ }
3385
+ });
3386
+ }
3387
+ state.emitted = end;
3388
+ if (end < chunkCount) setTimeout(pump, intervalMs);
3389
+ else state.emitting = false;
3390
+ };
3391
+ setTimeout(pump, 0);
3392
+ return marker;
3393
+ },
3394
+ /** Return a copy so browser probes cannot mutate the active producer. */
3395
+ reasoningChunkStormState() {
3396
+ return activeReasoningChunkStorm === null ? null : { ...activeReasoningChunkStorm };
3397
+ },
3398
+ /** Open one failed model step whose partial remains visible until llm/retry arrives. */
3399
+ beginModelRetry(id) {
3400
+ const sessionId = sid(id);
3401
+ const turn = nextTurn.get(sessionId) ?? 0;
3402
+ nextTurn.set(sessionId, turn + 1);
3403
+ retryScenarios.set(sessionId, {
3404
+ turn,
3405
+ stepStarted: true
3406
+ });
3407
+ setRunning(sessionId, true);
3408
+ append(sessionId, {
3409
+ type: "turn/start",
3410
+ data: { turn }
3411
+ });
3412
+ append(sessionId, {
3413
+ type: "user/message",
3414
+ surfaceOp: "append",
3415
+ data: {
3416
+ content: text("请重试这个请求"),
3417
+ source: { kind: "user" }
3418
+ }
3419
+ });
3420
+ append(sessionId, {
3421
+ type: "step/start",
3422
+ data: {
3423
+ turn,
3424
+ step: 1
3425
+ }
3426
+ });
3427
+ append(sessionId, {
3428
+ type: "assistant/chunk",
3429
+ data: {
3430
+ turn,
3431
+ step: 1,
3432
+ chunk: {
3433
+ type: "block-start",
3434
+ index: 0,
3435
+ blockType: "text"
3436
+ }
3437
+ }
3438
+ });
3439
+ append(sessionId, {
3440
+ type: "assistant/chunk",
3441
+ data: {
3442
+ turn,
3443
+ step: 1,
3444
+ chunk: {
3445
+ type: "text-delta",
3446
+ index: 0,
3447
+ text: "应撤回的半截回复"
3448
+ }
3449
+ }
3450
+ });
3451
+ },
3452
+ /** Record one retry decision; the next attempt remains in the same step. */
3453
+ scheduleModelRetry(id, retry = 1, delayMs = 450) {
3454
+ const sessionId = sid(id);
3455
+ const scenario = retryScenarios.get(sessionId);
3456
+ if (scenario === void 0) throw new Error(`fixture: no model retry scenario for ${id}`);
3457
+ if (!scenario.stepStarted) {
3458
+ append(sessionId, {
3459
+ type: "assistant/chunk",
3460
+ data: {
3461
+ turn: scenario.turn,
3462
+ step: 1,
3463
+ chunk: {
3464
+ type: "block-start",
3465
+ index: 0,
3466
+ blockType: "text"
3467
+ }
3468
+ }
3469
+ });
3470
+ append(sessionId, {
3471
+ type: "assistant/chunk",
3472
+ data: {
3473
+ turn: scenario.turn,
3474
+ step: 1,
3475
+ chunk: {
3476
+ type: "text-delta",
3477
+ index: 0,
3478
+ text: `第 ${String(retry)} 次应撤回的回复`
3479
+ }
3480
+ }
3481
+ });
3482
+ scenario.stepStarted = true;
3483
+ }
3484
+ append(sessionId, {
3485
+ type: "llm/retry",
3486
+ data: {
3487
+ turn: scenario.turn,
3488
+ step: 1,
3489
+ provider: "fixture",
3490
+ mode: "normal",
3491
+ policyKey: "fixture-normal",
3492
+ retry,
3493
+ maxRetries: 2,
3494
+ delayMs,
3495
+ failure: {
3496
+ code: "TRANSPORT",
3497
+ message: "连接被重置"
3498
+ }
3499
+ }
3500
+ });
3501
+ scenario.stepStarted = false;
3502
+ },
3503
+ /** Record one retry decision, then cancel its source turn before the retry starts. */
3504
+ cancelModelRetryDuringBackoff(id, delayMs = 450) {
3505
+ const sessionId = sid(id);
3506
+ const scenario = retryScenarios.get(sessionId);
3507
+ if (scenario === void 0) throw new Error(`fixture: no model retry scenario for ${id}`);
3508
+ append(sessionId, {
3509
+ type: "llm/retry",
3510
+ data: {
3511
+ turn: scenario.turn,
3512
+ step: 1,
3513
+ provider: "fixture",
3514
+ mode: "normal",
3515
+ policyKey: "fixture-normal",
3516
+ retry: 1,
3517
+ maxRetries: 2,
3518
+ delayMs,
3519
+ failure: {
3520
+ code: "TRANSPORT",
3521
+ message: "连接被重置"
3522
+ }
3523
+ }
3524
+ });
3525
+ append(sessionId, {
3526
+ type: "step/end",
3527
+ data: {
3528
+ turn: scenario.turn,
3529
+ step: 1
3530
+ }
3531
+ });
3532
+ append(sessionId, {
3533
+ type: "turn/end",
3534
+ data: {
3535
+ turn: scenario.turn,
3536
+ reason: {
3537
+ kind: "aborted",
3538
+ reason: { kind: "user" }
3539
+ }
3540
+ }
3541
+ });
3542
+ retryScenarios.delete(sessionId);
3543
+ setRunning(sessionId, false);
3544
+ },
3545
+ /** Finish the timing-hook retry with a finalized response in the open step. */
3546
+ completeModelRetry(id) {
3547
+ const sessionId = sid(id);
3548
+ const scenario = retryScenarios.get(sessionId);
3549
+ if (scenario === void 0) throw new Error(`fixture: no model retry scenario for ${id}`);
3550
+ retryScenarios.delete(sessionId);
3551
+ append(sessionId, {
3552
+ type: "assistant/chunk",
3553
+ data: {
3554
+ turn: scenario.turn,
3555
+ step: 1,
3556
+ chunk: {
3557
+ type: "block-start",
3558
+ index: 0,
3559
+ blockType: "text"
3560
+ }
3561
+ }
3562
+ });
3563
+ append(sessionId, {
3564
+ type: "assistant/message",
3565
+ surfaceOp: "append",
3566
+ data: {
3567
+ turn: scenario.turn,
3568
+ step: 1,
3569
+ message: assistantMessage(text("重试后的完整回复"))
3570
+ }
3571
+ });
3572
+ append(sessionId, {
3573
+ type: "step/end",
3574
+ data: {
3575
+ turn: scenario.turn,
3576
+ step: 1
3577
+ }
3578
+ });
3579
+ append(sessionId, {
3580
+ type: "turn/end",
3581
+ data: {
3582
+ turn: scenario.turn,
3583
+ reason: { kind: "completed" }
3584
+ }
3585
+ });
3586
+ setRunning(sessionId, false);
3587
+ },
3588
+ /** Log append without follow delivery: a frame lost in transit that page repair must recover. */
3589
+ appendSilent(id, msg) {
3590
+ const log = logOf(sid(id));
3591
+ log.push({
3592
+ type: "user/message",
3593
+ surfaceOp: "append",
3594
+ seq: SessionSeq(log.length),
3595
+ time: Date.now(),
3596
+ data: userMessage(text(msg))
3597
+ });
3598
+ },
3599
+ /** End every open stream generator (client sees both streams close -> reconnect + resync path). */
3600
+ breakStreams() {
3601
+ for (const breakNow of [...streamBreakers]) breakNow();
3602
+ }
3603
+ };
3604
+ /** Prompt replay: chunk typewriter (80ms/frame) -> assistant/message finalize -> turn/end + running flip. */
3605
+ const startReply = (id, turn, replyText) => {
3606
+ const step = 0;
3607
+ append(id, {
3608
+ type: "step/start",
3609
+ data: {
3610
+ turn,
3611
+ step
3612
+ }
3613
+ });
3614
+ append(id, {
3615
+ type: "assistant/chunk",
3616
+ data: {
3617
+ turn,
3618
+ step,
3619
+ chunk: {
3620
+ type: "block-start",
3621
+ index: 0,
3622
+ blockType: "text"
3623
+ }
3624
+ }
3625
+ });
3626
+ /* v8 ignore next -- the ?? arm needs a null match, but every fixture reply is non-empty. */
3627
+ const pieces = replyText.match(/[\s\S]{1,6}/gu) ?? [replyText];
3628
+ let i = 0;
3629
+ const finish = (aborted) => {
3630
+ replays.delete(id);
3631
+ const done = pieces.slice(0, i).join("");
3632
+ append(id, {
3633
+ type: "assistant/chunk",
3634
+ data: {
3635
+ turn,
3636
+ step,
3637
+ chunk: {
3638
+ type: "block-end",
3639
+ index: 0,
3640
+ block: {
3641
+ type: "text",
3642
+ text: done
3643
+ }
3644
+ }
3645
+ }
3646
+ });
3647
+ append(id, {
3648
+ type: "assistant/message",
3649
+ surfaceOp: "append",
3650
+ data: {
3651
+ turn,
3652
+ step,
3653
+ message: assistantMessage(text(aborted ? `${done}(已中断)` : done)),
3654
+ usage: fixtureUsage(turn, step)
3655
+ }
3656
+ });
3657
+ append(id, {
3658
+ type: "step/end",
3659
+ data: {
3660
+ turn,
3661
+ step
3662
+ }
3663
+ });
3664
+ append(id, {
3665
+ type: "turn/end",
3666
+ data: {
3667
+ turn,
3668
+ reason: { kind: aborted ? "cancelled" : "completed" }
3669
+ }
3670
+ });
3671
+ setRunning(id, false);
3672
+ };
3673
+ const tick = () => {
3674
+ const piece = pieces[i];
3675
+ if (piece === void 0) {
3676
+ finish(false);
3677
+ return;
3678
+ }
3679
+ i++;
3680
+ append(id, {
3681
+ type: "assistant/chunk",
3682
+ data: {
3683
+ turn,
3684
+ step,
3685
+ chunk: {
3686
+ type: "text-delta",
3687
+ index: 0,
3688
+ text: piece
3689
+ }
3690
+ }
3691
+ });
3692
+ replays.set(id, {
3693
+ timer: setTimeout(tick, 80),
3694
+ finish
3695
+ });
3696
+ };
3697
+ replays.set(id, {
3698
+ timer: setTimeout(tick, 80),
3699
+ finish
3700
+ });
3701
+ };
3702
+ const sessionApi = {
3703
+ list: (_request) => sessionOk({ items: [...sessions].sort((a, b) => b.updatedAt - a.updatedAt) }),
3704
+ search: (request, signal) => {
3705
+ if (signal.aborted) return sessionErr({
3706
+ code: "gateway/cancelled",
3707
+ message: "fixture session search was aborted",
3708
+ details: {}
3709
+ });
3710
+ const query = searchTokenSpans(request.query).tokens.map((token) => token.value);
3711
+ const matches = sessions.flatMap((summary) => {
3712
+ const log = logs.get(summary.sessionId) ?? [];
3713
+ const current = new Set(foldSurface(log).nodes);
3714
+ const best = log.flatMap((event) => {
3715
+ if (!current.has(event.seq)) return [];
3716
+ const eventText = searchEventText(event);
3717
+ const document = searchTokenSpans(eventText);
3718
+ const match = phraseMatch(document.tokens, query);
3719
+ if (match.count === 0) return [];
3720
+ return [{
3721
+ sessionId: summary.sessionId,
3722
+ seq: event.seq,
3723
+ time: event.time,
3724
+ text: document.text,
3725
+ matchCount: match.count,
3726
+ matchStart: match.start,
3727
+ matchEnd: match.end,
3728
+ documentLength: Array.from(eventText).length
3729
+ }];
3730
+ }).sort(compareSearchCandidates)[0];
3731
+ return best === void 0 ? [] : [best];
3732
+ }).sort(compareSearchCandidates);
3733
+ return sessionOk({
3734
+ items: matches.slice(0, FIXTURE_SESSION_SEARCH_RESULT_LIMIT).map((match) => ({
3735
+ sessionId: match.sessionId,
3736
+ snippet: searchSnippet(match.text, match.matchStart, match.matchEnd)
3737
+ })),
3738
+ hasMore: matches.length > FIXTURE_SESSION_SEARCH_RESULT_LIMIT
3739
+ });
3740
+ },
3741
+ create: async (request) => {
3742
+ const workspace = request.workspaceId === void 0 ? void 0 : workspaces.find((w) => w.workspaceId === request.workspaceId);
3743
+ if (request.workspaceId !== void 0 && workspace === void 0) return sessionErr({
3744
+ code: "workspace/not-found",
3745
+ message: `no workspace ${request.workspaceId}`,
3746
+ details: { workspaceId: request.workspaceId }
3747
+ });
3748
+ const cwd = workspace?.path ?? request.cwd ?? "/tmp/fixture";
3749
+ const requestedId = request.sessionId;
3750
+ const attachWorkspace = (sessionId) => {
3751
+ /* v8 ignore next -- callers enter only when a target Workspace exists. */
3752
+ if (workspace === void 0 || workspace.sessionIds.includes(sessionId)) return;
3753
+ workspace.sessionIds = [sessionId, ...workspace.sessionIds];
3754
+ workspace.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3755
+ emitWorkspace({
3756
+ type: "upsert",
3757
+ workspace: workspaceSnapshot(workspace)
3758
+ });
3759
+ };
3760
+ const attachFailure = (sessionId, workspaceId) => sessionErr({
3761
+ code: "session/workspace-attach-failed",
3762
+ message: `fixture rejected Workspace attachment for ${sessionId}`,
3763
+ details: {
3764
+ sessionId,
3765
+ workspaceId
3766
+ }
3767
+ });
3768
+ if (requestedId !== void 0) {
3769
+ const existing = summaryOf(requestedId);
3770
+ if (existing !== void 0) {
3771
+ if (existing.cwd !== cwd) return sessionErr({
3772
+ code: "session/conflict",
3773
+ message: `session ${requestedId} already uses ${existing.cwd ?? "no cwd"}`,
3774
+ details: {
3775
+ sessionId: requestedId,
3776
+ requestedCwd: cwd,
3777
+ ...existing.cwd === void 0 ? {} : { existingCwd: existing.cwd }
3778
+ }
3779
+ });
3780
+ if (workspace !== void 0 && !workspace.sessionIds.includes(requestedId)) {
3781
+ if (options.failWorkspaceAttach) return attachFailure(requestedId, workspace.workspaceId);
3782
+ attachWorkspace(requestedId);
3783
+ }
3784
+ return sessionOk({ sessionId: requestedId });
3785
+ }
3786
+ }
3787
+ const created = {
3788
+ sessionId: requestedId ?? sid(`fx-${nextSession++}`),
3789
+ updatedAt: Date.now(),
3790
+ running: false,
3791
+ blank: true,
3792
+ cwd
3793
+ };
3794
+ sessions.push(created);
3795
+ modelSelections.set(created.sessionId, {
3796
+ provider: "deepseek-official",
3797
+ model: "deepseek-v4-flash"
3798
+ });
3799
+ const emitSession = () => {
3800
+ emitRemote("api-session/added", [created]);
3801
+ };
3802
+ if (workspace !== void 0 && options.failWorkspaceAttach) {
3803
+ emitSession();
3804
+ return attachFailure(created.sessionId, workspace.workspaceId);
3805
+ }
3806
+ if (workspace !== void 0 && options.createFrameOrder === "workspace-first") {
3807
+ attachWorkspace(created.sessionId);
3808
+ emitSession();
3809
+ } else {
3810
+ emitSession();
3811
+ if (workspace !== void 0) attachWorkspace(created.sessionId);
3812
+ }
3813
+ if (options.dropSessionCreateResponse) throw new Error("fixture: dropped session.create response after publication");
3814
+ return sessionOk({ sessionId: created.sessionId });
3815
+ },
3816
+ rename: (request) => {
3817
+ const missing = requireRemoteSession(request);
3818
+ if (missing !== void 0) return missing;
3819
+ const { sessionId, title } = request;
3820
+ const normalized = title.trim().replace(/\s+/g, " ");
3821
+ if (normalized.length === 0) return sessionErr({
3822
+ code: "session/title-invalid",
3823
+ message: "session title must contain visible characters",
3824
+ details: { sessionId }
3825
+ });
3826
+ append(sessionId, {
3827
+ type: "session/title",
3828
+ data: {
3829
+ title: normalized,
3830
+ messageSeqs: [],
3831
+ source: { kind: "user" }
3832
+ }
3833
+ });
3834
+ return sessionOk({
3835
+ title: normalized,
3836
+ seq: logOf(sessionId).at(-1).seq
3837
+ });
3838
+ },
3839
+ fork: (request) => {
3840
+ const { sessionId, atSeq } = request;
3841
+ const source = summaryOf(sessionId);
3842
+ if (source === void 0) return sessionErr({
3843
+ code: "session/not-found",
3844
+ message: `no session ${sessionId}`,
3845
+ details: { sessionId }
3846
+ });
3847
+ const log = logs.get(sessionId) ?? [];
3848
+ const lastSeq = log.at(-1)?.seq ?? -1;
3849
+ const boundary = (atSeq === void 0 ? void 0 : log.find((e) => e.type === "turn/end" && e.seq >= atSeq)) ?? (atSeq === void 0 || atSeq > lastSeq ? log.findLast((e) => e.type === "turn/end") : void 0);
3850
+ if (boundary === void 0) return sessionErr({
3851
+ code: "session/fork-unavailable",
3852
+ message: atSeq !== void 0 && atSeq <= lastSeq ? `session ${sessionId} has not completed the turn containing event ${String(atSeq)}` : `session ${sessionId} has no completed turn`,
3853
+ details: { sessionId }
3854
+ });
3855
+ let cut = boundary.seq + 1;
3856
+ while (cut < log.length && log[cut]?.type !== "turn/start") cut++;
3857
+ const child = {
3858
+ sessionId: sid(`fx-${nextSession++}`),
3859
+ updatedAt: Date.now(),
3860
+ running: false,
3861
+ blank: false,
3862
+ parentSessionId: sessionId,
3863
+ ...source.cwd === void 0 ? {} : { cwd: source.cwd }
3864
+ };
3865
+ logs.set(child.sessionId, log.slice(0, cut));
3866
+ sessions.push(child);
3867
+ emitRemote("api-session/added", [child]);
3868
+ const workspace = workspaces.find((w) => w.sessionIds.includes(sessionId));
3869
+ if (workspace !== void 0) {
3870
+ workspace.sessionIds = [child.sessionId, ...workspace.sessionIds];
3871
+ workspace.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3872
+ emitWorkspace({
3873
+ type: "upsert",
3874
+ workspace: workspaceSnapshot(workspace)
3875
+ });
3876
+ }
3877
+ return sessionOk({ sessionId: child.sessionId });
3878
+ },
3879
+ history: async (request) => {
3880
+ const log = logs.get(request.sessionId) ?? [];
3881
+ const throughSeq = request.throughSeq ?? log.length - 1;
3882
+ const page = pageOf(log.slice(0, throughSeq + 1), request.beforeSeq, request.maxMessages ?? 50);
3883
+ const doomed = failNextHistory;
3884
+ failNextHistory = false;
3885
+ const delay = historyDelayMs;
3886
+ if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay));
3887
+ if (doomed) throw new Error("fixture: simulated history transport failure");
3888
+ return sessionOk(page);
3889
+ },
3890
+ selectModel: (request) => {
3891
+ const selected = {
3892
+ provider: request.provider,
3893
+ model: request.model,
3894
+ ...request.reasoningEffort === void 0 ? {} : { reasoningEffort: request.reasoningEffort }
3895
+ };
3896
+ append(request.sessionId, {
3897
+ type: "model/selection",
3898
+ data: selected
3899
+ });
3900
+ modelSelections.set(request.sessionId, selected);
3901
+ return sessionOk({ selected });
3902
+ },
3903
+ prompt: (request) => {
3904
+ const { sessionId: id, mode, content } = request;
3905
+ const summary = summaryOf(id);
3906
+ if (summary === void 0) return sessionErr({
3907
+ code: "session/not-found",
3908
+ message: `no session ${id}`,
3909
+ details: { sessionId: id }
3910
+ });
3911
+ if (options.rejectPrompt) {
3912
+ if (content.some((block) => block.type === "image")) return sessionErr({
3913
+ code: "session/attachment-invalid",
3914
+ message: "fixture: image side exceeds the deployment limit",
3915
+ details: { reason: "IMAGE_DIMENSION_TOO_LARGE" }
3916
+ });
3917
+ return sessionErr({
3918
+ code: "session/agent-busy",
3919
+ message: "fixture: prompt rejected before acceptance",
3920
+ details: { reason: "fixture-prompt-rejection" }
3921
+ });
3922
+ }
3923
+ summary.updatedAt = Date.now();
3924
+ summary.blank = false;
3925
+ const userText = content.map((b) => b.type === "text" ? b.text : "").join("");
3926
+ const durable = content.map((block) => {
3927
+ if (block.type === "text") return block;
3928
+ const attachment = {
3929
+ attachmentId: `fixture:${randomUuid()}`,
3930
+ mediaType: block.mediaType,
3931
+ bytes: Math.max(1, Math.floor(block.data.length * 3 / 4) - (block.data.endsWith("==") ? 2 : block.data.endsWith("=") ? 1 : 0)),
3932
+ width: 160,
3933
+ height: 90,
3934
+ ...block.name === void 0 ? {} : { name: block.name }
3935
+ };
3936
+ attachments.set(String(attachment.attachmentId), {
3937
+ attachment,
3938
+ data: block.data
3939
+ });
3940
+ return {
3941
+ type: "image",
3942
+ attachment
3943
+ };
3944
+ });
3945
+ const promptSource = {
3946
+ kind: "user",
3947
+ rpcId: request.requestId
3948
+ };
3949
+ if (mode === "steer" && replays.has(id)) {
3950
+ append(id, {
3951
+ type: "user/message",
3952
+ surfaceOp: "append",
3953
+ data: userMessage(durable, promptSource)
3954
+ });
3955
+ return sessionOk({ accepted: true });
3956
+ }
3957
+ const turn = nextTurn.get(id) ?? 0;
3958
+ nextTurn.set(id, turn + 1);
3959
+ setRunning(id, true);
3960
+ append(id, {
3961
+ type: "turn/start",
3962
+ data: { turn }
3963
+ });
3964
+ const plan = foldPlan(logOf(id));
3965
+ if (plan.wanted !== null && plan.wanted !== plan.active) append(id, {
3966
+ type: "plan/mode",
3967
+ data: { active: plan.wanted }
3968
+ });
3969
+ append(id, {
3970
+ type: "user/message",
3971
+ surfaceOp: "append",
3972
+ data: userMessage(durable, promptSource)
3973
+ });
3974
+ const selection = modelSelections.get(id) ?? {
3975
+ provider: "deepseek",
3976
+ model: "deepseek-v4-flash"
3977
+ };
3978
+ const previousHeader = logOf(id).findLast((event) => event.type === "request/header");
3979
+ if (!sameModelSelection(previousHeader?.type === "request/header" ? {
3980
+ provider: previousHeader.data.header.config.provider,
3981
+ model: previousHeader.data.header.config.model,
3982
+ ...previousHeader.data.header.config.reasoningEffort === void 0 ? {} : { reasoningEffort: previousHeader.data.header.config.reasoningEffort }
3983
+ } : null, selection)) append(id, {
3984
+ type: "request/header",
3985
+ data: {
3986
+ header: { config: selection },
3987
+ reason: previousHeader === void 0 ? "initial" : "change"
3988
+ }
3989
+ });
3990
+ if (lastRequestContext(logOf(id))?.model !== selection.model) append(id, {
3991
+ type: "request/context",
3992
+ data: {
3993
+ provider: selection.provider,
3994
+ model: selection.model,
3995
+ contextWindow: 128e3
3996
+ }
3997
+ });
3998
+ startReply(id, turn, userText === "render markdown" ? MARKDOWN_FIXTURE : userText === "report model" ? (() => {
3999
+ const selection = modelSelections.get(id);
4000
+ return `当前模型:${selection?.provider ?? "unknown"}/${selection?.model ?? "unknown"}` + (selection?.reasoningEffort === void 0 ? "" : ` · 推理等级:${selection.reasoningEffort}`);
4001
+ })() : `回声:${userText}。这是 fixture 的流式回复,用于验证打字机增长与定稿切换。`);
4002
+ return sessionOk({ accepted: true });
4003
+ },
4004
+ attachment: (request) => {
4005
+ const stored = attachments.get(String(request.attachmentId));
4006
+ if (stored === void 0) return sessionErr({
4007
+ code: "session/attachment-invalid",
4008
+ message: "fixture attachment missing",
4009
+ details: { reason: "ATTACHMENT_NOT_FOUND" }
4010
+ });
4011
+ if (!logReferencesAttachment(logs.get(request.sessionId) ?? [], String(request.attachmentId))) return sessionErr({
4012
+ code: "session/attachment-invalid",
4013
+ message: "fixture attachment is not referenced by this session",
4014
+ details: { reason: "ATTACHMENT_NOT_REFERENCED" }
4015
+ });
4016
+ return sessionOk(stored);
4017
+ },
4018
+ updateQueue: (request) => sessionErr({
4019
+ code: "session/queue-item-not-found",
4020
+ message: "fixture has no pending queue item",
4021
+ details: { itemId: request.itemId }
4022
+ }),
4023
+ cancel: (request) => {
4024
+ const replay = replays.get(request.sessionId);
4025
+ if (replay !== void 0) {
4026
+ clearTimeout(replay.timer);
4027
+ replay.finish(true);
4028
+ } else setRunning(request.sessionId, false);
4029
+ return sessionOk({ accepted: true });
4030
+ }
4031
+ };
4032
+ const controlBaseline = () => {
4033
+ const queues = {};
4034
+ const jobs = {};
4035
+ const projections = {};
4036
+ for (const summary of sessions) {
4037
+ queues[summary.sessionId] = [];
4038
+ jobs[summary.sessionId] = [];
4039
+ const log = logs.get(summary.sessionId) ?? [];
4040
+ projections[summary.sessionId] = {
4041
+ asOfSeq: log.length - 1,
4042
+ values: projectionValuesOf(log)
4043
+ };
4044
+ }
4045
+ return {
4046
+ type: "baseline",
4047
+ value: {
4048
+ queues,
4049
+ jobs,
4050
+ approvals: [],
4051
+ questions: [],
4052
+ projections
4053
+ }
4054
+ };
4055
+ };
4056
+ const approvalInvocation = () => ({
4057
+ type: "waterfall",
4058
+ event: "approval/request",
4059
+ eventId: pendingApprovalEventId,
4060
+ agentId: sid("fx-alpha"),
4061
+ request: {
4062
+ toolName: "dangerous_tool",
4063
+ reason: "fixture 常驻审批(可答:批准/拒绝后消失)"
4064
+ }
4065
+ });
4066
+ const questionInvocation = () => ({
4067
+ type: "waterfall",
4068
+ event: "user-questions/request",
4069
+ eventId: pendingQuestionEventId,
4070
+ agentId: sid("fx-alpha"),
4071
+ request: { questions: fixtureQuestions }
4072
+ });
4073
+ async function* openControl(signal) {
4074
+ signal.throwIfAborted();
4075
+ const conn = new FxInbox();
4076
+ controlConns.add(conn);
4077
+ const breakNow = () => {
4078
+ conn.breakNow();
4079
+ };
4080
+ streamBreakers.add(breakNow);
4081
+ try {
4082
+ yield controlBaseline();
4083
+ yield* conn.drain(signal);
4084
+ } finally {
4085
+ streamBreakers.delete(breakNow);
4086
+ controlConns.delete(conn);
4087
+ }
4088
+ }
4089
+ async function* openWorkspace(signal) {
4090
+ signal.throwIfAborted();
4091
+ const conn = new FxInbox();
4092
+ workspaceConns.add(conn);
4093
+ const breakNow = () => {
4094
+ conn.breakNow();
4095
+ };
4096
+ streamBreakers.add(breakNow);
4097
+ try {
4098
+ yield workspaceBaseline();
4099
+ yield* conn.drain(signal);
4100
+ } finally {
4101
+ streamBreakers.delete(breakNow);
4102
+ workspaceConns.delete(conn);
4103
+ }
4104
+ }
4105
+ async function* openRemoteEvents(signal) {
4106
+ signal.throwIfAborted();
4107
+ const clientId = randomUuid();
4108
+ const conn = new FxInbox();
4109
+ remoteEventConns.set(clientId, conn);
4110
+ const timer = setInterval(() => {
4111
+ const gamma = summaryOf(sid("fx-gamma"));
4112
+ /* v8 ignore next -- the fixture never removes fx-gamma. */
4113
+ if (gamma !== void 0) setRunning(gamma.sessionId, !gamma.running);
4114
+ }, 5e3);
4115
+ try {
4116
+ yield {
4117
+ type: "ready",
4118
+ clientId,
4119
+ host: { home: FIXTURE_HOME }
4120
+ };
4121
+ if (approvalPending) yield approvalInvocation();
4122
+ if (questionPending) yield questionInvocation();
4123
+ yield* conn.drain(signal);
4124
+ } finally {
4125
+ clearInterval(timer);
4126
+ remoteEventConns.delete(clientId);
4127
+ }
4128
+ }
4129
+ async function* openFollow(request, signal) {
4130
+ signal.throwIfAborted();
4131
+ const sessionId = request.address.kind === "session" ? request.address.sessionId : request.address.childSessionId;
4132
+ if (summaryOf(sessionId) === void 0) throw new Error(`fixture: no session ${sessionId}`);
4133
+ const conn = new FxInbox();
4134
+ let conns = followConns.get(sessionId);
4135
+ if (conns === void 0) {
4136
+ conns = /* @__PURE__ */ new Set();
4137
+ followConns.set(sessionId, conns);
4138
+ }
4139
+ conns.add(conn);
4140
+ const breakNow = () => {
4141
+ conn.breakNow();
4142
+ };
4143
+ streamBreakers.add(breakNow);
4144
+ const snapshot = [...logOf(sessionId)];
4145
+ const cursor = snapshot.at(-1)?.seq ?? -1;
4146
+ const summary = summaryOf(sessionId);
4147
+ /* v8 ignore next -- existence was checked before the stream registered. */
4148
+ if (summary === void 0) throw new Error(`fixture: no session ${sessionId}`);
4149
+ const initial = pageOf(snapshot, void 0, request.maxMessages ?? 50);
4150
+ let nextSeq = cursor + 1;
4151
+ try {
4152
+ yield {
4153
+ type: "snapshot",
4154
+ header: {
4155
+ version: 0,
4156
+ id: sessionId,
4157
+ createdAt: summary.updatedAt,
4158
+ ...summary.cwd === void 0 ? {} : { cwd: summary.cwd },
4159
+ ...summary.parentSessionId === void 0 ? {} : { parentSession: summary.parentSessionId },
4160
+ ...summary.origin === void 0 ? {} : { origin: summary.origin },
4161
+ ...summary.agentPreset === void 0 ? {} : { agentPreset: summary.agentPreset }
4162
+ },
4163
+ cursor,
4164
+ records: initial.records,
4165
+ hasMore: initial.hasMore,
4166
+ projections: {
4167
+ asOfSeq: cursor,
4168
+ values: projectionValuesOf(snapshot)
4169
+ }
4170
+ };
4171
+ for await (const frame of conn.drain(signal)) {
4172
+ if (frame.event.seq < nextSeq) continue;
4173
+ if (frame.event.seq !== nextSeq) throw new Error(`fixture: session event stream skipped seq ${String(nextSeq)}`);
4174
+ nextSeq++;
4175
+ yield frame;
4176
+ }
4177
+ } finally {
4178
+ streamBreakers.delete(breakNow);
4179
+ conns.delete(conn);
4180
+ if (conns.size === 0) followConns.delete(sessionId);
4181
+ }
4182
+ }
4183
+ const answerRemoteEvent = (result) => {
4184
+ if (!remoteEventConns.has(result.clientId)) return {
4185
+ ok: false,
4186
+ error: {
4187
+ code: "gateway/invocation-unavailable",
4188
+ message: "fixture Remote event result identifies no active event stream",
4189
+ details: {}
4190
+ }
4191
+ };
4192
+ if (result.eventId === pendingApprovalEventId) {
4193
+ if (!approvalPending) return {
4194
+ ok: true,
4195
+ value: void 0
4196
+ };
4197
+ approvalPending = false;
4198
+ } else if (result.eventId === pendingQuestionEventId) {
4199
+ if (!questionPending) return {
4200
+ ok: true,
4201
+ value: void 0
4202
+ };
4203
+ questionPending = false;
4204
+ } else return {
4205
+ ok: true,
4206
+ value: void 0
4207
+ };
4208
+ emitRemoteFrame({
4209
+ type: "cancel",
4210
+ eventId: result.eventId
4211
+ });
4212
+ return {
4213
+ ok: true,
4214
+ value: void 0
4215
+ };
4216
+ };
4217
+ const workspaceApi = {
4218
+ create: (request) => {
4219
+ const existing = workspaces.find((workspace) => workspace.path === request.path);
4220
+ if (existing !== void 0) return sessionOk({
4221
+ workspace: workspaceSnapshot(existing),
4222
+ created: false
4223
+ });
4224
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4225
+ const created = {
4226
+ workspaceId: wid(`fx-ws-${nextWorkspace++}`),
4227
+ path: request.path,
4228
+ title: request.path.split("/").filter(Boolean).at(-1) ?? request.path,
4229
+ sessionIds: [],
4230
+ createdAt: now,
4231
+ updatedAt: now
4232
+ };
4233
+ workspaces.unshift(created);
4234
+ const workspace = workspaceSnapshot(created);
4235
+ emitWorkspace({
4236
+ type: "upsert",
4237
+ workspace
4238
+ });
4239
+ return sessionOk({
4240
+ workspace,
4241
+ created: true
4242
+ });
4243
+ },
4244
+ rename: (request) => {
4245
+ const workspace = workspaces.find((candidate) => candidate.workspaceId === request.workspaceId);
4246
+ if (workspace === void 0) return sessionErr({
4247
+ code: "workspace/not-found",
4248
+ message: `no workspace ${request.workspaceId}`,
4249
+ details: { workspaceId: request.workspaceId }
4250
+ });
4251
+ const title = request.title.trim();
4252
+ if (title === "") return sessionErr({
4253
+ code: "gateway/bad-request",
4254
+ message: "Workspace rename requires a non-blank title",
4255
+ details: {}
4256
+ });
4257
+ if (title !== workspace.title) {
4258
+ if (workspaces.some((candidate) => candidate.workspaceId !== request.workspaceId && candidate.title === title)) return sessionErr({
4259
+ code: "workspace/name-conflict",
4260
+ message: `workspace name '${title}' is already in use`,
4261
+ details: { name: title }
4262
+ });
4263
+ workspace.title = title;
4264
+ workspace.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
4265
+ emitWorkspace({
4266
+ type: "upsert",
4267
+ workspace: workspaceSnapshot(workspace)
4268
+ });
4269
+ }
4270
+ return sessionOk({ workspace: workspaceSnapshot(workspace) });
4271
+ },
4272
+ delete: (request) => {
4273
+ const index = workspaces.findIndex((workspace) => workspace.workspaceId === request.workspaceId);
4274
+ if (index === -1) return sessionErr({
4275
+ code: "workspace/not-found",
4276
+ message: `no workspace ${request.workspaceId}`,
4277
+ details: { workspaceId: request.workspaceId }
4278
+ });
4279
+ workspaces.splice(index, 1);
4280
+ emitWorkspace({
4281
+ type: "remove",
4282
+ workspaceId: request.workspaceId
4283
+ });
4284
+ return sessionOk({ deleted: true });
4285
+ },
4286
+ insertBefore: (request) => {
4287
+ const source = workspaces.findIndex((workspace) => workspace.workspaceId === request.workspaceId);
4288
+ const anchor = request.beforeWorkspaceId === void 0 ? workspaces.length : workspaces.findIndex((workspace) => workspace.workspaceId === request.beforeWorkspaceId);
4289
+ const missing = source === -1 ? request.workspaceId : anchor === -1 ? request.beforeWorkspaceId : void 0;
4290
+ if (missing !== void 0) return sessionErr({
4291
+ code: "workspace/not-found",
4292
+ message: `no workspace ${missing}`,
4293
+ details: { workspaceId: missing }
4294
+ });
4295
+ if (request.beforeWorkspaceId !== request.workspaceId) {
4296
+ const previousOrder = workspaces.map((workspace) => workspace.workspaceId);
4297
+ const [workspace] = workspaces.splice(source, 1);
4298
+ /* v8 ignore next -- source was resolved from the same array immediately above. */
4299
+ if (workspace === void 0) throw new Error(`fixture lost workspace ${request.workspaceId}`);
4300
+ const at = request.beforeWorkspaceId === void 0 ? workspaces.length : workspaces.findIndex((candidate) => candidate.workspaceId === request.beforeWorkspaceId);
4301
+ workspaces.splice(at, 0, workspace);
4302
+ if (workspaces.some((candidate, index) => candidate.workspaceId !== previousOrder[index])) emitWorkspace({
4303
+ type: "order",
4304
+ workspaceIds: workspaces.map((candidate) => candidate.workspaceId)
4305
+ });
4306
+ }
4307
+ return sessionOk({ workspaceIds: workspaces.map((candidate) => candidate.workspaceId) });
4308
+ },
4309
+ insertSessionBefore: (request) => {
4310
+ const workspace = workspaces.find((candidate) => candidate.workspaceId === request.workspaceId);
4311
+ if (workspace === void 0) return sessionErr({
4312
+ code: "workspace/not-found",
4313
+ message: `no workspace ${request.workspaceId}`,
4314
+ details: { workspaceId: request.workspaceId }
4315
+ });
4316
+ if (!workspace.sessionIds.includes(request.sessionId) || request.beforeSessionId !== void 0 && !workspace.sessionIds.includes(request.beforeSessionId)) return sessionErr({
4317
+ code: "workspace/move-invalid",
4318
+ message: `session or anchor is not accounted by workspace ${request.workspaceId}`,
4319
+ details: {
4320
+ workspaceId: request.workspaceId,
4321
+ sessionId: request.sessionId,
4322
+ ...request.beforeSessionId === void 0 ? {} : { beforeSessionId: request.beforeSessionId }
4323
+ }
4324
+ });
4325
+ const without = workspace.sessionIds.filter((id) => id !== request.sessionId);
4326
+ const at = request.beforeSessionId === void 0 ? without.length : without.indexOf(request.beforeSessionId);
4327
+ const sessionIds = [
4328
+ ...without.slice(0, at),
4329
+ request.sessionId,
4330
+ ...without.slice(at)
4331
+ ];
4332
+ if (!sessionIds.every((id, index) => id === workspace.sessionIds[index])) {
4333
+ workspace.sessionIds = sessionIds;
4334
+ workspace.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
4335
+ emitWorkspace({
4336
+ type: "upsert",
4337
+ workspace: workspaceSnapshot(workspace)
4338
+ });
4339
+ }
4340
+ return sessionOk({ workspace: workspaceSnapshot(workspace) });
4341
+ },
4342
+ archiveSession: (request) => {
4343
+ if (summaryOf(request.sessionId) === void 0) return sessionErr({
4344
+ code: "session/not-found",
4345
+ message: `no session ${request.sessionId}`,
4346
+ details: { sessionId: request.sessionId }
4347
+ });
4348
+ if (!archivedSessionIds.includes(request.sessionId)) {
4349
+ archivedSessionIds.push(request.sessionId);
4350
+ emitWorkspace({
4351
+ type: "archived",
4352
+ archivedSessionIds: [...archivedSessionIds]
4353
+ });
4354
+ }
4355
+ return sessionOk({ archivedSessionIds: [...archivedSessionIds] });
4356
+ }
4357
+ };
4358
+ return { rpc: {
4359
+ call(channel, endpoint, payload, signal) {
4360
+ if (channel !== "/api") return Promise.reject(/* @__PURE__ */ new Error(`fixture connection RPC channel ${JSON.stringify(channel)} is unavailable`));
4361
+ const args = payload.args;
4362
+ const sessionId = args.agentId;
4363
+ const callSignal = signal ?? new AbortController().signal;
4364
+ const request = args.request;
4365
+ switch (endpoint) {
4366
+ case "commands/list": return Promise.resolve(commandRemotes.list(sessionId));
4367
+ case "commands/execute": return Promise.resolve(commandRemotes.execute(sessionId, args.line, args.images ?? []));
4368
+ case "fileReferences/list": return Promise.resolve(referenceRemotes.files(sessionId, args.query ?? ""));
4369
+ case "sessionReferenceResolver/candidates": return Promise.resolve(referenceRemotes.sessions(sessionId, args.query ?? ""));
4370
+ case "directoryPicker/pick": return Promise.resolve(directoryPickerRemotes.pick());
4371
+ case "directoryPicker/list": return Promise.resolve(directoryPickerRemotes.list(args.path));
4372
+ case "directoryPicker/createDirectory": return Promise.resolve(directoryPickerRemotes.createDirectory(args.path ?? "", args.name ?? ""));
4373
+ case "goals/create": return Promise.resolve(goalRemotes.create(sessionId, {
4374
+ objective: request?.objective,
4375
+ ...request?.maxGoalRounds === void 0 ? {} : { maxGoalRounds: request.maxGoalRounds }
4376
+ }));
4377
+ case "goals/edit": return Promise.resolve(goalRemotes.edit(sessionId, args.ref, request));
4378
+ case "goals/pause": return Promise.resolve(goalRemotes.pause(sessionId, args.ref));
4379
+ case "goals/resume": return Promise.resolve(goalRemotes.resume(sessionId, args.ref));
4380
+ case "goals/complete": return Promise.resolve(goalRemotes.complete(sessionId, args.ref));
4381
+ case "goals/clear": return Promise.resolve(goalRemotes.clear(sessionId, args.ref));
4382
+ case "agentPresets/list": return Promise.resolve(presetRemotes.list());
4383
+ case "agentPresets/select": return Promise.resolve(presetRemotes.select(sessionId, args.agentPreset));
4384
+ case "agentPresets/read": return Promise.resolve(presetRemotes.read(args.agentPreset));
4385
+ case "agentPresets/copy": return Promise.resolve(presetRemotes.copy(args.from, args.id));
4386
+ case "agentPresets/deletePreset": return Promise.resolve(presetRemotes.deletePreset(args.id));
4387
+ case "subagents/list": return Promise.resolve({
4388
+ ok: true,
4389
+ value: {
4390
+ entries: [],
4391
+ parentAvailable: true
4392
+ }
4393
+ });
4394
+ case "subagents/prompt": return Promise.resolve({
4395
+ ok: true,
4396
+ value: { messageId: `fixture-message-${request.childSessionId}` }
4397
+ });
4398
+ case "subagents/interruptByParent": return Promise.resolve({
4399
+ ok: true,
4400
+ value: { accepted: true }
4401
+ });
4402
+ case "credentials/describe": return Promise.resolve(credentialRemotes.describe(args.refs ?? []));
4403
+ case "credentials/set": return Promise.resolve(credentialRemotes.set(args.ref));
4404
+ case "credentials/unset": return Promise.resolve(credentialRemotes.unset(args.ref));
4405
+ case "settings/describe": return Promise.resolve(settingsRemotes.describe());
4406
+ case "settings/canOpenAgentPresetDirectory": return Promise.resolve({
4407
+ ok: true,
4408
+ value: true
4409
+ });
4410
+ case "settings/openSettingsDocument": return Promise.resolve(settingsRemotes.openSettingsDocument());
4411
+ case "settings/openAgentPresetDirectory": return Promise.resolve(settingsRemotes.openAgentPresetDirectory(args.agentPreset));
4412
+ case "skills/list": {
4413
+ const missing = requireRemoteSession(request);
4414
+ if (missing !== void 0) return missing;
4415
+ return sessionOk({ skills: [{
4416
+ name: "fixture-demo",
4417
+ description: "fixture 技能样本",
4418
+ whenToUse: "仅供 UI 目录渲染验收",
4419
+ modelInvocable: true
4420
+ }, {
4421
+ name: "fixture-user-only",
4422
+ description: "fixture 仅用户技能样本",
4423
+ modelInvocable: false
4424
+ }] });
4425
+ }
4426
+ case "session/openWorkspacePath": return sessionOk({ opened: true });
4427
+ case "session/canOpenWorkspacePath": return Promise.resolve({
4428
+ ok: true,
4429
+ value: true
4430
+ });
4431
+ case "session/modelCatalog": return Promise.resolve({
4432
+ ok: true,
4433
+ value: {
4434
+ default: {
4435
+ provider: "deepseek-official",
4436
+ model: "deepseek-v4-flash"
4437
+ },
4438
+ routableProviders: [
4439
+ "deepseek-official",
4440
+ "openai",
4441
+ "acme-gateway"
4442
+ ],
4443
+ groups: fixtureModelGroups(),
4444
+ failures: []
4445
+ }
4446
+ });
4447
+ case "llm/listProviders": return Promise.resolve({
4448
+ ok: true,
4449
+ value: [
4450
+ {
4451
+ id: "deepseek-official",
4452
+ name: "DeepSeek"
4453
+ },
4454
+ {
4455
+ id: "openai",
4456
+ name: "openai"
4457
+ },
4458
+ {
4459
+ id: "acme-gateway",
4460
+ name: "Acme Gateway"
4461
+ }
4462
+ ]
4463
+ });
4464
+ case "llm/listConfigurableProviders": return Promise.resolve({
4465
+ ok: true,
4466
+ value: [
4467
+ {
4468
+ provider: "deepseek-official",
4469
+ displayName: "DeepSeek",
4470
+ settingsNs: "llm-deepseek",
4471
+ settingsPath: []
4472
+ },
4473
+ {
4474
+ provider: "openai",
4475
+ displayName: "openai",
4476
+ settingsNs: "llm-pi-ai",
4477
+ settingsPath: ["providers", "openai"],
4478
+ declared: false
4479
+ },
4480
+ {
4481
+ provider: "anthropic",
4482
+ displayName: "anthropic",
4483
+ settingsNs: "llm-pi-ai",
4484
+ settingsPath: ["providers", "anthropic"],
4485
+ declared: false
4486
+ },
4487
+ {
4488
+ provider: "acme-gateway",
4489
+ displayName: "Acme Gateway",
4490
+ settingsNs: "llm-pi-ai",
4491
+ settingsPath: ["providers", "acme-gateway"],
4492
+ declared: true
4493
+ }
4494
+ ]
4495
+ });
4496
+ case "llm/discoverModels": return Promise.resolve({
4497
+ ok: true,
4498
+ value: fixtureModelGroups().flatMap((group) => group.models.map((model) => ({
4499
+ id: model.id,
4500
+ name: model.name
4501
+ })))
4502
+ });
4503
+ case "settings/update": return Promise.resolve(settingsRemotes.update(args.ns));
4504
+ case "settings/replace": return Promise.resolve(settingsRemotes.replace(args.ns));
4505
+ case "settings/mutate": return Promise.resolve(settingsRemotes.mutate(args.ns));
4506
+ case "session/list": return sessionApi.list(args._request);
4507
+ case "session/search": return sessionApi.search(request, callSignal);
4508
+ case "session/create": return sessionApi.create(request);
4509
+ case "session/selectModel": return sessionApi.selectModel(request);
4510
+ case "session/rename": return sessionApi.rename(request);
4511
+ case "session/fork": return sessionApi.fork(request);
4512
+ case "session/prompt": return sessionApi.prompt(request);
4513
+ case "session/attachment": return sessionApi.attachment(request);
4514
+ case "session/updateQueue": return sessionApi.updateQueue(request);
4515
+ case "session/cancel": return sessionApi.cancel(request);
4516
+ case "session/page": {
4517
+ const page = request;
4518
+ const pageSessionId = page.address.kind === "session" ? page.address.sessionId : page.address.childSessionId;
4519
+ return sessionApi.history({
4520
+ sessionId: pageSessionId,
4521
+ throughSeq: page.throughSeq,
4522
+ ...page.beforeSeq === void 0 ? {} : { beforeSeq: page.beforeSeq },
4523
+ ...page.maxMessages === void 0 ? {} : { maxMessages: page.maxMessages }
4524
+ });
4525
+ }
4526
+ case "$events/result": return Promise.resolve(answerRemoteEvent(args));
4527
+ case "workspace/create": return workspaceApi.create(request);
4528
+ case "workspace/rename": return workspaceApi.rename(request);
4529
+ case "workspace/delete": return workspaceApi.delete(request);
4530
+ case "workspace/insertBefore": return workspaceApi.insertBefore(request);
4531
+ case "workspace/insertSessionBefore": return workspaceApi.insertSessionBefore(request);
4532
+ case "workspace/archiveSession": return workspaceApi.archiveSession(request);
4533
+ default: return Promise.reject(/* @__PURE__ */ new Error(`fixture connection RPC endpoint ${JSON.stringify(endpoint)} is unavailable`));
4534
+ }
4535
+ },
4536
+ open(channel, endpoint, payload, signal) {
4537
+ if (channel !== "/api") throw new Error(`fixture connection RPC channel ${JSON.stringify(channel)} is unavailable`);
4538
+ const args = payload.args;
4539
+ switch (endpoint) {
4540
+ case "$events": return openRemoteEvents(signal);
4541
+ case "session/control": return openControl(signal);
4542
+ case "session/follow": return openFollow(args.request, signal);
4543
+ case "workspace/follow": return openWorkspace(signal);
4544
+ default: throw new Error(`fixture connection stream endpoint ${JSON.stringify(endpoint)} is unavailable`);
4545
+ }
4546
+ }
4547
+ } };
4548
+ }
4549
+ /**
4550
+ * Build the browser fixture transport from the current page's query switches.
4551
+ * @returns an in-memory Connection RPC transport.
4552
+ */
4553
+ function createFixtureConnectionRpc() {
4554
+ return createFixtureWorld(fixtureOptionsFromLocation()).rpc;
4555
+ }
4556
+ /** Browser query mapping; direct unit callers pass FixtureOptions explicitly. */
4557
+ function fixtureOptionsFromLocation() {
4558
+ if (typeof location === "undefined") return {};
4559
+ const query = new URLSearchParams(location.search);
4560
+ return {
4561
+ empty: query.get("fixture") === "empty",
4562
+ rejectPrompt: query.get("fixturePrompt") === "reject",
4563
+ failWorkspaceAttach: query.get("fixtureAttach") === "fail",
4564
+ dropSessionCreateResponse: query.get("fixtureSessionCreate") === "drop-response",
4565
+ createFrameOrder: query.get("fixtureFrames") === "workspace-first" ? "workspace-first" : "session-first"
4566
+ };
4567
+ }
4568
+ //#endregion
4569
+ //#region lib/types/rpc.js
4570
+ /** Generic unary RPC contracts shared by the Host and Client Connection halves. */
4571
+ /**
4572
+ * Brand one validated string as a Connection correlation id.
4573
+ * @param id - validated wire identity.
4574
+ * @returns the same string with the correlation-id brand.
4575
+ */
4576
+ function RpcId(id) {
4577
+ return id;
4578
+ }
4579
+ /**
4580
+ * Convert a rejected transport operation into a generic failure result.
4581
+ * @param error - rejected transport value.
4582
+ * @returns an `internal` failure preserving the available message.
4583
+ */
4584
+ function transportError(error) {
4585
+ return {
4586
+ ok: false,
4587
+ error: {
4588
+ code: "gateway/internal",
4589
+ message: error instanceof Error ? error.message : String(error),
4590
+ details: {}
4591
+ }
4592
+ };
4593
+ }
4594
+ //#endregion
4595
+ //#region lib/types/client/rpc.js
4596
+ /** Browser caller for generic Connection unary RPC channels. */
4597
+ const INTERNAL_BASE = "http://dsh.internal";
4598
+ const CHANNEL_PATTERN = /^\/[A-Za-z0-9._~-]+$/;
4599
+ const ENDPOINT_SEGMENT_PATTERN = /^[A-Za-z0-9_$.-]+$/;
4600
+ /**
4601
+ * Create the browser-backed generic RPC caller.
4602
+ * @param doFetch - transport override; defaults to the page's global fetch.
4603
+ * @param openStream - optional worker-local Gateway stream carrier.
4604
+ * @returns caller that owns request correlation and response-envelope validation.
4605
+ */
4606
+ function createWebConnectionRpc(doFetch, openStream) {
4607
+ const send = doFetch ?? ((input, init) => globalThis.fetch(input, init));
4608
+ return {
4609
+ async call(channel, endpoint, payload, signal) {
4610
+ assertTarget(channel, endpoint);
4611
+ const rpcId = RpcId(randomUuid());
4612
+ const message = {
4613
+ type: "client-request",
4614
+ rpcId,
4615
+ method: endpoint,
4616
+ payload
4617
+ };
4618
+ const response = await send(new URL(`${channel}/${endpoint}`, resolveBase()), {
4619
+ method: "POST",
4620
+ headers: { "content-type": "application/json" },
4621
+ body: JSON.stringify(message),
4622
+ ...signal === void 0 ? {} : { signal }
4623
+ });
4624
+ if (!response.ok) throw new Error(`transport failure for ${channel}/${endpoint}: HTTP ${response.status}`);
4625
+ const full = parseConnectionResponse(await response.json());
4626
+ if (full.rpcId !== rpcId) throw new Error(`rpcId mismatch for ${endpoint}: sent ${rpcId}, got ${full.rpcId}`);
4627
+ return full.result;
4628
+ },
4629
+ ...openStream === void 0 ? {} : { open(channel, endpoint, payload, signal) {
4630
+ assertTarget(channel, endpoint);
4631
+ if (channel !== "/api") throw new Error(`connection: worker-local streams require the /api channel, got ${JSON.stringify(channel)}`);
4632
+ return openStream(endpoint, payload, signal);
4633
+ } }
4634
+ };
4635
+ }
4636
+ function parseConnectionResponse(value) {
4637
+ if (!isRecord(value) || value.type !== "server-response" || typeof value.rpcId !== "string") throw new TypeError("connection: invalid server-response envelope");
4638
+ const result = value.result;
4639
+ if (!isRecord(result)) throw new TypeError("connection: invalid server-response result");
4640
+ if (result.ok === true) return {
4641
+ rpcId: RpcId(value.rpcId),
4642
+ result: {
4643
+ ok: true,
4644
+ value: result.value
4645
+ }
4646
+ };
4647
+ if (result.ok !== false || !isRecord(result.error)) throw new TypeError("connection: invalid server-response result");
4648
+ const error = result.error;
4649
+ if (typeof error.code !== "string" || typeof error.message !== "string" || !isRecord(error.details)) throw new TypeError("connection: invalid server-response failure");
4650
+ return {
4651
+ rpcId: RpcId(value.rpcId),
4652
+ result: {
4653
+ ok: false,
4654
+ error: {
4655
+ code: error.code,
4656
+ message: error.message,
4657
+ details: error.details
4658
+ }
4659
+ }
4660
+ };
4661
+ }
4662
+ function isRecord(value) {
4663
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4664
+ }
4665
+ function resolveBase() {
4666
+ const location = globalThis.location;
4667
+ return location?.origin !== void 0 && location.origin !== "null" ? location.origin : INTERNAL_BASE;
4668
+ }
4669
+ function assertTarget(channel, endpoint) {
4670
+ const segments = endpoint.split("/");
4671
+ if (!CHANNEL_PATTERN.test(channel) || segments.some((segment) => segment === "" || segment === "." || segment === ".." || !ENDPOINT_SEGMENT_PATTERN.test(segment))) throw new Error(`connection: invalid RPC target ${JSON.stringify(`${channel}/${endpoint}`)}`);
4672
+ }
4673
+ //#endregion
4674
+ //#region lib/types/loopback-hostname.js
4675
+ /**
4676
+ * Browser-safe, zero-dependency loopback classification shared by the `/api`
4677
+ * Host fence and the package's `ctx.connection` state. The predicate stays
4678
+ * package-internal; client plugins consume the derived state through Cordis.
4679
+ */
4680
+ /**
4681
+ * Whether a normalized URL hostname names the local loopback authority.
4682
+ * @param hostname - WHATWG URL hostname (IPv6 literals retain brackets).
4683
+ * @returns true for localhost, IPv6 loopback, or any IPv4 address in 127/8.
4684
+ */
4685
+ function isLoopbackHostname(hostname) {
4686
+ if (hostname === "localhost" || hostname === "[::1]") return true;
4687
+ const parts = hostname.split(".");
4688
+ return parts.length === 4 && parts[0] === "127" && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
4689
+ }
4690
+ //#endregion
4691
+ //#region lib/types/client/index.js
4692
+ /** Required services (none — this is the wire root). */
4693
+ const inject = [];
4694
+ function watchBrowserNetwork(controller) {
4695
+ const browser = globalThis.window;
4696
+ const initiallyAvailable = browser?.navigator?.onLine;
4697
+ if (browser === void 0 || initiallyAvailable === void 0) return () => {};
4698
+ const online = () => {
4699
+ controller.setNetworkAvailable(true);
4700
+ };
4701
+ const offline = () => {
4702
+ controller.setNetworkAvailable(false);
4703
+ };
4704
+ controller.setNetworkAvailable(initiallyAvailable);
4705
+ browser.addEventListener("online", online);
4706
+ browser.addEventListener("offline", offline);
4707
+ return () => {
4708
+ browser.removeEventListener("online", online);
4709
+ browser.removeEventListener("offline", offline);
4710
+ };
4711
+ }
4712
+ /**
4713
+ * Client plugin body: pick the api by page mode and provide ctx.connection.
4714
+ * @param ctx - client cordis context.
4715
+ */
4716
+ function apply(ctx) {
4717
+ const pageLocation = typeof location === "undefined" ? void 0 : location;
4718
+ const fixtureRpc = pageLocation !== void 0 && new URLSearchParams(pageLocation.search).has("fixture") ? createFixtureConnectionRpc() : void 0;
4719
+ const transport = globalThis.__DSH_TRANSPORT__;
4720
+ const rpc = fixtureRpc ?? createWebConnectionRpc(transport?.fetch, transport?.openStream);
4721
+ let generationSource;
4722
+ let owner;
4723
+ let generationId = 0;
4724
+ let generation;
4725
+ let state;
4726
+ const generationListeners = /* @__PURE__ */ new Set();
4727
+ const stateListeners = /* @__PURE__ */ new Set();
4728
+ const publishGeneration = (next) => {
4729
+ if (Object.is(generation, next)) return;
4730
+ generation = next;
4731
+ for (const listener of [...generationListeners]) try {
4732
+ listener();
4733
+ } catch (error) {
4734
+ console.error("[connection] generation listener threw:", error);
4735
+ }
4736
+ };
4737
+ const publishState = (next) => {
4738
+ if (state === next) return;
4739
+ state = next;
4740
+ for (const listener of [...stateListeners]) try {
4741
+ listener();
4742
+ } catch (error) {
4743
+ console.error("[connection] state listener threw:", error);
4744
+ }
4745
+ };
4746
+ const releaseOwner = (current) => {
4747
+ if (owner !== current) return;
4748
+ owner = void 0;
4749
+ current.stopNetworkWatch();
4750
+ current.controller.stop();
4751
+ publishGeneration(void 0);
4752
+ publishState(void 0);
4753
+ };
4754
+ const handle = {
4755
+ isLoopback: transport?.ownsHost === true || pageLocation === void 0 || isLoopbackHostname(pageLocation.hostname),
4756
+ generation: {
4757
+ getSnapshot: () => generation,
4758
+ subscribe: (listener) => {
4759
+ generationListeners.add(listener);
4760
+ return () => {
4761
+ generationListeners.delete(listener);
4762
+ };
4763
+ }
4764
+ },
4765
+ state: {
4766
+ getSnapshot: () => state,
4767
+ subscribe: (listener) => {
4768
+ stateListeners.add(listener);
4769
+ return () => {
4770
+ stateListeners.delete(listener);
4771
+ };
4772
+ }
4773
+ },
4774
+ rpc,
4775
+ reconnect() {
4776
+ owner?.controller.reconnect();
4777
+ },
4778
+ registerGenerationSource(source) {
4779
+ if (generationSource !== void 0) throw new Error("connection: a generation source is already registered");
4780
+ generationSource = source;
4781
+ return () => {
4782
+ if (generationSource !== source) return;
4783
+ generationSource = void 0;
4784
+ const current = owner;
4785
+ if (current?.source === source) releaseOwner(current);
4786
+ };
4787
+ },
4788
+ start(sinks, config) {
4789
+ if (owner !== void 0) throw new Error("connection: the stream loop is already owned by another consumer");
4790
+ const source = generationSource;
4791
+ if (source === void 0) throw new Error("connection: no generation source is registered");
4792
+ const token = {};
4793
+ const ownsGeneration = () => owner?.token === token;
4794
+ const controller = new ConnectionController(source, {
4795
+ ...sinks,
4796
+ onConnected: (host) => {
4797
+ const nextGeneration = {
4798
+ id: ++generationId,
4799
+ host
4800
+ };
4801
+ publishGeneration(nextGeneration);
4802
+ if (!ownsGeneration() || !Object.is(generation, nextGeneration)) return;
4803
+ sinks.onConnected?.(host);
4804
+ },
4805
+ onStateChange: (state) => {
4806
+ if (state !== "connected") publishGeneration(void 0);
4807
+ if (!ownsGeneration()) return;
4808
+ publishState(state);
4809
+ sinks.onStateChange?.(state);
4810
+ }
4811
+ }, config ?? {});
4812
+ const current = {
4813
+ token,
4814
+ source,
4815
+ controller,
4816
+ stopNetworkWatch: watchBrowserNetwork(controller)
4817
+ };
4818
+ owner = current;
4819
+ controller.start();
4820
+ return { stop: () => {
4821
+ releaseOwner(current);
4822
+ } };
4823
+ }
4824
+ };
4825
+ ctx.provide("connection", handle);
4826
+ }
4827
+ //#endregion
4828
+ exports.RpcId = RpcId;
4829
+ exports.apply = apply;
4830
+ exports.inject = inject;
4831
+ exports.transportError = transportError;
4832
+ return module.exports;
4833
+ }
4834
+ });
4835
+
4836
+ //# sourceMappingURL=client.js.map