dsh-context 0.64.0 → 0.66.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.
Files changed (5) hide show
  1. package/README.md +9 -10
  2. package/lib/client.js +30 -13560
  3. package/lib/index.d.ts +102 -39
  4. package/lib/index.js +690 -1578
  5. package/package.json +16 -15
package/lib/index.js CHANGED
@@ -1,21 +1,24 @@
1
+ /**
2
+ * dsh-context v0.66.0
3
+ * A DeepSeek Harness plugin for context insight and management, with context dashboard and context command, for understanding how the context is made of, and how it evolves.
4
+ * @author bowenliang123
5
+ * @license Apache-2.0
6
+ * @homepage https://github.com/bowenliang123/dsh-context
7
+ * @built 2026-10-08T17:44:49+00:00
8
+ * @commit f60e1a56439b
9
+ * @commit-date 2026-10-09T01:41:58+08:00
10
+ */
11
+
1
12
  import { createRequire } from "node:module";
2
13
  import { z } from "zod";
3
- import { SessionLogOffset, deriveEventMessage, interruptedTurnClosers } from "@deepseek-ai/dsh-session";
14
+ import { deriveEventMessage, interruptedTurnClosers } from "@deepseek-ai/dsh-session";
15
+ import z$1 from "@deepseek-ai/schemastery";
4
16
  import { existsSync, readFileSync } from "node:fs";
5
17
  import { dirname, isAbsolute, join, normalize, relative } from "node:path";
6
18
  import { fileURLToPath } from "node:url";
7
- import z$1 from "@deepseek-ai/schemastery";
19
+ import { getHeapStatistics } from "node:v8";
8
20
  import { randomUUID } from "node:crypto";
9
21
  //#region src/shared/providers.ts
10
- /**
11
- * The provider-id seam between dsh request envelopes and the models.dev
12
- * registry: client/cost.ts resolves price-book branches through it, and
13
- * host/fold.ts uses the DeepSeek resolution to split the session-cost totals
14
- * into peak/off-peak periods. Only the renames live here — a dsh route key
15
- * that is not a registry id (user-declared gateways, `claude`/`codex`
16
- * subscriptions, org-prefixed catalogs) passes through and prices through
17
- * the model-side resolution index instead (client/cost.ts).
18
- */
19
22
  const MODELS_DEV_PROVIDER_IDS = {
20
23
  "deepseek-official": "deepseek",
21
24
  "deepseek-account": "deepseek",
@@ -23,38 +26,20 @@ const MODELS_DEV_PROVIDER_IDS = {
23
26
  "minimax-cn": "minimax",
24
27
  "zai-coding-cn": "zhipuai"
25
28
  };
26
- /** The models.dev provider id that prices a dsh provider (identity for unmapped ids). */
27
29
  function modelsDevProviderOf(dshProviderId) {
28
30
  return MODELS_DEV_PROVIDER_IDS[dshProviderId] ?? dshProviderId;
29
31
  }
30
- /** Whether a dsh provider prices through DeepSeek's period-based list (peak / half-price off-peak). */
31
32
  function isDeepSeekProvider(dshProviderId) {
32
33
  return modelsDevProviderOf(dshProviderId) === "deepseek";
33
34
  }
34
35
  //#endregion
35
36
  //#region src/shared/estimate.ts
36
- /**
37
- * Token heuristics shared by the host fold and the client boundary — the
38
- * harness token-meter's own fixed-density figure (dsh-token-meter/estimate.ts:
39
- * ~4 chars ≈ 1 token, +4 role framing). Priced identically on both sides so a
40
- * legacy value normalized at the client boundary matches what the host view
41
- * would have served.
42
- */
43
37
  const CHARS_PER_TOKEN$1 = 4;
44
38
  const ROLE_OVERHEAD$1 = 4;
45
- /** Price rendered system-prompt text; 0 for absent/empty/non-string input. */
46
39
  function estimateSystemTokens(text) {
47
40
  if (typeof text !== "string" || text.length === 0) return 0;
48
41
  return Math.ceil(text.length / CHARS_PER_TOKEN$1) + ROLE_OVERHEAD$1;
49
42
  }
50
- /**
51
- * Price a `system/message` payload's content exactly like the harness's
52
- * token-meter (`estimateSystemMessage`): text density over EVERY text block
53
- * plus role framing, with no per-block overhead — an adapter serializes the
54
- * prompt as plain text, so a text block costs its characters alone. Any other
55
- * block (or a hostile element) falls back to its JSON length. 0 for empty
56
- * content, which the harness reads as "no system prompt".
57
- */
58
43
  function estimateSystemContent(blocks) {
59
44
  if (!Array.isArray(blocks) || blocks.length === 0) return 0;
60
45
  let characters = 0;
@@ -73,30 +58,9 @@ function estimateSystemContent(blocks) {
73
58
  }
74
59
  //#endregion
75
60
  //#region src/shared/imageTokens.ts
76
- /**
77
- * Per-image token estimate for DeepSeek's vision model — a faithful port of
78
- * the official "图片 Token 计算器" (Image Token Calculator) shipped on the
79
- * DeepSeek API docs (https://api-docs.deepseek.com/zh-cn/quick_start/token_usage),
80
- * which implements the provider's own image→token conversion:
81
- *
82
- * - every image is aspect-preserved rescaled before entering the model:
83
- * below ~384×384 total pixels it is enlarged, above it is shrunk;
84
- * - tokens follow the patch grid (patch 14px, downsample 3), so every
85
- * image costs at least 117 and at most ~384 tokens (the documented cap).
86
- *
87
- * Verified against the docs calculator itself: 2048×1365→313, 800×600→341,
88
- * 2048×2048→349, 512×512→201, 100×100→117, 1920×1080→369, 400×900→249.
89
- * The DSH request pipeline's own 640k-pixel pre-resize does not change the
90
- * result (the provider formula rescales to the same patch grid), so the
91
- * durable attachment dimensions can be fed in directly.
92
- *
93
- * Pure math shared by the Host fold (message pricing) and the Client
94
- * (attachment card token badges) — no dependencies, never mutates.
95
- */
96
61
  const PATCH_SIZE = 14;
97
62
  const DOWNSAMPLE_RATIO = 3;
98
63
  const MAX_WH_RATIO = 8;
99
- /** ~384×384 total pixels: smaller images are enlarged before patching. */
100
64
  const MIN_PIXELS = 147456;
101
65
  const floorDiv = (a, b) => Math.floor(a / b);
102
66
  const ceilDiv = (a, b) => Math.floor((a + b - 1) / b);
@@ -106,7 +70,6 @@ function gridTokens(rows, cols) {
106
70
  n += ceilDiv(rows, 2) * (cols + 1) % 2 * 2;
107
71
  return n;
108
72
  }
109
- /** Solve the largest in-grid resize whose token count fits `budget`. */
110
73
  function solveResizeRatio(height, width, budget) {
111
74
  const ratio = height / width;
112
75
  const gridW = Math.sqrt((budget - 2) / ratio + .25) - .5;
@@ -116,8 +79,6 @@ function solveResizeRatio(height, width, budget) {
116
79
  let bestWidth;
117
80
  if (gridW < 1) {
118
81
  let rows = floorDiv(budget - 2, 2);
119
- /* v8 ignore else -- budget enters at 381 and the tall solve always fits
120
- (never decrements), so rows is always odd here; parity-defensive. */
121
82
  if (rows % 2 === 1) rows -= 1;
122
83
  bestWidth = unit;
123
84
  bestHeight = rows * unit;
@@ -144,7 +105,6 @@ function solveResizeRatio(height, width, budget) {
144
105
  numTokens: gridTokens(nLlmH, nLlmW)
145
106
  };
146
107
  }
147
- /** Resize so the patch grid fits the cap, then re-add the pad reserve. */
148
108
  function safeResize(height, width, paddedHeight, paddedWidth) {
149
109
  const nLlmH = ceilDiv(floorDiv(paddedHeight, PATCH_SIZE), DOWNSAMPLE_RATIO);
150
110
  const nLlmW = ceilDiv(floorDiv(paddedWidth, PATCH_SIZE), DOWNSAMPLE_RATIO);
@@ -182,10 +142,6 @@ function calcResizeInner(width, height) {
182
142
  const paddedHeight = ceilDiv(h, PATCH_SIZE) * PATCH_SIZE;
183
143
  return safeResize(h, w, paddedHeight, paddedWidth);
184
144
  }
185
- /**
186
- * Estimate the tokens one image consumes in a DeepSeek vision request from its pixel dimensions. Returns null for non-positive/non-finite
187
- * dimensions or when the official iteration fails to converge — callers fall back to the generic structural price.
188
- */
189
145
  function estimateImageTokens(width, height) {
190
146
  if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return null;
191
147
  try {
@@ -197,35 +153,17 @@ function estimateImageTokens(width, height) {
197
153
  }
198
154
  return null;
199
155
  } catch {
200
- /* v8 ignore next -- the only throw site is the clamped-away budget guard
201
- above; the catch keeps a hostile dimension from ever breaking the fold. */
202
156
  return null;
203
157
  }
204
158
  }
205
159
  //#endregion
206
160
  //#region src/host/pricing.ts
207
- /**
208
- * Token pricing — the same fixed-density heuristic as the harness's own
209
- * token-meter (`dsh-token-meter/estimate.ts`): ~4 chars ≈ 1 token, +4 per
210
- * content block, +4 role framing. Pure functions over message payloads.
211
- *
212
- * One deliberate refinement over the meter: `image` blocks. The meter prices
213
- * them through its generic JSON branch (~40 tokens for the durable ref),
214
- * while DeepSeek's vision model actually bills 117-384 tokens per image by
215
- * pixel dimensions (https://api-docs.deepseek.com/zh-cn/guides/vision/).
216
- * Image blocks therefore price through the official docs calculator port
217
- * (shared/imageTokens.ts), falling back to the meter's JSON price when the
218
- * attachment's dimensions are unknown.
219
- */
220
161
  const CHARS_PER_TOKEN = 4;
221
162
  const BLOCK_OVERHEAD = 4;
222
163
  const ROLE_OVERHEAD = 4;
223
164
  function estimateToolsTotal(tools) {
224
165
  return tools.length > 0 ? Math.ceil(JSON.stringify(tools).length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD : 0;
225
166
  }
226
- /** The `ContentBlock` walkers take `unknown`: block arrays ride the untrusted
227
- * log, so their element shapes (null and primitives included) are re-proved
228
- * here, not trusted from the declared message types. */
229
167
  function estimateBlocks(blocks) {
230
168
  let tokens = 0;
231
169
  if (!Array.isArray(blocks)) return 0;
@@ -243,9 +181,6 @@ function estimateBlocks(blocks) {
243
181
  case "tool-call":
244
182
  tokens += Math.ceil((block.name || "").length / CHARS_PER_TOKEN) + Math.ceil((block.arguments || "").length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD;
245
183
  break;
246
- case "tool-result":
247
- tokens += estimateBlocks(block.content) + BLOCK_OVERHEAD;
248
- break;
249
184
  case "image": {
250
185
  const ref = block.attachment;
251
186
  const priced = ref !== null && typeof ref === "object" && typeof ref.width === "number" && typeof ref.height === "number" ? estimateImageTokens(ref.width, ref.height) : null;
@@ -257,24 +192,13 @@ function estimateBlocks(blocks) {
257
192
  }
258
193
  return tokens;
259
194
  }
260
- /**
261
- * Price one surface message exactly like dsh's token-meter estimate:
262
- * an empty-content assistant/developer event projects to NO message, so it
263
- * prices 0; every other message pays content + role framing.
264
- */
265
195
  function estimateMessage(message, emptyIsZero = false) {
266
196
  if (emptyIsZero && (message === null || message === void 0 || !Array.isArray(message.content) || message.content.length === 0)) return 0;
267
197
  return estimateBlocks(message?.content) + ROLE_OVERHEAD;
268
198
  }
269
- /** The shared meter heuristic over rendered system-prompt text (shared/estimate.ts). */
270
- /** Per-tool price for the top-tools display (the total uses dsh's whole-array price). */
271
199
  function estimateToolSchema(tool) {
272
200
  return Math.ceil(JSON.stringify(tool).length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD;
273
201
  }
274
- /**
275
- * Count image blocks in a message payload, recursing into nested content (tool-result blocks carry their inner blocks) — seeds each node's
276
- * `imgs`, which the stats board's image cell sums over the LIVE surface (compacted/pruned messages stop counting).
277
- */
278
202
  function imageCountOf(blocks) {
279
203
  let count = 0;
280
204
  if (!Array.isArray(blocks)) return 0;
@@ -305,13 +229,6 @@ function toolCallNames(blocks) {
305
229
  }
306
230
  return names;
307
231
  }
308
- /**
309
- * Producer label for an injection event, mirroring the dsh transcript's
310
- * context provenance (client-runtime context-provenance.ts): workspace
311
- * instructions name the files they were reconciled from, a plugin source its
312
- * plugin id, and any other producer its own durable kind. Returns '' when
313
- * the source is missing or carries no readable identity at all.
314
- */
315
232
  function injectionSourceName(source) {
316
233
  if (source?.kind === "agent-instructions" && Array.isArray(source.changes)) {
317
234
  const paths = [];
@@ -331,31 +248,6 @@ function isInjection(source) {
331
248
  }
332
249
  //#endregion
333
250
  //#region src/host/logShapes.ts
334
- /**
335
- * Shape-driven readers over the durable session-event vocabulary — the ONE
336
- * place the plugin's log spellings meet. The supported range (dsh
337
- * 0.1.5-rc.1+, session formats V3/V4) speaks ONE dialect:
338
- * `system/message` surface nodes, `assistant/message.data.stream` /
339
- * `assistant/attempt.data.stream` (packed delta runs plus raw chunk
340
- * records), `SurfaceOp { startSeq, endSeq }`, `tool/ptc-dispatch`.
341
- *
342
- * The fold still reads SHAPES, never a detected harness version: the raw log
343
- * is untrusted input at every layer, and the supported harness refuses —
344
- * never rewrites — a legacy-format log, so anything that reaches these
345
- * readers is current-dialect or hostile. Every reader is total over
346
- * untrusted input — a malformed record yields "nothing here", never a throw
347
- * (the projection registry drives the fold without an error boundary; one
348
- * throw stalls the unit's push feed and the browser waits on "loading"
349
- * forever).
350
- *
351
- * @module dsh-context/host/log-shapes
352
- */
353
- /**
354
- * Whether one raw stream chunk carries a token delta — the first-token
355
- * marker, mirroring dsh-llm's `isTokenDelta` (a non-empty text or reasoning
356
- * fragment, or any Tool-call delta carrying arguments or a name); a
357
- * malformed chunk is simply not a token.
358
- */
359
251
  function isTokenChunk(chunk) {
360
252
  if (chunk === null || typeof chunk !== "object") return false;
361
253
  const c = chunk;
@@ -366,21 +258,11 @@ function isTokenChunk(chunk) {
366
258
  default: return false;
367
259
  }
368
260
  }
369
- /** Map one `blockType` to its timing bucket (see {@link DecodeKind}). */
370
261
  function decodeKindOfBlock(blockType) {
371
262
  if (blockType === "reasoning") return "reasoning";
372
263
  if (blockType === "text") return "text";
373
264
  if (blockType === "tool-call") return "toolarg";
374
265
  }
375
- /**
376
- * Per-kind decode spans AND counted `block-start` markers inside one embedded
377
- * assistant stream, tiling [first block-start, endTime]: each `block-start`
378
- * record owns the interval up to the next one, the last one up to `endTime`.
379
- * Total over untrusted input — a malformed record is skipped whole (a
380
- * marker with an unusable time anchors no span and counts nothing), a
381
- * non-finite boundary yields zero, and a stream with no marker (or not an
382
- * array) yields all zeros.
383
- */
384
266
  function decodeTallyOfStream(stream, endTime) {
385
267
  const tally = {
386
268
  spans: {
@@ -413,16 +295,6 @@ function decodeTallyOfStream(stream, endTime) {
413
295
  if (kind !== void 0) tally.spans[kind] += Math.max(0, endTime - since);
414
296
  return tally;
415
297
  }
416
- /**
417
- * The ORDERED decode spans of one embedded assistant stream — the same
418
- * ownership rule as {@link decodeTallyOfStream} (each `block-start` marker
419
- * owns the interval up to the next one, the last one up to `endTime`), but
420
- * keeping the sequence: the timing strip paints the decode window in stream
421
- * order. Total over untrusted input, mirroring the tally: a malformed record
422
- * is skipped whole, an unknown `blockType` leaves its interval unspanned
423
- * (the caller's residue fill owns it), a zero/negative-length span drops
424
- * out, and a stream with no marker (or not an array) yields none.
425
- */
426
298
  function decodeSpansOfStream(stream, endTime) {
427
299
  const spans = [];
428
300
  if (!Array.isArray(stream) || !Number.isFinite(endTime)) return spans;
@@ -450,14 +322,6 @@ function decodeSpansOfStream(stream, endTime) {
450
322
  close(endTime);
451
323
  return spans;
452
324
  }
453
- /**
454
- * The first token's instant inside one PACKED delta run (`text-chunks` /
455
- * `reasoning-chunks` / `tool-call-chunks`): the run's base time plus the
456
- * accumulated inter-member deltas, taken at the first qualifying member —
457
- * a name-bearing Tool-call run starts at its first member. Mirrors dsh-llm's
458
- * `runFirstTokenTime`; a non-finite base or delta yields undefined rather
459
- * than a NaN instant.
460
- */
461
325
  function runFirstTokenTime(record) {
462
326
  const time0 = record.time0;
463
327
  if (typeof time0 !== "number" || !Number.isFinite(time0)) return void 0;
@@ -475,12 +339,6 @@ function runFirstTokenTime(record) {
475
339
  if (typeof fragment === "string" && fragment !== "") return time;
476
340
  }
477
341
  }
478
- /**
479
- * The first token's instant inside an embedded assistant stream
480
- * (`assistant/message.data.stream`, `assistant/attempt.data.stream`), or
481
- * undefined when the stream carries no token. Mirrors dsh-llm's
482
- * `assistantStreamFirstTokenTime` over the compact record union.
483
- */
484
342
  function firstTokenTimeOfStream(stream) {
485
343
  if (!Array.isArray(stream)) return void 0;
486
344
  for (const record of stream) {
@@ -495,12 +353,6 @@ function firstTokenTimeOfStream(stream) {
495
353
  if (time !== void 0) return time;
496
354
  }
497
355
  }
498
- /**
499
- * The inclusive surface range a replacement op covers, or null for `append`
500
- * and for any unrecognized/hostile op (which the fold treats as an append).
501
- * The `startSeq`/`endSeq` endpoints are each accepted only as a finite
502
- * number, so a hostile op with one malformed endpoint degrades to append.
503
- */
504
356
  function replaceRangeOf(surfaceOp) {
505
357
  if (surfaceOp === null || typeof surfaceOp !== "object") return null;
506
358
  const op = surfaceOp;
@@ -515,7 +367,6 @@ function replaceRangeOf(surfaceOp) {
515
367
  }
516
368
  //#endregion
517
369
  //#region src/shared/fileOps.ts
518
- /** Parse a call's raw JSON arguments; non-string/malformed/non-record inputs yield null. */
519
370
  function parseCallArgs(raw) {
520
371
  if (typeof raw !== "string" || raw === "") return null;
521
372
  try {
@@ -533,45 +384,20 @@ const KIND_BY_TOOL = {
533
384
  grep: "search",
534
385
  glob: "search"
535
386
  };
536
- /** The file purpose of a tool, or null for non-file tools (bash, web_search…). */
537
387
  function kindOfTool(tool) {
538
388
  if (tool === void 0) return null;
539
389
  return KIND_BY_TOOL[tool] ?? null;
540
390
  }
541
- /**
542
- * The file purpose of one executed call. Like {@link kindOfTool} except for
543
- * the one file tool whose purpose follows its arguments: `str_replace_editor`
544
- * reads on `view` and writes on every other command (create / str_replace /
545
- * insert — an unknown command writes too; the call failed and the row keeps
546
- * its error flag).
547
- */
548
391
  function kindOfCall(tool, args) {
549
392
  if (tool === "str_replace_editor") return args !== null && args.command === "view" ? "read" : "write";
550
393
  return kindOfTool(tool);
551
394
  }
552
- /**
553
- * Whether a settled call's arguments can yield an op at all: the file tools
554
- * plus `str_replace_editor`, whose purpose follows its `command`. The public
555
- * face of the gate {@link opsOfCall} short-circuits on — callers that decide
556
- * whether to parse or to KEEP a call's raw arguments ask here instead of
557
- * restating the tool list.
558
- */
559
395
  function opBearingTool(tool) {
560
396
  return tool === "str_replace_editor" || kindOfTool(tool) !== null;
561
397
  }
562
- /**
563
- * Whether a call's raw arguments must ride the fold state
564
- * (TimelineState.callNames): every op-bearing tool, plus `run_code`, whose
565
- * `description` labels the ops its nested dispatches book at flush time.
566
- */
567
398
  function rawArgsNeeded(tool) {
568
399
  return opBearingTool(tool) || tool === "run_code";
569
400
  }
570
- /**
571
- * The operation's target path: the path-ish argument of read/write tools;
572
- * for searches the narrowing `path`, else the pattern itself (a pathless
573
- * grep/glob's target IS the pattern — the workspace-wide search text).
574
- */
575
401
  function pathOfArgs(tool, args) {
576
402
  if (args === null) return null;
577
403
  if (tool === "grep" || tool === "glob") {
@@ -590,27 +416,18 @@ function pathOfArgs(tool, args) {
590
416
  }
591
417
  return null;
592
418
  }
593
- /** Rendered line count: '' is 0, a trailing newline closes its own line. */
594
419
  function linesOf(s) {
595
420
  if (s === "") return 0;
596
421
  let n = 0;
597
422
  for (let i = 0; i < s.length; i++) if (s[i] === "\n") n++;
598
423
  return s.endsWith("\n") ? n : n + 1;
599
424
  }
600
- /** The added/removed pair of one content-bearing argument set, or zeros. */
601
425
  function pairOf(added, removed) {
602
426
  return {
603
427
  added: typeof added === "string" ? linesOf(added) : 0,
604
428
  removed: typeof removed === "string" ? linesOf(removed) : 0
605
429
  };
606
430
  }
607
- /**
608
- * The signed line footprint of one call: an edit removes its old string and
609
- * adds its new one; a write adds its content (the pre-existing body, if any,
610
- * is unknowable from the arguments — the estimate stays honest about that);
611
- * `str_replace_editor` splits the same shapes across its commands. Callers
612
- * reach here only with parsed args (a null parse yields no path).
613
- */
614
431
  function deltaOf(tool, args) {
615
432
  if (tool === "edit") return pairOf(args.new_string, args.old_string);
616
433
  if (tool === "write") return pairOf(args.content, void 0);
@@ -624,9 +441,6 @@ function deltaOf(tool, args) {
624
441
  removed: 0
625
442
  };
626
443
  }
627
- /** The exact window a read's result meta reports: `offset` plus the retained
628
- * `lines` array (the same bounded payload the read card renders from). Null
629
- * for a foreign or malformed meta. */
630
444
  function readWindowOf(meta) {
631
445
  if (meta === null || typeof meta !== "object") return null;
632
446
  const m = meta;
@@ -638,8 +452,6 @@ function readWindowOf(meta) {
638
452
  count: m.lines.length
639
453
  };
640
454
  }
641
- /** The limit estimate: the tool reads up to `limit` lines from `offset`;
642
- * absent when the call reads unbounded. */
643
455
  function readEstimateOf(args) {
644
456
  const limit = args.limit;
645
457
  return typeof limit === "number" && Number.isFinite(limit) && limit > 0 ? {
@@ -647,8 +459,6 @@ function readEstimateOf(args) {
647
459
  est: true
648
460
  } : void 0;
649
461
  }
650
- /** What a read op shows: the exact window off the result meta, else the
651
- * limit estimate, else nothing (an unbounded read names no footprint). */
652
462
  function readOf(meta, args) {
653
463
  const win = readWindowOf(meta);
654
464
  if (win !== null) return {
@@ -657,23 +467,12 @@ function readOf(meta, args) {
657
467
  };
658
468
  return readEstimateOf(args);
659
469
  }
660
- /**
661
- * A search op's detail: the pattern, with the include filter appended when
662
- * one narrowed the call. A patternless (malformed) search has no detail.
663
- */
664
470
  function searchDetailOf(args) {
665
471
  const pattern = args?.pattern;
666
472
  if (typeof pattern !== "string" || pattern === "") return void 0;
667
473
  const include = args?.include;
668
474
  return typeof include === "string" && include !== "" ? `${pattern} (${include})` : pattern;
669
475
  }
670
- /**
671
- * The files a search demonstrably reached, read off the result's bounded
672
- * presentation meta (grep groups matched lines by file; glob lists paths).
673
- * Only the COMPLETE list attributes: a capped search (`truncated`) names a
674
- * partial file set, and a malformed meta names none — both fall back to the
675
- * call's own target. Each entry carries the reported match count.
676
- */
677
476
  function searchFilesOf(meta) {
678
477
  if (meta === null || typeof meta !== "object") return null;
679
478
  const m = meta;
@@ -695,16 +494,6 @@ function searchFilesOf(meta) {
695
494
  }
696
495
  return files.length > 0 ? files : null;
697
496
  }
698
- /**
699
- * The one-shot per-call op assembly, uniform across every producer: the
700
- * host's call/result pairing (args off the armed call, meta off the
701
- * result), the nested Code-Mode settle (no meta exists on a dispatch — the
702
- * read window and per-file search attribution degrade to the argument-only
703
- * forms), and the client's inline-generation join fallback. Returns zero to
704
- * N records: a search with the complete matched-file meta rows per file;
705
- * any other file call rows once; a non-file tool (or a call whose arguments
706
- * resolve no target) rows nothing.
707
- */
708
497
  function opsOfCall(input) {
709
498
  const args = opBearingTool(input.tool) ? parseCallArgs(input.argsRaw) : null;
710
499
  const kind = kindOfCall(input.tool, args);
@@ -813,12 +602,6 @@ function trimState(st, bounds) {
813
602
  }
814
603
  }
815
604
  }
816
- /**
817
- * Bound on the timing strip's painted spans (TimelineState.spans): a count
818
- * cap, newest tail kept — the same hardcoded-cap family as SYSTEM_NODES_MAX.
819
- * At ~6–8 spans per step the cap covers a few hundred recent steps, far past
820
- * the strip's pixel resolution on any real pane.
821
- */
822
605
  const TIMING_SPANS_MAX = 2e3;
823
606
  function createTimelineState() {
824
607
  return {
@@ -850,41 +633,20 @@ function categoryOf(type, message) {
850
633
  if (isInjection(message?.source)) return "inject";
851
634
  return "user";
852
635
  }
853
- /**
854
- * Mark the detail collections dirty (TimelineState.detailRev). Every caller
855
- * is a fold branch that just mutated the requests/events/surface/archive;
856
- * branches that touch only the working slots (stepStart, callNames, the
857
- * shadow claim) or the envelope scalars do NOT bump — the served detail is
858
- * unchanged, and an open tab has nothing to refetch.
859
- */
860
636
  function bumpDetailRev(st) {
861
637
  st.detailRev = (st.detailRev ?? 0) + 1;
862
638
  }
863
- /**
864
- * Bound on the live system-prompt nodes (TimelineState.systems). The
865
- * effective figure is the LAST nonempty node, so dropping the oldest can only
866
- * under-report a pathological log whose newest SYSTEM_NODES_MAX nodes are all
867
- * empty while an older one still carried text.
868
- */
869
639
  const SYSTEM_NODES_MAX = 8;
870
- /** The effective system-prompt price: the last nonempty node, else 0 (the harness's own rule). */
871
640
  function systemTokensOf(systems) {
872
641
  for (let i = systems.length - 1; i >= 0; i--) if (systems[i].tokens > 0) return systems[i].tokens;
873
642
  return 0;
874
643
  }
875
- /** Append one system-prompt node, bounding the list (see SYSTEM_NODES_MAX). */
876
644
  function pushSystem(st, node) {
877
645
  const systems = [...st.systems ?? [], node];
878
646
  st.systems = systems.length > SYSTEM_NODES_MAX ? systems.slice(-8) : systems;
879
647
  st.systemTokens = systemTokensOf(st.systems);
880
648
  }
881
- /**
882
- * Bound on the buffered nested Code-Mode ops (TimelineState.pendingCodeOps)
883
- * — a hostile log that dispatches without settling the parent run_code
884
- * cannot grow the persisted state past this.
885
- */
886
649
  const PENDING_CODE_OPS_MAX = 200;
887
- /** JSON-stringify an unknown argument payload; a hostile (cyclic) value yields no args. */
888
650
  function argsRawOf(value) {
889
651
  if (typeof value === "string") return value;
890
652
  if (value === void 0 || value === null) return void 0;
@@ -894,15 +656,9 @@ function argsRawOf(value) {
894
656
  return;
895
657
  }
896
658
  }
897
- /** Append op records to the fold-derived log (the trim lives in trimState, with the other collections). */
898
659
  function pushFileOps(st, ops) {
899
660
  for (const op of ops) st.fileOps.push(op);
900
661
  }
901
- /**
902
- * Buffer nested Code-Mode ops under their top run_code call id (they flush
903
- * when the parent's result folds — the ops' locate target). A full buffer
904
- * drops new arrivals wholesale (defensive logs only).
905
- */
906
662
  function bufferCodeOps(st, rootCallId, ops) {
907
663
  const pending = st.pendingCodeOps ?? {};
908
664
  let total = 0;
@@ -913,24 +669,12 @@ function bufferCodeOps(st, rootCallId, ops) {
913
669
  [rootCallId]: [...pending[rootCallId] ?? [], ...ops]
914
670
  };
915
671
  }
916
- /**
917
- * Archive removed surface nodes as stamped COPIES — the objects leaving
918
- * `st.surface` are shared with the persisted previous state, so `gone` must
919
- * never be written onto them directly.
920
- */
921
672
  function archiveRemoved(st, removed, goneSeq) {
922
673
  for (const n of removed) st.archived.push({
923
674
  ...n,
924
675
  gone: goneSeq
925
676
  });
926
677
  }
927
- /**
928
- * Remove every live surface node whose seq the replacement claims, keeping the
929
- * per-category sums equal to the surviving nodes and archiving the removals.
930
- * Removal follows the SEQ list, not the declared range: pruned replacement
931
- * nodes keep their own seqs beyond the range end, so a range-based removal
932
- * would leave them behind and overcount. Returns the removed nodes.
933
- */
934
678
  function removeSurfaceSeqs(st, claimed, goneSeq) {
935
679
  if (claimed.size === 0) return [];
936
680
  const kept = [];
@@ -943,44 +687,24 @@ function removeSurfaceSeqs(st, claimed, goneSeq) {
943
687
  st.surface = kept;
944
688
  return removed;
945
689
  }
946
- /**
947
- * A message nested under data.message. A structural read keeps malformed
948
- * payloads total and handles developer messages absent from the V3
949
- * dependency's deriveEventMessage. User messages instead live in data itself.
950
- */
951
690
  function messageOf$1(data) {
952
691
  const message = data?.message;
953
692
  return message !== null && typeof message === "object" ? message : null;
954
693
  }
955
- /**
956
- * The first full text block, recursing through nested content blocks (a tool
957
- * result wraps its text in a `tool-result` block). Unlike `firstText` this
958
- * must NOT truncate/normalize: the skill name is matched off the raw
959
- * `<skill_content name="…">` wrapper.
960
- */
961
- function nestedText(blocks) {
694
+ function firstFullText(blocks) {
962
695
  if (!Array.isArray(blocks)) return "";
963
696
  for (const item of blocks) {
964
697
  if (item === null || typeof item !== "object") continue;
965
698
  const block = item;
966
699
  if (block.type === "text" && typeof block.text === "string" && block.text !== "") return block.text;
967
- if (block.content !== void 0) {
968
- const nested = nestedText(block.content);
969
- if (nested !== "") return nested;
970
- }
971
700
  }
972
701
  return "";
973
702
  }
974
- /**
975
- * The skill name a `skill`-tool result carries. Loaded skills are rendered as
976
- * `<skill_content name="…">…</skill_content>` in the result's text, so the name
977
- * is recovered from the content rather than trusted from the call envelope.
978
- */
979
703
  function skillNameOf(msg) {
980
- const match = nestedText(msg?.content).match(/<skill_content\s+name="([^"]+)"/);
704
+ const match = firstFullText(msg !== null && typeof msg === "object" ? msg.content : void 0).match(/<skill_content\s+name="([^"]+)"/);
981
705
  return match === null ? "" : match[1];
982
706
  }
983
- function applySurface(st, ev, type, data, message) {
707
+ function applySurface(st, ev, type, message) {
984
708
  const cat = categoryOf(type, message ?? void 0);
985
709
  const node = {
986
710
  seq: ev.seq,
@@ -1002,10 +726,7 @@ function applySurface(st, ev, type, data, message) {
1002
726
  }
1003
727
  } else if (type === "tool/result") {
1004
728
  const srcId = source?.callId;
1005
- const blockId = (message?.content?.[0])?.toolCallId;
1006
- const srcEntry = typeof srcId === "string" ? st.callNames[srcId] : void 0;
1007
- const blockEntry = srcEntry === void 0 && typeof blockId === "string" ? st.callNames[blockId] : void 0;
1008
- const toolEntry = srcEntry ?? blockEntry;
729
+ const toolEntry = typeof srcId === "string" ? st.callNames[srcId] : void 0;
1009
730
  if (toolEntry !== void 0) {
1010
731
  node.tool = toolEntry.name;
1011
732
  const timing = ensureTiming(st);
@@ -1019,12 +740,12 @@ function applySurface(st, ev, type, data, message) {
1019
740
  end: ev.time
1020
741
  }];
1021
742
  }
1022
- if (typeof srcId === "string" || typeof blockId === "string") {
743
+ if (typeof srcId === "string") {
1023
744
  const kept = {};
1024
- for (const k in st.callNames) if (k !== srcId && k !== blockId) kept[k] = st.callNames[k];
745
+ for (const k in st.callNames) if (k !== srcId) kept[k] = st.callNames[k];
1025
746
  st.callNames = kept;
1026
747
  }
1027
- if (data?.error || message?.isError === true) node.err = true;
748
+ if (message?.isError === true) node.err = true;
1028
749
  } else if (source?.kind === "skill-invocation") node.skill = typeof source.name === "string" ? source.name : "?";
1029
750
  else if (source?.kind === "plugin") {
1030
751
  if (source.form === "notice" && typeof source.summary === "string") node.text = source.summary;
@@ -1078,21 +799,6 @@ function applySurface(st, ev, type, data, message) {
1078
799
  st.sums[cat] += node.tokens;
1079
800
  return node;
1080
801
  }
1081
- /**
1082
- * One provider-reported usage bucket as a billed count, or null when the
1083
- * field carries no readable number. Accepts finite numbers and numeric
1084
- * strings; fractions round (some gateways report fractional counts) and
1085
- * negatives clamp to 0 — a mis-accounting gateway that reports
1086
- * `cached_tokens > prompt_tokens` drives the disjoint uncached-input figure
1087
- * below zero, and one raw figure in the state would fail the wire and state
1088
- * schemas' `.int().nonnegative()` gates on EVERY later delivery, permanently
1089
- * freezing the projection feed for the session (issue #44). NaN, infinities,
1090
- * and non-numeric values read as absent.
1091
- *
1092
- * Exported for the activity unit (host/activity.ts): the daily ledger re-proves
1093
- * the same durable usage buckets with the same sanitizer, so one raw figure
1094
- * can never enter either fold's state.
1095
- */
1096
802
  function tokenCountOf(value) {
1097
803
  if (typeof value === "number") return Number.isFinite(value) ? Math.max(0, Math.round(value)) : null;
1098
804
  if (typeof value === "string" && value.trim() !== "") {
@@ -1101,15 +807,6 @@ function tokenCountOf(value) {
1101
807
  }
1102
808
  return null;
1103
809
  }
1104
- /**
1105
- * DeepSeek's peak windows (the official list: UTC 01:00–04:00 and 06:00–10:00,
1106
- * Monday through Friday — Beijing Time 09:00–12:00 and 14:00–18:00). All other
1107
- * hours, plus entire weekends, bill at the half-price off-peak rate.
1108
- *
1109
- * Exported for the activity unit (host/activity.ts): the daily ledger prices
1110
- * each day's buckets with the same split, so its per-day fees sum to the
1111
- * session-cost totals the timeline fold books.
1112
- */
1113
810
  function isPeakUtc(time) {
1114
811
  const at = new Date(time);
1115
812
  const day = at.getUTCDay();
@@ -1117,26 +814,9 @@ function isPeakUtc(time) {
1117
814
  const h = at.getUTCHours();
1118
815
  return h >= 1 && h < 4 || h >= 6 && h < 10;
1119
816
  }
1120
- /**
1121
- * Fold one billed request into the session-cost totals, cloning along the
1122
- * mutated path only (the untouched branch stays shared with the persisted
1123
- * previous state — the apply contract never mutates it in place). The
1124
- * buckets arrive sanitized ({@link BilledUsage}), so the totals stay at the
1125
- * schemas' non-negative safe integers no matter what the provider reported.
1126
- * The key is the request envelope's (provider, model) face — the exact
1127
- * lookup the Client's model-price book resolves (models.dev). A request
1128
- * without a provider still accumulates (under the '' key) and the Client
1129
- * prices it when the model id is unambiguous; without a model there is
1130
- * nothing to price. DeepSeek's period-based list splits the buckets
1131
- * (peak windows at list price, all other hours half price); every other
1132
- * provider books everything under the list-price period.
1133
- */
1134
- function accumulateCost(st, time, usage) {
1135
- const model = st.model;
1136
- if (model === void 0) return;
1137
- const provider = st.provider ?? "";
817
+ function addBilledUsage(prev, provider, model, time, usage) {
1138
818
  const period = isDeepSeekProvider(provider) && !isPeakUtc(time) ? "off" : "peak";
1139
- const models = st.cost?.[provider] ?? {};
819
+ const models = prev?.[provider] ?? {};
1140
820
  const periods = models[model] ?? {};
1141
821
  const b = periods[period] ?? {
1142
822
  uncached: 0,
@@ -1155,44 +835,27 @@ function accumulateCost(st, time, usage) {
1155
835
  ...models,
1156
836
  [model]: nextPeriods
1157
837
  };
1158
- st.cost = {
1159
- ...st.cost ?? {},
838
+ return {
839
+ ...prev ?? {},
1160
840
  [provider]: nextModels
1161
841
  };
1162
842
  }
1163
- /**
1164
- * Advance the fold over ONE committed session event under the projection
1165
- * contract. Uninteresting events return the same reference (`Object.is` gates
1166
- * the change feed); any change returns a new reference over a lazy shallow
1167
- * clone, so the persisted state is never mutated in place by the caller.
1168
- * `bounds` come from the plugin config (config.ts) — retention only, they
1169
- * never change the state shape.
1170
- */
1171
- /** The timing card's per-tool ranking cap: the busiest 16 names are kept. */
843
+ function accumulateCost(st, time, usage) {
844
+ const model = st.model;
845
+ if (model === void 0) return;
846
+ st.cost = addBilledUsage(st.cost, st.provider ?? "", model, time, usage);
847
+ }
1172
848
  const TOOL_TIMING_CAP = 16;
1173
- /**
1174
- * The interactive Q&A tool (`dsh-tool-ask-user`): its settled result IS the
1175
- * user's answer, so it folds into the human-input tally alongside the user's
1176
- * own messages. One result = one answer submission, however many questions
1177
- * the prompt carried.
1178
- */
1179
849
  const ASK_USER_TOOL = "ask_user_question";
1180
- /** The decode buckets of the generation split, in card order (see TimingTotals). */
1181
850
  const DECODE_KINDS = [
1182
851
  "reasoning",
1183
852
  "text",
1184
853
  "toolarg"
1185
854
  ];
1186
- /** Non-negative, NaN-proof duration between two instants (hostile times degrade to 0). */
1187
855
  function durOf(from, to) {
1188
856
  if (!Number.isFinite(from) || !Number.isFinite(to)) return 0;
1189
857
  return Math.max(0, to - from);
1190
858
  }
1191
- /**
1192
- * The fold's private timing accumulator: created on first use, and CLONED on
1193
- * every later ensure() (see `applyTimeline`) — the object left in the
1194
- * persisted previous state is never written into in place.
1195
- */
1196
859
  function ensureTiming(st) {
1197
860
  if (st.timing === void 0) st.timing = {
1198
861
  wallMs: 0,
@@ -1205,7 +868,6 @@ function ensureTiming(st) {
1205
868
  };
1206
869
  return st.timing;
1207
870
  }
1208
- /** The TimingTotals fields each decode bucket's span and count land in. */
1209
871
  const DECODE_FIELDS = {
1210
872
  reasoning: {
1211
873
  ms: "reasoningMs",
@@ -1220,24 +882,11 @@ const DECODE_FIELDS = {
1220
882
  n: "toolArgBlocks"
1221
883
  }
1222
884
  };
1223
- /**
1224
- * Fold one block's decode span and count into the totals' generation split
1225
- * (see TimingTotals). A zero span or count stays ABSENT — the field then
1226
- * carries the "nothing was decoded/counted in this bucket" fact without adding
1227
- * dead properties to every pre-split-shaped state, and the card reads absence
1228
- * as 0.
1229
- */
1230
885
  function addDecode(timing, kind, ms, blocks) {
1231
886
  const field = DECODE_FIELDS[kind];
1232
887
  if (ms > 0) timing[field.ms] = (timing[field.ms] ?? 0) + ms;
1233
888
  if (blocks > 0) timing[field.n] = (timing[field.n] ?? 0) + blocks;
1234
889
  }
1235
- /**
1236
- * Tally one completed tool call into the per-name ranking, bounded to
1237
- * TOOL_TIMING_CAP names: repeated names update in place, a new name beyond
1238
- * the cap evicts the smallest tally first (the ranking's tail), so state
1239
- * stays bounded even over a hostile log of unique names.
1240
- */
1241
890
  function bumpToolTotals(timing, name, ms) {
1242
891
  if (!Object.hasOwn(timing.tools, name)) {
1243
892
  if (Object.keys(timing.tools).length >= TOOL_TIMING_CAP) {
@@ -1275,16 +924,6 @@ const CLONE_ALL = [
1275
924
  ];
1276
925
  function applyTimeline(state, event, bounds) {
1277
926
  let st;
1278
- /**
1279
- * The event's private working state: a shallow object clone up front, with
1280
- * only the named collections cloned into private copies. Everything else
1281
- * stays shared with the persisted previous state — the apply contract is
1282
- * "never mutate the previous state in place". Called EXACTLY ONCE per event
1283
- * (each case names the collections its own path may write into; helpers
1284
- * never call it), so building afresh per call is safe. The default is the
1285
- * full clone — a future branch that omits its list degrades to the old
1286
- * behavior instead of sharing a collection it mutates.
1287
- */
1288
927
  const ensure = (keys = CLONE_ALL) => {
1289
928
  st = { ...state };
1290
929
  for (const key of keys) if (key === "surface") st.surface = [...state.surface];
@@ -1334,7 +973,7 @@ function applyTimeline(state, event, bounds) {
1334
973
  const op = replaceRangeOf(event.surfaceOp);
1335
974
  if (op !== null) {
1336
975
  s.systems = (s.systems ?? []).filter((n) => n.seq < op.start || n.seq > op.end);
1337
- const claimed = /* @__PURE__ */ new Set();
976
+ const claimed = new Set();
1338
977
  for (const n of s.surface) if (n.seq >= op.start && n.seq <= op.end) claimed.add(n.seq);
1339
978
  if (removeSurfaceSeqs(s, claimed, event.seq).length > 0) bumpDetailRev(s);
1340
979
  }
@@ -1507,7 +1146,7 @@ function applyTimeline(state, event, bounds) {
1507
1146
  "events"
1508
1147
  ]);
1509
1148
  bumpDetailRev(s);
1510
- const node = applySurface(s, event, event.type, data, msg);
1149
+ const node = applySurface(s, event, event.type, msg);
1511
1150
  const source = msg?.source;
1512
1151
  if (developer || isInjection(source)) {
1513
1152
  const rec = {
@@ -1538,10 +1177,8 @@ function applyTimeline(state, event, bounds) {
1538
1177
  case "tool/result": {
1539
1178
  const toolMsg = deriveEventMessage(event);
1540
1179
  const srcId = (toolMsg?.source)?.callId;
1541
- const firstBlock = toolMsg?.content?.[0];
1542
- const blockId = firstBlock?.toolCallId;
1543
- const pendingEntry = (typeof srcId === "string" ? state.callNames[srcId] : void 0) ?? (typeof blockId === "string" ? state.callNames[blockId] : void 0);
1544
- const buffered = (typeof srcId === "string" ? state.pendingCodeOps?.[srcId] : void 0) ?? (typeof blockId === "string" ? state.pendingCodeOps?.[blockId] : void 0);
1180
+ const pendingEntry = typeof srcId === "string" ? state.callNames[srcId] : void 0;
1181
+ const buffered = typeof srcId === "string" ? state.pendingCodeOps?.[srcId] : void 0;
1545
1182
  const s = ensure([
1546
1183
  "surface",
1547
1184
  "sums",
@@ -1551,7 +1188,7 @@ function applyTimeline(state, event, bounds) {
1551
1188
  "timing"
1552
1189
  ]);
1553
1190
  bumpDetailRev(s);
1554
- const node = applySurface(s, event, event.type, data, toolMsg);
1191
+ const node = applySurface(s, event, event.type, toolMsg);
1555
1192
  if (node.tool === ASK_USER_TOOL) s.humanInputs = (s.humanInputs ?? 0) + 1;
1556
1193
  if (pendingEntry !== void 0 && pendingEntry.name === ASK_USER_TOOL && state.stepStart !== void 0) s.stepWaits = [...state.stepWaits ?? [], {
1557
1194
  start: pendingEntry.start,
@@ -1563,7 +1200,7 @@ function applyTimeline(state, event, bounds) {
1563
1200
  tool: pendingEntry.name,
1564
1201
  argsRaw: pendingEntry.argsRaw,
1565
1202
  meta: data?.meta,
1566
- err: Boolean(data?.error) || toolMsg?.isError === true || firstBlock?.isError === true
1203
+ err: toolMsg?.isError === true
1567
1204
  }));
1568
1205
  if (buffered !== void 0 && buffered.length > 0) {
1569
1206
  const program = parseCallArgs(pendingEntry?.argsRaw)?.description;
@@ -1573,7 +1210,7 @@ function applyTimeline(state, event, bounds) {
1573
1210
  ...typeof program === "string" && program !== "" ? { program } : {}
1574
1211
  })));
1575
1212
  const kept = {};
1576
- for (const k in s.pendingCodeOps) if (k !== srcId && k !== blockId) kept[k] = s.pendingCodeOps[k];
1213
+ for (const k in s.pendingCodeOps) if (k !== srcId) kept[k] = s.pendingCodeOps[k];
1577
1214
  if (Object.keys(kept).length > 0) s.pendingCodeOps = kept;
1578
1215
  else delete s.pendingCodeOps;
1579
1216
  }
@@ -1681,7 +1318,7 @@ function applyTimeline(state, event, bounds) {
1681
1318
  }
1682
1319
  }
1683
1320
  const asstMsg = deriveEventMessage(event);
1684
- applySurface(s, event, event.type, data, asstMsg);
1321
+ if (applySurface(s, event, event.type, asstMsg).text !== void 0) s.answers = (s.answers ?? 0) + 1;
1685
1322
  break;
1686
1323
  }
1687
1324
  case "session/end-seed": {
@@ -1730,15 +1367,6 @@ function applyTimeline(state, event, bounds) {
1730
1367
  }
1731
1368
  return state;
1732
1369
  }
1733
- /**
1734
- * The envelope scalars both wire generations share: current composition, the
1735
- * live-surface counters, and the copied cost/timing totals. Served value
1736
- * fields are COPIES — the served value must never alias persisted state.
1737
- * Optional scalars use conditional spread: an unknown value must not
1738
- * materialize an `undefined`-valued property (the lossless-JSON pipeline —
1739
- * a single such property can fail the whole push, the failure mode behind
1740
- * issue #29).
1741
- */
1742
1370
  function headFieldsOf(state) {
1743
1371
  const surfaceTotal = state.sums.user + state.sums.inject + state.sums.skill + state.sums.assistant + state.sums.tool;
1744
1372
  const result = {
@@ -1759,6 +1387,7 @@ function headFieldsOf(state) {
1759
1387
  images: state.surface.reduce((n, node) => n + (node.imgs ?? 0), 0),
1760
1388
  toolCalls: state.surface.reduce((n, node) => node.cat === "tool" || node.cat === "skill" && node.tool !== void 0 ? n + 1 : n, 0),
1761
1389
  humanInputs: state.humanInputs ?? 0,
1390
+ answers: state.answers ?? 0,
1762
1391
  ...state.lastUser !== void 0 ? { lastUser: state.lastUser } : {},
1763
1392
  requests: [],
1764
1393
  events: [],
@@ -1792,13 +1421,6 @@ function headFieldsOf(state) {
1792
1421
  if (state.systems !== void 0 && state.systems.length > 0) result.systems = state.systems.map((n) => ({ ...n }));
1793
1422
  return result;
1794
1423
  }
1795
- /**
1796
- * The heavy collections: copies of the retained request records and context
1797
- * events (each event attached to the requests around it — the chart's ✂
1798
- * anchoring), the bounded served surface window, and the removed-node
1799
- * archive. Shared verbatim by the inline wire view (channel-less hosts) and
1800
- * the on-demand detail payload (host/detail.ts).
1801
- */
1802
1424
  function detailCollectionsOf(state, bounds) {
1803
1425
  const result = {
1804
1426
  requests: state.requests.map((r) => ({ ...r })),
@@ -1840,29 +1462,27 @@ function detailCollectionsOf(state, bounds) {
1840
1462
  }
1841
1463
  return result;
1842
1464
  }
1843
- /**
1844
- * The split generation's SLIM wire head: the envelope scalars plus the
1845
- * precomputed count figures, the newest request's billing summary (the
1846
- * headline's derived anchor), and the detail revision marker. Small enough
1847
- * to ride every delivery channel whole (~1KB) — the heavy collections moved
1848
- * to the on-demand detail channel (host/detail.ts).
1849
- */
1850
1465
  function buildTimelineHead(state) {
1851
1466
  const result = headFieldsOf(state);
1852
- const turns = /* @__PURE__ */ new Set();
1467
+ const turns = new Set();
1853
1468
  for (const r of state.requests) turns.add(r.turn ?? 0);
1854
1469
  let injects = 0;
1855
1470
  let compactions = 0;
1856
1471
  let prunes = 0;
1857
- for (const e of state.events) if (e.kind === "inject") injects++;
1858
- else if (e.kind === "compaction") compactions++;
1859
- else if (e.kind === "prune") prunes++;
1472
+ const skills = new Set();
1473
+ for (const e of state.events) {
1474
+ if (e.kind === "inject") injects++;
1475
+ else if (e.kind === "compaction") compactions++;
1476
+ else if (e.kind === "prune") prunes++;
1477
+ if (e.sub === "skill" && typeof e.name === "string" && e.name !== "") skills.add(e.name);
1478
+ }
1860
1479
  result.counts = {
1861
1480
  turns: turns.size,
1862
1481
  steps: state.requests.length,
1863
1482
  injects,
1864
1483
  compactions,
1865
- prunes
1484
+ prunes,
1485
+ skills: skills.size
1866
1486
  };
1867
1487
  const last = state.requests.at(-1);
1868
1488
  if (last !== void 0) result.last = {
@@ -1873,12 +1493,6 @@ function buildTimelineHead(state) {
1873
1493
  result.detailRev = state.detailRev ?? 0;
1874
1494
  return result;
1875
1495
  }
1876
- /**
1877
- * The on-demand detail payload (host/detail.ts serves it off the live fold
1878
- * state): the heavy collections, the slim head at the SAME cut (the Agent
1879
- * network's cold-node ring fetch renders composition off it), and the
1880
- * revision marker the head carries.
1881
- */
1882
1496
  function buildTimelineDetail(state, bounds) {
1883
1497
  return {
1884
1498
  rev: state.detailRev ?? 0,
@@ -1886,13 +1500,6 @@ function buildTimelineDetail(state, bounds) {
1886
1500
  ...detailCollectionsOf(state, bounds)
1887
1501
  };
1888
1502
  }
1889
- /**
1890
- * Serve the INLINE projection wire view (channel-less hosts): the head
1891
- * scalars with the detail collections in place — the shape every delivery
1892
- * channel carried before the split generation. Bound the surface nodes to
1893
- * the newest tail and attach each event to the request around it; stamp
1894
- * COPIES — the persisted state objects are never mutated.
1895
- */
1896
1503
  function buildTimelineView(state, bounds) {
1897
1504
  return {
1898
1505
  ...headFieldsOf(state),
@@ -1900,94 +1507,355 @@ function buildTimelineView(state, bounds) {
1900
1507
  };
1901
1508
  }
1902
1509
  //#endregion
1903
- //#region src/shared/days.ts
1904
- /** Zero-pad to two digits (no Intl allocation on the hot path). */
1905
- function pad2(n) {
1906
- return n < 10 ? "0" + String(n) : String(n);
1510
+ //#region src/host/config.ts
1511
+ const DEFAULT_BOUNDS = {
1512
+ maxRequestSteps: 1500,
1513
+ maxKeptTurns: 300,
1514
+ maxEvents: 400,
1515
+ maxNodes: 2e3,
1516
+ maxArchiveNodes: 400,
1517
+ maxFileOps: 400
1518
+ };
1519
+ function volatileField(field) {
1520
+ const volatile = field.volatile;
1521
+ return typeof volatile === "function" ? volatile.call(field) : field;
1907
1522
  }
1908
- /**
1909
- * The local calendar-day key of an epoch-ms instant, or null when the time
1910
- * is not a finite number or falls outside the representable date range —
1911
- * untrusted log data can never produce a garbage key.
1912
- */
1913
- function dayKeyOf(time) {
1914
- if (!Number.isFinite(time)) return null;
1915
- const d = new Date(time);
1916
- const y = d.getFullYear();
1917
- if (!Number.isFinite(y) || y < 1970 || y > 9999) return null;
1918
- return `${String(y).padStart(4, "0")}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
1523
+ function count(defaultValue) {
1524
+ return z$1.number().min(1).step(1).default(defaultValue);
1525
+ }
1526
+ const Config = z$1.object({
1527
+ maxRequestSteps: count(DEFAULT_BOUNDS.maxRequestSteps),
1528
+ maxKeptTurns: count(DEFAULT_BOUNDS.maxKeptTurns),
1529
+ maxEvents: count(DEFAULT_BOUNDS.maxEvents),
1530
+ maxNodes: count(DEFAULT_BOUNDS.maxNodes),
1531
+ maxArchiveNodes: count(DEFAULT_BOUNDS.maxArchiveNodes),
1532
+ maxFileOps: count(DEFAULT_BOUNDS.maxFileOps),
1533
+ defaultPlacement: volatileField(z$1.union([
1534
+ "all",
1535
+ "tab",
1536
+ "sidebar"
1537
+ ]).default("all").loose()),
1538
+ defaultGranularity: volatileField(z$1.union(["step", "turn"]).default("step").loose()),
1539
+ defaultTrendMode: volatileField(z$1.union(["total", "delta"]).default("total").loose()),
1540
+ defaultDeltaBase: volatileField(z$1.union(["step", "turn"]).default("step").loose()),
1541
+ defaultToolSort: volatileField(z$1.union([
1542
+ "size",
1543
+ "count",
1544
+ "name"
1545
+ ]).default("count").loose()),
1546
+ defaultFileSort: volatileField(z$1.union([
1547
+ "count",
1548
+ "latest",
1549
+ "path"
1550
+ ]).default("count").loose()),
1551
+ insightsEntry: volatileField(z$1.union(["show", "hide"]).default("show").loose()),
1552
+ defaultDurationCurve: volatileField(z$1.union(["show", "hide"]).default("show").loose())
1553
+ });
1554
+ function resolveBounds(config) {
1555
+ const resolved = Config(config ?? {});
1556
+ return {
1557
+ maxRequestSteps: resolved.maxRequestSteps,
1558
+ maxKeptTurns: resolved.maxKeptTurns,
1559
+ maxEvents: resolved.maxEvents,
1560
+ maxNodes: resolved.maxNodes,
1561
+ maxArchiveNodes: resolved.maxArchiveNodes,
1562
+ maxFileOps: resolved.maxFileOps
1563
+ };
1919
1564
  }
1920
1565
  //#endregion
1921
- //#region src/host/activity.ts
1922
- /**
1923
- * The `contextActivity` session projection unit — the per-day activity
1924
- * ledger behind the Context Dashboard's heatmap and its last-7-days usage
1925
- * chart.
1926
- *
1927
- * The timeline unit (fold.ts) is a "current snapshot" fold: it prices the
1928
- * context as it stands NOW, which cannot draw a per-day chart. This unit
1929
- * folds the same committed event stream into a ledger keyed by local
1930
- * calendar day (shared/days.ts): every assistant settlement adds one
1931
- * completed request to its day and, when the provider reported a usage
1932
- * object, the day's billed-token figure grows by the disjoint buckets
1933
- * (uncached input + cache read + cache write + output — the same semantics
1934
- * the timeline's request records and the official token meter use). Each
1935
- * metered settlement also books its buckets into the day's per-(provider,
1936
- * model, period) pricing record — the same raw material the timeline's
1937
- * session-cost totals carry (SessionCostUsage), so the client prices every
1938
- * day off the SAME model-price book and estimator it prices the KPI band
1939
- * with. Buckets attribute to the day the open STEP STARTED (the turn's
1940
- * initiation, armed by `step/start` and cleared by `step/end` — the
1941
- * timeline fold's own pending-slot protocol), falling back to the
1942
- * settlement instant when no stamp is armed; the peak/off split prices the
1943
- * same instant, so a fee always lands on one day at one rate.
1944
- *
1945
- * One wire contract, one small state: a day entry is two integers plus the
1946
- * optional pricing record, the route in force rides the state (last
1947
- * `request/header` wins), and the retention cap keeps at most
1948
- * {@link MAX_KEPT_DAYS} keys, so the value riding every session-list row
1949
- * stays trivial next to the timeline head. Same projection contract as the
1950
- * sibling units: pure init/apply/view, unknown or malformed events return
1951
- * the state unchanged, and no `undefined`-valued property ever materializes
1952
- * (the plain-JSON persisted-state precondition).
1953
- */
1954
- /**
1955
- * Retention cap on ledger days. A little over a year of daily-active
1956
- * sessions; the oldest keys drop first (key order IS chronological order).
1957
- */
1958
- const MAX_KEPT_DAYS = 400;
1959
- const costBucketSchema = z.object({
1566
+ //#region src/host/timeline.ts
1567
+ const surfaceNodeSchema = z.object({
1568
+ seq: z.number().int().nonnegative(),
1569
+ time: z.number().optional(),
1570
+ cat: z.enum([
1571
+ "user",
1572
+ "inject",
1573
+ "skill",
1574
+ "assistant",
1575
+ "tool"
1576
+ ]),
1577
+ tokens: z.number().int().nonnegative(),
1578
+ imgs: z.number().int().nonnegative().optional(),
1579
+ gone: z.number().int().nonnegative().optional(),
1580
+ form: z.string().optional(),
1581
+ name: z.string().optional(),
1582
+ text: z.string().optional(),
1583
+ tool: z.string().optional(),
1584
+ err: z.boolean().optional(),
1585
+ skill: z.string().optional(),
1586
+ calls: z.array(z.string()).optional()
1587
+ }).strict();
1588
+ const systemPromptNodeSchema = z.object({
1589
+ seq: z.number().int().nonnegative(),
1590
+ time: z.number(),
1591
+ tokens: z.number().int().nonnegative()
1592
+ }).strict();
1593
+ const requestRecordSchema = z.object({
1594
+ turn: z.number().optional(),
1595
+ step: z.number().optional(),
1596
+ time: z.number(),
1597
+ seq: z.number(),
1598
+ system: z.number().int().nonnegative(),
1599
+ tools: z.number().int().nonnegative(),
1600
+ user: z.number().int().nonnegative(),
1601
+ inject: z.number().int().nonnegative(),
1602
+ assistant: z.number().int().nonnegative(),
1603
+ tool: z.number().int().nonnegative(),
1604
+ total: z.number().int().nonnegative(),
1605
+ prompt: z.number().int().nonnegative().optional(),
1606
+ skill: z.number().int().nonnegative().optional(),
1607
+ cacheRead: z.number().int().nonnegative().optional(),
1608
+ output: z.number().int().nonnegative().optional(),
1609
+ activeMs: z.number().nonnegative().optional(),
1610
+ stepCount: z.number().int().positive().optional()
1611
+ }).strict();
1612
+ const contextEventSchema = z.object({
1613
+ seq: z.number(),
1614
+ time: z.number(),
1615
+ kind: z.enum([
1616
+ "compaction",
1617
+ "prune",
1618
+ "inject",
1619
+ "model",
1620
+ "mode"
1621
+ ]),
1622
+ form: z.string().optional(),
1623
+ tokens: z.number().optional(),
1624
+ count: z.number().optional(),
1625
+ sub: z.string().optional(),
1626
+ name: z.string().optional(),
1627
+ detail: z.string().optional(),
1628
+ from: z.string().optional(),
1629
+ to: z.string().optional(),
1630
+ fromTurn: z.number().optional(),
1631
+ fromStep: z.number().optional(),
1632
+ turn: z.number().optional(),
1633
+ step: z.number().optional()
1634
+ }).strict();
1635
+ const fileOpSchema = z.object({
1636
+ seq: z.number().int().nonnegative(),
1637
+ path: z.string(),
1638
+ kind: z.enum([
1639
+ "read",
1640
+ "write",
1641
+ "search"
1642
+ ]),
1643
+ tool: z.string(),
1644
+ time: z.number().optional(),
1645
+ err: z.boolean(),
1646
+ added: z.number().int().nonnegative(),
1647
+ removed: z.number().int().nonnegative(),
1648
+ detail: z.string().optional(),
1649
+ hits: z.number().int().positive().optional(),
1650
+ read: z.union([z.object({
1651
+ start: z.number().int().positive(),
1652
+ count: z.number().int().nonnegative()
1653
+ }).strict(), z.object({
1654
+ count: z.number().int().positive(),
1655
+ est: z.literal(true)
1656
+ }).strict()]).optional(),
1657
+ parent: z.number().int().nonnegative().optional(),
1658
+ program: z.string().optional(),
1659
+ pattern: z.literal(true).optional(),
1660
+ gone: z.number().int().nonnegative().optional()
1661
+ }).strict();
1662
+ const timingSpanSchema = z.object({
1663
+ kind: z.enum([
1664
+ "ttft",
1665
+ "reasoning",
1666
+ "text",
1667
+ "toolarg",
1668
+ "tools",
1669
+ "other"
1670
+ ]),
1671
+ start: z.number(),
1672
+ end: z.number()
1673
+ }).strict();
1674
+ const currentSchema = z.object({
1675
+ system: z.number().int().nonnegative(),
1676
+ tools: z.number().int().nonnegative(),
1677
+ user: z.number().int().nonnegative(),
1678
+ inject: z.number().int().nonnegative(),
1679
+ skill: z.number().int().nonnegative(),
1680
+ assistant: z.number().int().nonnegative(),
1681
+ tool: z.number().int().nonnegative(),
1682
+ total: z.number().int().nonnegative()
1683
+ }).strict();
1684
+ const costBucketsSchema = z.object({
1960
1685
  uncached: z.number().int().nonnegative(),
1961
1686
  cacheRead: z.number().int().nonnegative(),
1962
1687
  cacheWrite: z.number().int().nonnegative(),
1963
1688
  output: z.number().int().nonnegative()
1964
1689
  }).strict();
1965
- const costModelSchema$1 = z.object({
1966
- peak: costBucketSchema.optional(),
1967
- off: costBucketSchema.optional()
1690
+ const costModelSchema = z.object({
1691
+ peak: costBucketsSchema.optional(),
1692
+ off: costBucketsSchema.optional()
1968
1693
  }).strict();
1969
- const costUsageSchema$1 = z.record(z.string(), z.record(z.string(), costModelSchema$1));
1970
- const activityDaySchema = z.object({
1971
- tokens: z.number().int().nonnegative(),
1972
- requests: z.number().int().nonnegative(),
1973
- cost: costUsageSchema$1.optional()
1694
+ const costModelsSchema = z.record(z.string(), costModelSchema);
1695
+ const costUsageSchema = z.record(z.string(), costModelsSchema);
1696
+ const toolTimingSchema = z.object({
1697
+ calls: z.number().int().nonnegative(),
1698
+ ms: z.number().nonnegative()
1699
+ }).strict();
1700
+ const timingTotalsSchema = z.object({
1701
+ wallMs: z.number().nonnegative(),
1702
+ ttftMs: z.number().nonnegative(),
1703
+ genMs: z.number().nonnegative(),
1704
+ reasoningMs: z.number().nonnegative().optional(),
1705
+ reasoningBlocks: z.number().int().nonnegative().optional(),
1706
+ textMs: z.number().nonnegative().optional(),
1707
+ textBlocks: z.number().int().nonnegative().optional(),
1708
+ toolArgMs: z.number().nonnegative().optional(),
1709
+ toolArgBlocks: z.number().int().nonnegative().optional(),
1710
+ speedTokens: z.number().nonnegative().optional(),
1711
+ speedMs: z.number().nonnegative().optional(),
1712
+ calls: z.number().int().nonnegative(),
1713
+ toolsMs: z.number().nonnegative(),
1714
+ toolCalls: z.number().int().nonnegative(),
1715
+ tools: z.record(z.string(), toolTimingSchema)
1716
+ }).strict();
1717
+ const unsupportedSchema = z.object({
1718
+ current: z.string(),
1719
+ minimum: z.string()
1720
+ }).strict();
1721
+ const countsSchema = z.object({
1722
+ turns: z.number().int().nonnegative(),
1723
+ steps: z.number().int().nonnegative(),
1724
+ injects: z.number().int().nonnegative(),
1725
+ compactions: z.number().int().nonnegative(),
1726
+ prunes: z.number().int().nonnegative(),
1727
+ skills: z.number().int().nonnegative().optional()
1728
+ }).strict();
1729
+ const lastSchema = z.object({
1730
+ seq: z.number(),
1731
+ total: z.number().int().nonnegative(),
1732
+ prompt: z.number().int().nonnegative().optional()
1733
+ }).strict();
1734
+ const contextTimelineSchema = z.object({
1735
+ ok: z.literal(true),
1736
+ unsupported: unsupportedSchema.optional(),
1737
+ model: z.string().optional(),
1738
+ provider: z.string().optional(),
1739
+ contextWindow: z.number().optional(),
1740
+ current: currentSchema,
1741
+ images: z.number().int().nonnegative().optional(),
1742
+ toolCalls: z.number().int().nonnegative().optional(),
1743
+ humanInputs: z.number().int().nonnegative().optional(),
1744
+ answers: z.number().int().nonnegative().optional(),
1745
+ lastUser: z.string().optional(),
1746
+ counts: countsSchema.optional(),
1747
+ last: lastSchema.optional(),
1748
+ detailRev: z.number().int().nonnegative().optional(),
1749
+ requests: z.array(requestRecordSchema).optional(),
1750
+ events: z.array(contextEventSchema).optional(),
1751
+ cost: costUsageSchema.optional(),
1752
+ timing: timingTotalsSchema.optional(),
1753
+ systems: z.array(systemPromptNodeSchema).optional(),
1754
+ nodes: z.array(surfaceNodeSchema).optional(),
1755
+ droppedNodes: z.number().int().nonnegative().optional(),
1756
+ archive: z.array(surfaceNodeSchema).optional(),
1757
+ surfaceFloor: z.number().int().nonnegative().optional(),
1758
+ archiveFloor: z.number().int().nonnegative().optional(),
1759
+ fileOps: z.array(fileOpSchema).optional(),
1760
+ fileOpsFloor: z.number().int().nonnegative().optional(),
1761
+ spans: z.array(timingSpanSchema).optional()
1762
+ }).strict();
1763
+ const timelineStateSchema = z.object({
1764
+ surface: z.array(surfaceNodeSchema),
1765
+ sums: z.object({
1766
+ user: z.number().int().nonnegative(),
1767
+ inject: z.number().int().nonnegative(),
1768
+ skill: z.number().int().nonnegative(),
1769
+ assistant: z.number().int().nonnegative(),
1770
+ tool: z.number().int().nonnegative()
1771
+ }).strict(),
1772
+ systemTokens: z.number().int().nonnegative(),
1773
+ systems: z.array(systemPromptNodeSchema).optional(),
1774
+ toolsTokens: z.number().int().nonnegative(),
1775
+ model: z.string().optional(),
1776
+ provider: z.string().optional(),
1777
+ lastModel: z.string().optional(),
1778
+ contextWindow: z.number().optional(),
1779
+ requests: z.array(requestRecordSchema),
1780
+ turnRuns: z.number().int().nonnegative().optional(),
1781
+ events: z.array(contextEventSchema),
1782
+ archived: z.array(surfaceNodeSchema),
1783
+ cost: costUsageSchema.optional(),
1784
+ archiveFloor: z.number().optional(),
1785
+ timing: timingTotalsSchema.optional(),
1786
+ humanInputs: z.number().int().nonnegative().optional(),
1787
+ answers: z.number().int().nonnegative().optional(),
1788
+ lastUser: z.string().optional(),
1789
+ stepStart: z.object({
1790
+ time: z.number(),
1791
+ firstToken: z.number().optional()
1792
+ }).strict().optional(),
1793
+ stepWaits: z.array(z.object({
1794
+ start: z.number(),
1795
+ end: z.number()
1796
+ }).strict()).optional(),
1797
+ stepApprovals: z.record(z.string(), z.number()).optional(),
1798
+ stepRequestSeq: z.number().optional(),
1799
+ callNames: z.record(z.string(), z.object({
1800
+ name: z.string(),
1801
+ start: z.number(),
1802
+ argsRaw: z.string().optional()
1803
+ }).strict()),
1804
+ pendingShadowedSeqs: z.array(z.number()).optional(),
1805
+ pendingShadowEventSeq: z.number().optional(),
1806
+ detailRev: z.number().int().nonnegative().optional(),
1807
+ fileOps: z.array(fileOpSchema),
1808
+ fileOpsFloor: z.number().int().nonnegative().optional(),
1809
+ spans: z.array(timingSpanSchema),
1810
+ stepSpans: z.array(timingSpanSchema).optional(),
1811
+ pendingCodeOps: z.record(z.string(), z.array(fileOpSchema)).optional()
1812
+ });
1813
+ function createContextTimelineDefinition(config, slim) {
1814
+ const bounds = resolveBounds(config);
1815
+ const view = (state) => slim() ? buildTimelineHead(state) : buildTimelineView(state, bounds);
1816
+ return {
1817
+ key: "contextTimeline",
1818
+ stateSchema: timelineStateSchema,
1819
+ wire: {
1820
+ viewSchema: contextTimelineSchema,
1821
+ view
1822
+ },
1823
+ init: () => createTimelineState(),
1824
+ apply: (state, event) => applyTimeline(state, event, bounds),
1825
+ stateVersion: 27
1826
+ };
1827
+ }
1828
+ //#endregion
1829
+ //#region src/shared/days.ts
1830
+ function dayKeyOf(time) {
1831
+ if (!Number.isFinite(time)) return null;
1832
+ const d = new Date(time);
1833
+ const y = d.getFullYear();
1834
+ if (!Number.isFinite(y) || y < 1970 || y > 9999) return null;
1835
+ return `${String(y).padStart(4, "0")}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
1836
+ }
1837
+ //#endregion
1838
+ //#region src/host/activity.ts
1839
+ const MAX_KEPT_DAYS = 400;
1840
+ const MAX_SKILLS_PER_DAY = 100;
1841
+ const activitySkillSchema = z.object({
1842
+ n: z.number().int().nonnegative(),
1843
+ last: z.number()
1844
+ }).strict();
1845
+ const activityDaySchema = z.object({
1846
+ tokens: z.number().int().nonnegative(),
1847
+ requests: z.number().int().nonnegative(),
1848
+ cost: costUsageSchema.optional(),
1849
+ skills: z.record(z.string(), activitySkillSchema).optional()
1974
1850
  }).strict();
1975
- /** Validate the wire payload before it leaves the host (strict: no drift). */
1976
1851
  const contextActivitySchema = z.object({ days: z.record(z.string(), activityDaySchema) }).strict();
1977
- /** The persisted fold-state schema — validated before a checkpoint row seeds a fold. */
1978
1852
  const activityStateSchema = z.object({
1979
1853
  days: z.record(z.string(), activityDaySchema),
1980
1854
  model: z.string().optional(),
1981
1855
  provider: z.string().optional(),
1982
- stepStart: z.number().optional()
1856
+ stepStart: z.number().optional(),
1857
+ skillCalls: z.array(z.string()).optional()
1983
1858
  });
1984
- /**
1985
- * One durable usage object's billed buckets, or null when NO bucket is
1986
- * readable (the same rule the timeline fold bills by: a settlement that
1987
- * carried no usage still counts its request without fabricating tokens).
1988
- * Every bucket passes the shared per-bucket sanitizer ({@link tokenCountOf}
1989
- * — fractions round, negatives clamp, garbage reads absent).
1990
- */
1991
1859
  function billedBucketsOf(value) {
1992
1860
  if (value === null || typeof value !== "object") return null;
1993
1861
  const usage = value;
@@ -2003,48 +1871,59 @@ function billedBucketsOf(value) {
2003
1871
  output: output ?? 0
2004
1872
  };
2005
1873
  }
2006
- /**
2007
- * Add one settlement's billed buckets to a day's pricing record — the same
2008
- * clone-along-the-mutated-path walk the timeline fold's accumulateCost
2009
- * prices by, so the two folds' records share one shape and one semantics.
2010
- * The pricing period splits off the SAME instant the day bucket does: a fee
2011
- * can never land on one day at the neighbouring window's rate.
2012
- */
2013
- function pricedDayOf(prev, provider, model, period, usage) {
2014
- const models = prev?.[provider] ?? {};
2015
- const periods = models[model] ?? {};
2016
- const b = periods[period] ?? {
2017
- uncached: 0,
2018
- cacheRead: 0,
2019
- cacheWrite: 0,
2020
- output: 0
1874
+ function withDayEntry(state, key, entry) {
1875
+ let days = {
1876
+ ...state.days,
1877
+ [key]: entry
2021
1878
  };
1879
+ const keys = Object.keys(days);
1880
+ if (keys.length > MAX_KEPT_DAYS) {
1881
+ keys.sort();
1882
+ const drop = new Set(keys.slice(0, keys.length - MAX_KEPT_DAYS));
1883
+ days = Object.fromEntries(Object.entries(days).filter(([k]) => !drop.has(k)));
1884
+ }
2022
1885
  return {
2023
- ...prev,
2024
- [provider]: {
2025
- ...models,
2026
- [model]: {
2027
- ...periods,
2028
- [period]: {
2029
- uncached: b.uncached + usage.input,
2030
- cacheRead: b.cacheRead + usage.cacheRead,
2031
- cacheWrite: b.cacheWrite + usage.cacheWrite,
2032
- output: b.output + usage.output
2033
- }
2034
- }
1886
+ ...state,
1887
+ days
1888
+ };
1889
+ }
1890
+ function bookSkillLoad(state, name, time) {
1891
+ const key = dayKeyOf(time);
1892
+ if (key === null) return state;
1893
+ const prev = state.days[key];
1894
+ const existing = prev?.skills?.[name];
1895
+ if (existing === void 0 && prev?.skills !== void 0 && Object.keys(prev.skills).length >= MAX_SKILLS_PER_DAY) return state;
1896
+ const skills = {
1897
+ ...prev?.skills,
1898
+ [name]: {
1899
+ n: (existing?.n ?? 0) + 1,
1900
+ last: existing === void 0 ? time : Math.max(existing.last, time)
2035
1901
  }
2036
1902
  };
1903
+ return withDayEntry(state, key, {
1904
+ tokens: prev?.tokens ?? 0,
1905
+ requests: prev?.requests ?? 0,
1906
+ ...prev?.cost !== void 0 ? { cost: prev.cost } : {},
1907
+ skills
1908
+ });
1909
+ }
1910
+ function invokedSkillOf(event) {
1911
+ const data = event.data;
1912
+ const carrier = event.type === "developer/message" ? data?.message : data;
1913
+ const source = carrier !== null && typeof carrier === "object" ? carrier.source : void 0;
1914
+ if (source === null || typeof source !== "object") return "";
1915
+ const rec = source;
1916
+ return rec.kind === "skill-invocation" && typeof rec.name === "string" ? rec.name : "";
1917
+ }
1918
+ function toolResultCallIdOf(message) {
1919
+ if (message === null || typeof message !== "object") return null;
1920
+ const rec = message;
1921
+ if (typeof rec.toolCallId === "string" && rec.toolCallId !== "") return rec.toolCallId;
1922
+ const source = rec.source;
1923
+ if (source === null || typeof source !== "object") return null;
1924
+ const callId = source.callId;
1925
+ return typeof callId === "string" && callId !== "" ? callId : null;
2037
1926
  }
2038
- /**
2039
- * Fold one committed event into the ledger. Four event types advance it:
2040
- * `request/header` tracks the route in force (the timeline fold's rule —
2041
- * last header wins), `step/start`/`step/end` maintain the initiation stamp,
2042
- * and `assistant/message` (the step settlement — both supported log
2043
- * generations) books the day. Everything else returns the state reference
2044
- * unchanged, as does a settlement with no attributable instant. The next
2045
- * state is a copy along the mutated path only — the persisted previous
2046
- * state is never mutated in place.
2047
- */
2048
1927
  function applyActivity(state, event) {
2049
1928
  if (event.type === "request/header") {
2050
1929
  const config = event.data?.header?.config;
@@ -2077,6 +1956,33 @@ function applyActivity(state, event) {
2077
1956
  if (event.data?.inherited !== true) return state;
2078
1957
  return { days: {} };
2079
1958
  }
1959
+ if (event.type === "user/message" || event.type === "developer/message") {
1960
+ const name = invokedSkillOf(event);
1961
+ return name === "" ? state : bookSkillLoad(state, name, event.time);
1962
+ }
1963
+ if (event.type === "tool/call") {
1964
+ const data = event.data;
1965
+ const callId = data?.callId;
1966
+ if (data?.name !== "skill" || typeof callId !== "string" || callId === "") return state;
1967
+ const pending = state.skillCalls ?? [];
1968
+ if (pending.includes(callId)) return state;
1969
+ return {
1970
+ ...state,
1971
+ skillCalls: [...pending, callId].slice(-100)
1972
+ };
1973
+ }
1974
+ if (event.type === "tool/result") {
1975
+ const message = event.data?.message;
1976
+ const callId = toolResultCallIdOf(message);
1977
+ const pending = state.skillCalls;
1978
+ if (callId === null || pending === void 0 || !pending.includes(callId)) return state;
1979
+ const skillCalls = pending.filter((id) => id !== callId);
1980
+ const next = { ...state };
1981
+ if (skillCalls.length > 0) next.skillCalls = skillCalls;
1982
+ else delete next.skillCalls;
1983
+ const name = skillNameOf(message);
1984
+ return name === "" ? next : bookSkillLoad(next, name, event.time);
1985
+ }
2080
1986
  if (event.type !== "assistant/message") return state;
2081
1987
  const initiated = state.stepStart !== void 0 ? state.stepStart : event.time;
2082
1988
  const key = dayKeyOf(initiated);
@@ -2085,40 +1991,14 @@ function applyActivity(state, event) {
2085
1991
  const buckets = billedBucketsOf(data?.usage);
2086
1992
  const prev = state.days[key];
2087
1993
  let cost = prev?.cost;
2088
- if (buckets !== null && state.model !== void 0) {
2089
- const provider = state.provider ?? "";
2090
- const period = isDeepSeekProvider(provider) && !isPeakUtc(initiated) ? "off" : "peak";
2091
- cost = pricedDayOf(cost, provider, state.model, period, buckets);
2092
- }
2093
- const entry = {
1994
+ if (buckets !== null && state.model !== void 0) cost = addBilledUsage(cost, state.provider ?? "", state.model, initiated, buckets);
1995
+ return withDayEntry(state, key, {
2094
1996
  tokens: (prev === void 0 ? 0 : prev.tokens) + (buckets === null ? 0 : buckets.input + buckets.cacheRead + buckets.cacheWrite + buckets.output),
2095
1997
  requests: (prev === void 0 ? 0 : prev.requests) + 1,
2096
- ...cost !== void 0 ? { cost } : {}
2097
- };
2098
- let days = {
2099
- ...state.days,
2100
- [key]: entry
2101
- };
2102
- const keys = Object.keys(days);
2103
- if (keys.length > MAX_KEPT_DAYS) {
2104
- keys.sort();
2105
- const drop = new Set(keys.slice(0, keys.length - MAX_KEPT_DAYS));
2106
- days = Object.fromEntries(Object.entries(days).filter(([k]) => !drop.has(k)));
2107
- }
2108
- return {
2109
- ...state,
2110
- days
2111
- };
1998
+ ...cost !== void 0 ? { cost } : {},
1999
+ ...prev?.skills !== void 0 ? { skills: prev.skills } : {}
2000
+ });
2112
2001
  }
2113
- /**
2114
- * The daily-activity projection unit, registered alongside the timeline and
2115
- * headers units (host/index.ts); the overview reads it through the session
2116
- * list's projection column. `stateVersion` 3: the fold resets the ledger at
2117
- * the tagged fork/seed boundary, so a seeded session's days count post-seed
2118
- * activity only (issue #94) — cached rows for seeded forks read
2119
- * version-stale on upgrade and the dashboard's warm-up cold-refolds them
2120
- * from the durable log, which drops the inherited prefix they booked.
2121
- */
2122
2002
  function createContextActivityDefinition() {
2123
2003
  return {
2124
2004
  key: "contextActivity",
@@ -2129,17 +2009,9 @@ function createContextActivityDefinition() {
2129
2009
  viewSchema: contextActivitySchema,
2130
2010
  view: (state) => ({ days: Object.fromEntries(Object.entries(state.days).map(([k, v]) => [k, { ...v }])) })
2131
2011
  },
2132
- stateVersion: 3
2012
+ stateVersion: 4
2133
2013
  };
2134
2014
  }
2135
- /**
2136
- * Recover the MCP server display label from a proxied tool name, or undefined
2137
- * for non-MCP names. `dsh-mcp-client` names tools `mcp__<server>__<rawName>`
2138
- * (normalized, and hash-appended when overlong/invalid — the label then shows
2139
- * whatever of the server survived the truncation). The separating `__` is the
2140
- * LAST one in the name, and it must sit AFTER the `mcp__` prefix: the prefix's
2141
- * own separator (or an empty server right after it) is not a server.
2142
- */
2143
2015
  function mcpServerOf(name) {
2144
2016
  if (!name.startsWith("mcp__")) return void 0;
2145
2017
  const cut = name.lastIndexOf("__");
@@ -2147,23 +2019,13 @@ function mcpServerOf(name) {
2147
2019
  const server = name.slice(5, cut);
2148
2020
  return server.length > 0 ? server : void 0;
2149
2021
  }
2150
- /** The `mcp:<server>` display label of a proxied tool name, or undefined. */
2151
2022
  function mcpSourceOf(name) {
2152
2023
  const server = mcpServerOf(name);
2153
2024
  return server !== void 0 ? `mcp:${server}` : void 0;
2154
2025
  }
2155
- /** The pinned package of a first-party tool name, or undefined. */
2156
2026
  function pinnedSourceOf(name) {
2157
2027
  return FIRST_PARTY_SOURCES[name];
2158
2028
  }
2159
- /**
2160
- * Pinned first-party tool → plugin package map (see header comment). One entry
2161
- * per model-facing name of the shipped tool packages in the official
2162
- * tool-schema catalog of the supported dsh baseline; names are verified
2163
- * stable across the supported harness range. Packages that mount under
2164
- * distinct names per composition (bash/pwsh persistent variants, the
2165
- * `subagent_fork` fixed-route alias) map to their primary package.
2166
- */
2167
2029
  const FIRST_PARTY_SOURCES = Object.freeze({
2168
2030
  read: "@deepseek-ai/dsh-tool-fs",
2169
2031
  write: "@deepseek-ai/dsh-tool-fs",
@@ -2202,68 +2064,8 @@ const FIRST_PARTY_SOURCES = Object.freeze({
2202
2064
  });
2203
2065
  //#endregion
2204
2066
  //#region src/host/attribution.ts
2205
- /**
2206
- * Live tool→plugin attribution layered on the static recovery in
2207
- * toolSources.ts.
2208
- *
2209
- * The session log records tools as plain `ToolSchema` entries (name /
2210
- * description / parameters) — the registering plugin is not in there.
2211
- * toolSources.ts derives the deterministic sources (harness-logged field,
2212
- * `mcp:<server>` naming, pinned first-party map). This module additionally
2213
- * watches RUNTIME registrations: cordis fires the `internal/get` waterfall on
2214
- * every context read of a service property, passing the READING context as the
2215
- * first argument, so `reader.fiber.name` identifies the plugin that is about
2216
- * to call `register()`.
2217
- *
2218
- * - The `internal/get` handler records the reading fiber's NAME — a scalar,
2219
- * never the reader context, which would pin the caller's whole agent — and
2220
- * wraps the tools service's `register` (once per underlying instance —
2221
- * cordis hands out a fresh traced proxy per read of a tracked service, so
2222
- * the proxy is peeled to the stable instance first; an earlier wrapper of a
2223
- * previous hook incarnation is peeled back to the original, so a plugin
2224
- * reload re-wraps without stacking) to capture the reader at registration
2225
- * time into a live map. Every wrapper is undone when the plugin unloads:
2226
- * the original `register` goes back on the instance, unless a newer hook
2227
- * incarnation re-wrapped it first (that incarnation's own cleanup then
2228
- * owns the restore).
2229
- * - When the reader slot is missing, root-named, or this plugin's own (e.g.
2230
- * LOCAL-LINK plugins — dev installs via `dsh plugin add <path>` or
2231
- * npm/pnpm link — whose anonymous entrypoints make cordis fall back to the
2232
- * root name), the wrapped `register` falls back to the call stack: the first
2233
- * frame outside this package is resolved to its nearest `package.json`
2234
- * `name`. That covers both npm installs (`node_modules/<pkg>`) and local
2235
- * links (any directory carrying a package.json), which never pass through
2236
- * node_modules. Frames that resolve back to this package are skipped.
2237
- * - `ownerOf(name)` prefers the name-derived `mcp:<server>` label (it names
2238
- * the actual provider, where the live record would only ever name the MCP
2239
- * proxy client), then the LIVE record — for a post-boot registration it is
2240
- * the truth, even when the name collides with a pinned first-party tool —
2241
- * then the pinned map (the boot-time guess for tools registered before the
2242
- * hook), and finally tags tools that were ALREADY registered when the hook
2243
- * installed (the boot snapshot — third-party bundles, e.g. local links like
2244
- * dsh-file-claim, that applied before dsh-context) with the
2245
- * `UNKNOWN_TOOL_SOURCE` sentinel: their registering plugin is unknowable,
2246
- * and a bare gap would read as "no plugin" instead of "unknown plugin".
2247
- *
2248
- * Best-effort by design: a read separated from `register()` by an `await` can
2249
- * be overwritten by another plugin's read (misattribution) and the stack
2250
- * fallback needs a resolvable package.json — both degrade to the name/pinned
2251
- * chain; registrations that predate the hook degrade to the unknown tag. The
2252
- * hook costs roughly +1.4us per service-property read and is negligible on
2253
- * the rare register path (the stack walk only runs when the reader slot is
2254
- * unusable, and its package lookups are cached per directory).
2255
- */
2256
- /** This module's own file URL — the stack walk skips its own frames. */
2257
2067
  const selfUrl = normalize(fileURLToPath(import.meta.url));
2258
- /** Directory → package-name cache for the synchronous walk below. */
2259
- const packageCache = /* @__PURE__ */ new Map();
2260
- /**
2261
- * Best-effort package name for a module file: walk up to the nearest
2262
- * `package.json` carrying a `name`. Works for dependencies installed under
2263
- * `node_modules` as well as local links whose package root is any on-disk
2264
- * directory. The per-directory results are cached.
2265
- * @param file - absolute path of a module file.
2266
- */
2068
+ const packageCache = new Map();
2267
2069
  function packageNameFrom(file) {
2268
2070
  let dir = dirname(file);
2269
2071
  for (let depth = 0; depth < 12; depth++) {
@@ -2284,31 +2086,11 @@ function packageNameFrom(file) {
2284
2086
  }
2285
2087
  }
2286
2088
  const FRAME_POSITION = /:\d+:\d+$/;
2287
- /** Symbol cordis registers (`Symbol.for`) on its traced proxies: reading it
2288
- * yields the wrapped target — the stable underlying service instance. */
2289
2089
  const CORDIS_ORIGINAL = Symbol.for("cordis.original");
2290
- /**
2291
- * Peel cordis's per-read traced proxy of a tracked service (dsh's ToolRuntime
2292
- * is one — every `ctx.tools` read returns a fresh proxy closing over the
2293
- * reader's context) down to the instance it wraps, so identity-keyed
2294
- * bookkeeping sees one stable object. Plain service objects pass through
2295
- * unchanged.
2296
- * @param tools - the value as handed out by a context read.
2297
- */
2298
2090
  function rawInstanceOf(tools) {
2299
2091
  return tools?.[CORDIS_ORIGINAL] ?? tools;
2300
2092
  }
2301
- /** Package name of this module's own package (self-fallbacks are filtered). */
2302
2093
  const selfPackage = packageNameFrom(selfUrl);
2303
- /**
2304
- * Resolve the registering package from a stack trace: walk frames from the
2305
- * innermost out, skipping this module's own frames and frames that resolve to
2306
- * this package, and return the package name of the first frame that resolves
2307
- * elsewhere. Works with both `file://` URLs and bare absolute paths
2308
- * (transpiled modules render without a scheme), with optional `fn (...)` and
2309
- * `async` wrappers.
2310
- * @param stack - `Error().stack`, or undefined when no fallback is desired.
2311
- */
2312
2094
  function callerPackageFrom(stack) {
2313
2095
  if (!stack) return void 0;
2314
2096
  for (const raw of stack.split("\n").slice(1)) {
@@ -2333,16 +2115,9 @@ function callerPackageFrom(stack) {
2333
2115
  if (name !== void 0 && name !== selfPackage) return name;
2334
2116
  }
2335
2117
  }
2336
- /**
2337
- * Install the runtime-attribution hook on a cordis app context. The hook
2338
- * rides the calling fiber's lifetime (`ctx.on`, and an effect that restores
2339
- * every patched `register`), so it is disposed with the plugin.
2340
- * @param ctx - the context the dsh-context plugin runs in; its fiber name is
2341
- * excluded from attributions.
2342
- */
2343
2118
  function createToolAttribution(ctx) {
2344
- const live = /* @__PURE__ */ new Map();
2345
- const wrapped = /* @__PURE__ */ new WeakSet();
2119
+ const live = new Map();
2120
+ const wrapped = new WeakSet();
2346
2121
  const self = ctx.fiber.name;
2347
2122
  let lastReader;
2348
2123
  const patched = [];
@@ -2357,7 +2132,7 @@ function createToolAttribution(ctx) {
2357
2132
  const wrappedRegister = function(definition) {
2358
2133
  const toolName = definition?.name;
2359
2134
  let owner = lastReader;
2360
- if (!owner || owner === "root" || owner === self) owner = callerPackageFrom((/* @__PURE__ */ new Error()).stack);
2135
+ if (!owner || owner === "root" || owner === self) owner = callerPackageFrom(new Error().stack);
2361
2136
  const dispose = original.call(this, definition);
2362
2137
  if (typeof toolName === "string" && owner && owner !== "root" && owner !== self && owner !== selfPackage) {
2363
2138
  live.set(toolName, owner);
@@ -2389,7 +2164,7 @@ function createToolAttribution(ctx) {
2389
2164
  }, "tools.register attribution");
2390
2165
  const toolsService = rawInstanceOf(ctx.get("tools", false));
2391
2166
  wrapInstance(toolsService);
2392
- const boot = /* @__PURE__ */ new Set();
2167
+ const boot = new Set();
2393
2168
  try {
2394
2169
  const toolEntries = toolsService?.layers?.global?.tools;
2395
2170
  if (toolEntries !== void 0 && typeof toolEntries.entries === "function") for (const [name] of toolEntries.entries()) boot.add(name);
@@ -2397,53 +2172,34 @@ function createToolAttribution(ctx) {
2397
2172
  return { ownerOf: (name) => mcpSourceOf(name) ?? live.get(name) ?? pinnedSourceOf(name) ?? (boot.has(name) ? "<unknown-plugin>" : void 0) };
2398
2173
  }
2399
2174
  //#endregion
2175
+ //#region src/host/connection.ts
2176
+ function fetchRouteRegistrar(connection) {
2177
+ return typeof connection?.fetch?.register === "function" ? connection.fetch.register.bind(connection.fetch) : void 0;
2178
+ }
2179
+ //#endregion
2400
2180
  //#region src/host/balance.ts
2401
- /** The plugin's balance route, under the authenticated `/api` fence. */
2402
2181
  const BALANCE_ROUTE = "/api/dsh-context/balance";
2403
- /** The settings id the DeepSeek API-key provider has served its connection
2404
- * section under: the V3 registered namespace, kept as the entry id on V4+
2405
- * product profiles. */
2406
2182
  const DEEPSEEK_SETTINGS_NS = "llm-deepseek";
2407
- /** llm-deepseek's default credential ref (the env-var name its section resolves). */
2408
- const DEFAULT_API_KEY_ENV = "DEEPSEEK_API_KEY";
2409
- /** The platform's public API root (llm-deepseek's PUBLIC_BASE_URL default). */
2410
2183
  const PUBLIC_BASE_URL = "https://api.deepseek.com";
2411
- /** One platform read's whole budget — never worth blocking the route longer. */
2412
2184
  const FETCH_TIMEOUT_MS = 1e4;
2413
- /** Narrow an unknown value to a string-keyed record, or null. */
2414
- function asRecord$1(value) {
2185
+ function asRecord$2(value) {
2415
2186
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2416
2187
  }
2417
- /** One platform amount ('110.00', or an already-numeric producer variant), or null. */
2418
2188
  function amountOf(value) {
2419
2189
  const n = typeof value === "string" ? Number(value) : value;
2420
2190
  return typeof n === "number" && Number.isFinite(n) && n >= 0 ? n : null;
2421
2191
  }
2422
- /**
2423
- * The DeepSeek API-key provider's settings section, read through whichever
2424
- * face the running line serves. V3's `get` reads the registered
2425
- * `llm-deepseek` section directly; V4+ retired that face, so its `describe()`
2426
- * projection is folded instead — the provider's row is the one whose served
2427
- * value declares the top-level `apiKeyEnv` credential ref (the volatile shape
2428
- * only that provider declares at the section root), preferred under the entry
2429
- * ids the generations have served it as (`llm-deepseek` on product profiles,
2430
- * `llm-deepseek-api-key` elsewhere). Rows fold in isolation — a hostile row
2431
- * drops whole, valid siblings keep serving — and a describe that throws is no
2432
- * section at all.
2433
- */
2434
2192
  function deepseekSectionOf(ctx) {
2435
2193
  const settings = ctx.get("settings");
2436
- const section = typeof settings?.get === "function" ? asRecord$1(settings.get(DEEPSEEK_SETTINGS_NS)) : null;
2437
- if (section !== null) return section;
2438
2194
  if (typeof settings?.describe !== "function") return null;
2439
2195
  try {
2440
2196
  const rows = settings.describe();
2441
2197
  if (!Array.isArray(rows)) return null;
2442
2198
  let shaped = null;
2443
2199
  for (const row of rows) try {
2444
- const record = asRecord$1(row);
2200
+ const record = asRecord$2(row);
2445
2201
  if (record === null) continue;
2446
- const value = asRecord$1(record.value);
2202
+ const value = asRecord$2(record.value);
2447
2203
  if (value === null || typeof value.apiKeyEnv !== "string" || value.apiKeyEnv === "") continue;
2448
2204
  if (record.ns === DEEPSEEK_SETTINGS_NS || record.ns === "llm-deepseek-api-key") return value;
2449
2205
  shaped ??= value;
@@ -2455,16 +2211,10 @@ function deepseekSectionOf(ctx) {
2455
2211
  return null;
2456
2212
  }
2457
2213
  }
2458
- /**
2459
- * Resolve the DeepSeek connection facts exactly as llm-deepseek serves its
2460
- * requests: its settings row for the credential ref and endpoint, the
2461
- * credentials service for the key. `null` whenever any fact is missing —
2462
- * the platform is simply not configured for this deployment.
2463
- */
2464
2214
  async function resolveFacts(ctx) {
2465
2215
  const section = deepseekSectionOf(ctx);
2466
2216
  if (section === null) return null;
2467
- const apiKeyEnv = typeof section.apiKeyEnv === "string" && section.apiKeyEnv !== "" ? section.apiKeyEnv : DEFAULT_API_KEY_ENV;
2217
+ const apiKeyEnv = section.apiKeyEnv;
2468
2218
  const baseUrl = typeof section.baseURL === "string" && section.baseURL !== "" ? section.baseURL : PUBLIC_BASE_URL;
2469
2219
  const credentials = ctx.get("credentials");
2470
2220
  const resolve = typeof credentials?.resolve === "function" ? credentials.resolve.bind(credentials) : void 0;
@@ -2481,22 +2231,13 @@ async function resolveFacts(ctx) {
2481
2231
  apiKey
2482
2232
  };
2483
2233
  }
2484
- /**
2485
- * Narrow the platform's payload to the wire value (the boundary rigor every
2486
- * parser owes untrusted input): each entry's currency and parts are re-proved
2487
- * and the total is the two summed — the platform's own `total_balance` rounds
2488
- * independently of them, so it can land a cent away from what the breakdown
2489
- * beside it adds up to, and the viewer's arithmetic is the authority. An entry
2490
- * failing the shape drops whole, and a payload with no valid entry is no
2491
- * balance at all.
2492
- */
2493
2234
  function balanceOfPayload(value) {
2494
- const data = asRecord$1(value);
2235
+ const data = asRecord$2(value);
2495
2236
  if (data === null) return null;
2496
2237
  const infos = Array.isArray(data.balance_infos) ? data.balance_infos : [];
2497
2238
  const balances = [];
2498
2239
  for (const info of infos) try {
2499
- const entry = asRecord$1(info);
2240
+ const entry = asRecord$2(info);
2500
2241
  if (entry === null) continue;
2501
2242
  const currency = entry.currency;
2502
2243
  const granted = amountOf(entry.granted_balance);
@@ -2516,19 +2257,9 @@ function balanceOfPayload(value) {
2516
2257
  balances
2517
2258
  } : null;
2518
2259
  }
2519
- /** The outbound platform fetch through the real global fetch. */
2520
2260
  const defaultFetcher = (url, init) => fetch(url, init);
2521
2261
  let fetcher = defaultFetcher;
2522
2262
  let inFlight = null;
2523
- /**
2524
- * One platform read for these facts, shared by the identical reads already in
2525
- * flight. Each read that finds none goes to the platform: the account's
2526
- * balance moves with every request the harness bills, so a remembered figure
2527
- * would disagree with the console page the capsule links to. The client's own
2528
- * open-time read is the rate limit. Every failure path — transport, timeout,
2529
- * non-ok status, malformed payload — resolves `null`; the route never throws
2530
- * into the transport.
2531
- */
2532
2263
  async function readBalance(facts) {
2533
2264
  if (inFlight !== null) return inFlight;
2534
2265
  inFlight = (async () => {
@@ -2552,16 +2283,9 @@ async function readBalance(facts) {
2552
2283
  inFlight = null;
2553
2284
  }
2554
2285
  }
2555
- /**
2556
- * Serve the balance route whenever the connection service composes (see
2557
- * host/detail.ts for the load-order contract). The registration rides the
2558
- * injected fiber: the service unloading withdraws the route. A rejecting
2559
- * registry only closes the capsule — never the plugin.
2560
- */
2561
2286
  function watchBalanceChannel(ctx) {
2562
2287
  ctx.inject(["connection"], (c) => {
2563
- const connection = c.get("connection");
2564
- const register = typeof connection?.fetch?.register === "function" ? connection.fetch.register.bind(connection.fetch) : void 0;
2288
+ const register = fetchRouteRegistrar(c.get("connection"));
2565
2289
  if (register === void 0) return;
2566
2290
  const handler = async () => {
2567
2291
  const facts = await resolveFacts(ctx);
@@ -2584,69 +2308,61 @@ function watchBalanceChannel(ctx) {
2584
2308
  });
2585
2309
  }
2586
2310
  //#endregion
2311
+ //#region src/host/coldRead.ts
2312
+ const HIGH_WATER = 2 / 3;
2313
+ function v8Reading() {
2314
+ const stats = getHeapStatistics();
2315
+ return {
2316
+ used: stats.used_heap_size,
2317
+ limit: stats.heap_size_limit
2318
+ };
2319
+ }
2320
+ const noop = () => {};
2321
+ function makeColdReadGate(reading = v8Reading) {
2322
+ const clear = () => {
2323
+ try {
2324
+ const { used, limit } = reading();
2325
+ if (!Number.isFinite(used) || !Number.isFinite(limit) || limit <= 0) return true;
2326
+ return used < limit * HIGH_WATER;
2327
+ } catch {
2328
+ return true;
2329
+ }
2330
+ };
2331
+ let tail = Promise.resolve();
2332
+ return { admit(read) {
2333
+ const run = tail.then(() => clear() ? read() : void 0);
2334
+ tail = run.then(noop, noop);
2335
+ return run;
2336
+ } };
2337
+ }
2338
+ //#endregion
2587
2339
  //#region src/host/backfill.ts
2588
- /** The plugin's warm-up trigger route, under the authenticated `/api` fence. */
2589
2340
  const BACKFILL_ROUTE = "/api/dsh-context/backfill";
2590
- /**
2591
- * Inter-session pacing so a multi-hundred-session backfill never starve the
2592
- * host: each cold read is a full durable-log decode, and a boot-time host
2593
- * under sustained read pressure stalls its own client handshake (the
2594
- * workspace sidebar rides it), so the pass breathes between sessions.
2595
- */
2596
2341
  const YIELD_MS = 100;
2597
- /**
2598
- * The `name` family dsh raises for a durable log it cannot interpret. The
2599
- * format edge throws `SessionFormatUnsupportedMigrationError`, but the
2600
- * persistence seam translates it — together with every other "intact yet not
2601
- * interpretable" refusal — into `SessionFormatUnsupportedError` before the
2602
- * error escapes (issue #75): classify by the family prefix, since either
2603
- * exact name may be the one that arrives.
2604
- */
2605
2342
  const UNSUPPORTED_FORMAT = "SessionFormatUnsupported";
2606
- function asRecord(value) {
2343
+ function asRecord$1(value) {
2607
2344
  return value !== null && typeof value === "object" ? value : null;
2608
2345
  }
2609
- /** One listed record's header, re-proved: id and cwd are the two fields the run relies on. */
2610
2346
  function headerOf(record) {
2611
- const header = asRecord(asRecord(record)?.header);
2347
+ const header = asRecord$1(asRecord$1(record)?.header);
2612
2348
  if (header === null) return null;
2613
2349
  if (typeof header.id !== "string" || header.id === "") return null;
2614
2350
  return header;
2615
2351
  }
2616
- /** The probe keys: the projection keys the cache must already serve. */
2617
2352
  const PROBE_KEYS = ["contextActivity", "contextTimeline"];
2618
- /**
2619
- * Probe the cache through the face the running generation serves. The older
2620
- * spelling is tried first and the newer one answers the fallback: on a
2621
- * 0.1.7-rc.2 cache the 3-arg call binds the branded offset onto `keys`,
2622
- * where `new Set(0)` throws as soon as a served record is reached — while a
2623
- * record-less session returns `undefined` without throwing, which is the
2624
- * correct "not served" either way. On 0.1.5-rc.1 the 3-arg face answers
2625
- * directly; if it ever throws there, the 2-arg retry degrades to the same
2626
- * "not served" verdict through the caller's catch.
2627
- */
2628
- function cachedSnapshotOf(cache, header) {
2629
- try {
2630
- return cache.cachedSnapshot(header, SessionLogOffset(0), PROBE_KEYS);
2631
- } catch {
2632
- return cache.cachedSnapshot(header, PROBE_KEYS);
2633
- }
2634
- }
2635
- /** Whether the cache already serves BOTH projection rows for this header (nothing to backfill). */
2636
2353
  function servesRows(cache, header) {
2637
2354
  try {
2638
- const values = asRecord(asRecord(cachedSnapshotOf(cache, header))?.values);
2355
+ const values = asRecord$1(asRecord$1(cache.cachedSnapshot(header, PROBE_KEYS))?.values);
2639
2356
  return values !== null && values.contextActivity !== void 0 && values.contextTimeline !== void 0;
2640
2357
  } catch {
2641
2358
  return false;
2642
2359
  }
2643
2360
  }
2644
- /** Read one stored log through a read handle, mirroring dsh-session-query's readColdSessionLog. */
2645
2361
  async function readColdLog(persistence, id, signal) {
2646
2362
  const handle = await persistence.open(id, "read", { signal });
2647
2363
  let events;
2648
2364
  try {
2649
- const result = asRecord(await handle.read(0, void 0, { signal }))?.events;
2365
+ const result = asRecord$1(await handle.read(0, void 0, { signal }))?.events;
2650
2366
  events = Array.isArray(result) ? result : [];
2651
2367
  } catch (error) {
2652
2368
  try {
@@ -2661,12 +2377,6 @@ async function readColdLog(persistence, id, signal) {
2661
2377
  events: [...events, ...interruptedTurnClosers(events)]
2662
2378
  };
2663
2379
  }
2664
- /**
2665
- * Whether one session is live (folds for itself — a cold write would only
2666
- * race its own checkpoints). A throwing registry read conservatively skips
2667
- * the session too: better to leave a row unfolded than to write over a
2668
- * possibly-live one.
2669
- */
2670
2380
  function isLive(sessions, id) {
2671
2381
  if (sessions === null || typeof sessions.get !== "function") return false;
2672
2382
  try {
@@ -2675,21 +2385,13 @@ function isLive(sessions, id) {
2675
2385
  return true;
2676
2386
  }
2677
2387
  }
2678
- /**
2679
- * Whether the cold read failed because the running harness refuses this log's
2680
- * format — a permanent, source-side refusal (the artifact is left unchanged,
2681
- * so retrying cannot succeed). Matched by the error's documented `name`, so no
2682
- * dsh symbol needs importing and every supported baseline classifies alike: an
2683
- * unknown error shape just falls to the warn path.
2684
- */
2685
2388
  function isUnsupportedFormat(error) {
2686
2389
  try {
2687
- return String(asRecord(error)?.name).startsWith(UNSUPPORTED_FORMAT);
2390
+ return String(asRecord$1(error)?.name).startsWith(UNSUPPORTED_FORMAT);
2688
2391
  } catch {
2689
2392
  return false;
2690
2393
  }
2691
2394
  }
2692
- /** The error's printable form — a hostile error may throw on its own toString. */
2693
2395
  function messageOf(error) {
2694
2396
  try {
2695
2397
  return String(error);
@@ -2697,14 +2399,7 @@ function messageOf(error) {
2697
2399
  return "unprintable error";
2698
2400
  }
2699
2401
  }
2700
- /**
2701
- * Arm the warm-up behind its trigger route. The route flips `requested`
2702
- * when the dashboard first opens; the cold-path faces compose `launch`.
2703
- * Either half may land first; the first request after both launches the one
2704
- * pass, and every later request is a no-op. Returns the deferred injects'
2705
- * disposer (abort on unload).
2706
- */
2707
- function watchActivityBackfill(ctx) {
2402
+ function watchActivityBackfill(ctx, coldReads = makeColdReadGate()) {
2708
2403
  let requested = false;
2709
2404
  let started = false;
2710
2405
  let launch = null;
@@ -2714,8 +2409,7 @@ function watchActivityBackfill(ctx) {
2714
2409
  launch();
2715
2410
  };
2716
2411
  const routeFiber = ctx.inject(["connection"], (c) => {
2717
- const connection = c.get("connection");
2718
- const register = typeof connection?.fetch?.register === "function" ? connection.fetch.register.bind(connection.fetch) : void 0;
2412
+ const register = fetchRouteRegistrar(c.get("connection"));
2719
2413
  if (register === void 0) return;
2720
2414
  try {
2721
2415
  c.effect(() => register({
@@ -2739,19 +2433,20 @@ function watchActivityBackfill(ctx) {
2739
2433
  "sessions"
2740
2434
  ], (raw) => {
2741
2435
  const injected = raw;
2742
- const query = asRecord(injected.sessionQuery);
2743
- const cache = asRecord(injected.sessionProjectionCache);
2744
- const persistence = asRecord(injected.sessionPersistence);
2436
+ const query = asRecord$1(injected.sessionQuery);
2437
+ const cache = asRecord$1(injected.sessionProjectionCache);
2438
+ const persistence = asRecord$1(injected.sessionPersistence);
2745
2439
  if (query === null || typeof query.listSessions !== "function") return;
2746
2440
  if (cache === null || typeof cache.cachedSnapshot !== "function" || typeof cache.coldSnapshot !== "function") return;
2747
2441
  if (persistence === null || typeof persistence.open !== "function") return;
2748
- const sessions = asRecord(injected.sessions);
2442
+ const sessions = asRecord$1(injected.sessions);
2749
2443
  const abort = new AbortController();
2750
2444
  const run = async () => {
2751
2445
  const listed = await query.listSessions(abort.signal);
2752
2446
  if (!Array.isArray(listed)) return;
2753
2447
  let folded = 0;
2754
2448
  let refused = 0;
2449
+ let paused = false;
2755
2450
  for (const record of listed) {
2756
2451
  if (abort.signal.aborted) return;
2757
2452
  const header = headerOf(record);
@@ -2759,8 +2454,14 @@ function watchActivityBackfill(ctx) {
2759
2454
  if (isLive(sessions, header.id)) continue;
2760
2455
  if (servesRows(cache, header)) continue;
2761
2456
  try {
2762
- const log = await readColdLog(persistence, header.id, abort.signal);
2763
- cache.coldSnapshot(log.header, log.inheritedEventCount, log.events);
2457
+ if (await coldReads.admit(async () => {
2458
+ const log = await readColdLog(persistence, header.id, abort.signal);
2459
+ cache.coldSnapshot(log.header, log.inheritedEventCount, log.events);
2460
+ return true;
2461
+ }) === void 0) {
2462
+ paused = true;
2463
+ break;
2464
+ }
2764
2465
  folded++;
2765
2466
  } catch (error) {
2766
2467
  if (isUnsupportedFormat(error)) {
@@ -2771,6 +2472,7 @@ function watchActivityBackfill(ctx) {
2771
2472
  await new Promise((resolve) => setTimeout(resolve, YIELD_MS));
2772
2473
  }
2773
2474
  if (folded > 0) ctx.logger.info(`dsh-context: projection rows backfilled for ${folded} session(s)`);
2475
+ if (paused) ctx.logger.info("dsh-context: projection backfill stopped early under heap pressure (remaining sessions refold on their next live activity)");
2774
2476
  if (refused > 0) ctx.logger.info(`dsh-context: projection backfill skipped ${refused} session(s) (legacy log format is not migratable; sources left unchanged)`);
2775
2477
  };
2776
2478
  launch = () => {
@@ -2788,96 +2490,11 @@ function watchActivityBackfill(ctx) {
2788
2490
  };
2789
2491
  }
2790
2492
  //#endregion
2791
- //#region src/host/config.ts
2792
- /**
2793
- * dsh-context entry configuration — the `config:` block of the `dsh-context`
2794
- * loader row in cordis.yml, plus the per-user display preferences.
2795
- *
2796
- * The schema is schemastery so both consumers it must serve accept it: cordis
2797
- * validates the entry config through the schema's Standard Schema face before
2798
- * `apply` runs (defaults per field, unknown keys merged through), and the
2799
- * harness's Config-form generation (dsh 0.1.7+) derives the Plugins page's
2800
- * live form from this same schema — there the namespace is the entry id
2801
- * (`dsh-context`) and only the `.volatile()` preference fields are served and
2802
- * editable. Bounds edits remount the entry (the folds read them at apply);
2803
- * preference edits commit volatile-only and never remount. On the older lines
2804
- * the preferences are inert here — their surface is the registered settings
2805
- * namespace (settings.ts). The `Config` type describes the RESOLVED shape
2806
- * cordis hands to `apply`; the raw patch values are partial and the schema
2807
- * fills every default.
2808
- */
2809
- const DEFAULT_BOUNDS = {
2810
- maxRequestSteps: 1500,
2811
- maxKeptTurns: 300,
2812
- maxEvents: 400,
2813
- maxNodes: 2e3,
2814
- maxArchiveNodes: 400,
2815
- maxFileOps: 400
2816
- };
2817
- /**
2818
- * Mark a field live-editable where the harness's schemastery ships the
2819
- * `.volatile()` modifier (the Config-form generation reads the mark to serve
2820
- * the field on the Plugins page); a plain field on the older lines whose
2821
- * schemastery predates the modifier.
2822
- */
2823
- function volatileField(field) {
2824
- const volatile = field.volatile;
2825
- return typeof volatile === "function" ? volatile.call(field) : field;
2826
- }
2827
- /** One bounded positive-integer count. */
2828
- function count(defaultValue) {
2829
- return z$1.number().min(1).step(1).default(defaultValue);
2493
+ //#region src/host/detail.ts
2494
+ const DETAIL_ROUTE = "/api/dsh-context/detail";
2495
+ function reply(value) {
2496
+ return Response.json(value, { headers: { "cache-control": "no-store" } });
2830
2497
  }
2831
- /** The cordis `Config` validator and the Config-form generation's served schema. */
2832
- const Config = z$1.object({
2833
- maxRequestSteps: count(DEFAULT_BOUNDS.maxRequestSteps),
2834
- maxKeptTurns: count(DEFAULT_BOUNDS.maxKeptTurns),
2835
- maxEvents: count(DEFAULT_BOUNDS.maxEvents),
2836
- maxNodes: count(DEFAULT_BOUNDS.maxNodes),
2837
- maxArchiveNodes: count(DEFAULT_BOUNDS.maxArchiveNodes),
2838
- maxFileOps: count(DEFAULT_BOUNDS.maxFileOps),
2839
- defaultPlacement: volatileField(z$1.union([
2840
- "all",
2841
- "tab",
2842
- "sidebar"
2843
- ]).default("all").loose()),
2844
- defaultGranularity: volatileField(z$1.union(["step", "turn"]).default("step").loose()),
2845
- defaultTrendMode: volatileField(z$1.union(["total", "delta"]).default("total").loose()),
2846
- defaultDeltaBase: volatileField(z$1.union(["step", "turn"]).default("step").loose()),
2847
- defaultToolSort: volatileField(z$1.union([
2848
- "size",
2849
- "count",
2850
- "name"
2851
- ]).default("count").loose()),
2852
- defaultFileSort: volatileField(z$1.union([
2853
- "count",
2854
- "latest",
2855
- "path"
2856
- ]).default("count").loose()),
2857
- insightsEntry: volatileField(z$1.union(["show", "hide"]).default("show").loose()),
2858
- defaultDurationCurve: volatileField(z$1.union(["show", "hide"]).default("show").loose())
2859
- });
2860
- /** Resolve the fold's retention bounds (the schema fills every default). */
2861
- function resolveBounds(config) {
2862
- const resolved = Config(config ?? {});
2863
- return {
2864
- maxRequestSteps: resolved.maxRequestSteps,
2865
- maxKeptTurns: resolved.maxKeptTurns,
2866
- maxEvents: resolved.maxEvents,
2867
- maxNodes: resolved.maxNodes,
2868
- maxArchiveNodes: resolved.maxArchiveNodes,
2869
- maxFileOps: resolved.maxFileOps
2870
- };
2871
- }
2872
- //#endregion
2873
- //#region src/host/detail.ts
2874
- /** The plugin's detail route, under the authenticated `/api` fence. */
2875
- const DETAIL_ROUTE = "/api/dsh-context/detail";
2876
- /** One JSON reply of the detail route. */
2877
- function reply(value) {
2878
- return Response.json(value, { headers: { "cache-control": "no-store" } });
2879
- }
2880
- /** The typed failure envelope (the same shape the Connection RPC carried). */
2881
2498
  function failure(code, message) {
2882
2499
  return reply({
2883
2500
  ok: false,
@@ -2887,18 +2504,11 @@ function failure(code, message) {
2887
2504
  }
2888
2505
  });
2889
2506
  }
2890
- /**
2891
- * Serve the detail route whenever the connection and sessions services are
2892
- * both composed (see the module header for the load-order contract). The
2893
- * registration rides the injected fiber: either service unloading withdraws
2894
- * the route and closes the gate.
2895
- */
2896
- function watchDetailChannel(ctx, bounds) {
2507
+ function watchDetailChannel(ctx, bounds, coldReads = makeColdReadGate()) {
2897
2508
  const gate = { live: false };
2898
2509
  ctx.inject(["connection", "sessions"], (c) => {
2899
- const connection = c.get("connection");
2900
2510
  const sessions = c.get("sessions");
2901
- const register = typeof connection?.fetch?.register === "function" ? connection.fetch.register.bind(connection.fetch) : void 0;
2511
+ const register = fetchRouteRegistrar(c.get("connection"));
2902
2512
  const getSession = typeof sessions?.get === "function" ? sessions.get.bind(sessions) : void 0;
2903
2513
  if (register === void 0 || getSession === void 0) return;
2904
2514
  const projections = ctx.sessionProjections;
@@ -2930,22 +2540,21 @@ function watchDetailChannel(ctx, bounds) {
2930
2540
  ok: true,
2931
2541
  value: null
2932
2542
  });
2933
- const observation = await observe(sessionId, { projectionMode: "none" });
2934
- const events = observation?.events;
2935
- if (!Array.isArray(events)) return reply({
2936
- ok: true,
2937
- value: null
2938
- });
2939
- let state = createTimelineState();
2940
- try {
2941
- for (const ev of events) state = applyTimeline(state, ev, bounds);
2942
- } finally {
2943
- const dispose = observation?.[Symbol.dispose];
2944
- if (typeof dispose === "function") dispose.call(observation);
2945
- }
2946
2543
  return reply({
2947
2544
  ok: true,
2948
- value: buildTimelineDetail(state, bounds)
2545
+ value: await coldReads.admit(async () => {
2546
+ const observation = await observe(sessionId, { projectionMode: "none" });
2547
+ const events = observation?.events;
2548
+ if (!Array.isArray(events)) return null;
2549
+ let state = createTimelineState();
2550
+ try {
2551
+ for (const ev of events) state = applyTimeline(state, ev, bounds);
2552
+ } finally {
2553
+ const dispose = observation?.[Symbol.dispose];
2554
+ if (typeof dispose === "function") dispose.call(observation);
2555
+ }
2556
+ return buildTimelineDetail(state, bounds);
2557
+ }) ?? null
2949
2558
  });
2950
2559
  } catch (err) {
2951
2560
  return failure("gateway/internal", err instanceof Error ? err.message : String(err));
@@ -2970,33 +2579,11 @@ function watchDetailChannel(ctx, bounds) {
2970
2579
  }
2971
2580
  //#endregion
2972
2581
  //#region src/shared/version.ts
2973
- /**
2974
- * The harness-version gate's shared arithmetic — the supported dsh baseline
2975
- * and the version compare behind it. Runtime code shared by BOTH halves (the
2976
- * host probes and gates; the client displays what the wire record carries),
2977
- * so this module must stay dependency-free.
2978
- *
2979
- * The baseline mirrors the support matrix (docs/compatibility.md and the
2980
- * package's `dsh.compatibility.dshReleases` declaration): the oldest dsh
2981
- * release this plugin works on. A harness BELOW it gets the fallback units
2982
- * (host/fallback.ts) instead of the real folds.
2983
- */
2984
- /** The oldest supported dsh release (see the matrix note above). */
2985
- const BASELINE_DSH_VERSION = "0.1.5-rc.1";
2986
- /**
2987
- * Release-channel rank at an equal X.Y.Z: a final release outranks its
2988
- * release candidates, which outrank betas, which outrank alphas
2989
- * (正式版 > RC > Beta > Alpha).
2990
- */
2582
+ const BASELINE_DSH_VERSION = "0.1.7-rc.2";
2991
2583
  function channelRank(channel) {
2992
2584
  return channel === "rc" ? 3 : channel === "beta" ? 2 : 1;
2993
2585
  }
2994
2586
  const RELEASE_RANK = 4;
2995
- /**
2996
- * Parse `v?[major].[minor].[patch][-(alpha|beta|rc)[.N]][+build]`, or null
2997
- * when the string is not that shape. Channels other than alpha/beta/rc
2998
- * (nightly, dev, …) do not parse — the gate fails open on them.
2999
- */
3000
2587
  function parseVersion(version) {
3001
2588
  const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-(alpha|beta|rc)(?:\.(\d+))?)?(?:\+[0-9a-z.-]+)?$/i.exec(version.trim());
3002
2589
  if (match === null) return null;
@@ -3009,10 +2596,6 @@ function parseVersion(version) {
3009
2596
  serial: match[5] ? Number(match[5]) : 0
3010
2597
  };
3011
2598
  }
3012
- /**
3013
- * Total order over parsed versions: X.Y.Z numerically first, then the
3014
- * channel rank, then the prerelease serial.
3015
- */
3016
2599
  function compareParsed(a, b) {
3017
2600
  if (a.major !== b.major) return a.major - b.major;
3018
2601
  if (a.minor !== b.minor) return a.minor - b.minor;
@@ -3020,12 +2603,6 @@ function compareParsed(a, b) {
3020
2603
  if (a.rank !== b.rank) return a.rank - b.rank;
3021
2604
  return a.serial - b.serial;
3022
2605
  }
3023
- /**
3024
- * Whether `version` satisfies the supported baseline. FAIL OPEN by design: a
3025
- * version that cannot be parsed (a dev/nightly harness build) must not blank
3026
- * a working deployment, so it passes — the gate trips only on a proven
3027
- * below-baseline release.
3028
- */
3029
2606
  function meetsBaseline(version, baseline = BASELINE_DSH_VERSION) {
3030
2607
  const v = parseVersion(version);
3031
2608
  const b = parseVersion(baseline);
@@ -3034,46 +2611,7 @@ function meetsBaseline(version, baseline = BASELINE_DSH_VERSION) {
3034
2611
  }
3035
2612
  //#endregion
3036
2613
  //#region src/host/headers.ts
3037
- /**
3038
- * The `contextHeaders` session projection unit — the request-header EPOCH
3039
- * METADATA behind the timeline's envelope figures.
3040
- *
3041
- * The hot `contextTimeline` unit carries only token prices of the system
3042
- * prompt and tool schemas; this companion unit keeps the per-epoch METADATA
3043
- * (epoch seq/time boundaries, per-tool token prices and plugin attribution)
3044
- * so the Context browser can pick the header epoch in force at any step and
3045
- * size its sections immediately. The epoch CONTENT (full system prompt text,
3046
- * full tool JSON schemas) deliberately does NOT ride the projection VALUE:
3047
- * session projections are served whole in every `session.list` row, control
3048
- * baseline, push frame, and change notification, so carrying content here
3049
- * multiplied it by sessions × epochs across every channel. The client
3050
- * fetches one epoch's `request/header` event on demand — a seq-anchored
3051
- * history read off the epoch's `seq`, the same targeted read the browser
3052
- * already uses for message content — and caches it per session (history is
3053
- * immutable).
3054
- *
3055
- * Read-compat over the persisted state (the pinned decision behind keeping
3056
- * `stateVersion` at 1): the harness serves a cold session's projections from
3057
- * its CACHED checkpoint rows and has no refresh channel for an idle session
3058
- * — a version bump invalidates every row and orphans the key until the
3059
- * session goes live again (the #37 regression). The state therefore still
3060
- * ACCEPTS the v1 content-bearing record shape, current folds append
3061
- * metadata-only records alongside any seeded legacy ones, and the view
3062
- * normalizes BOTH to the metadata-only wire shape (pricing the legacy system
3063
- * text at read time). Cached v1 rows keep working, new checkpoint writes
3064
- * shrink as legacy epochs age out of the capped list, and the wire — the
3065
- * part every delivery channel carries — is metadata-only from day one.
3066
- *
3067
- * Same projection contract as the timeline unit: pure init/apply/view,
3068
- * `Object.is` reference stability for uninteresting events, plain-JSON
3069
- * bounded state (epoch list capped — see HEADERS_MAX).
3070
- */
3071
- /** Retention cap on header epochs (metadata only; changes are rare; 50 is generous). */
3072
2614
  const HEADERS_MAX = 50;
3073
- /**
3074
- * The persisted-state schema: the SUPERSET of both record generations, so a
3075
- * cached v1 row (content-bearing) seeds the fold instead of being discarded.
3076
- */
3077
2615
  const storedToolSchema = z.object({
3078
2616
  name: z.string(),
3079
2617
  tokens: z.number().int().nonnegative(),
@@ -3089,13 +2627,11 @@ const storedEpochSchema = z.object({
3089
2627
  tools: z.array(storedToolSchema)
3090
2628
  }).strict();
3091
2629
  const contextHeadersStateSchema = z.object({ headers: z.array(storedEpochSchema) }).strict();
3092
- /** The wire schema: strict metadata — the shape every delivery channel carries. */
3093
2630
  const headerToolWireSchema = z.object({
3094
2631
  name: z.string(),
3095
2632
  tokens: z.number().int().nonnegative(),
3096
2633
  plugin: z.string().optional()
3097
2634
  }).strict();
3098
- /** Exported for the fallback unit (fallback.ts): one wire contract, one schema. */
3099
2635
  const contextHeadersSchema = z.object({ headers: z.array(z.object({
3100
2636
  seq: z.number(),
3101
2637
  time: z.number(),
@@ -3122,13 +2658,6 @@ function recordOf(event) {
3122
2658
  })
3123
2659
  };
3124
2660
  }
3125
- /**
3126
- * The context-headers projection unit; registered alongside the timeline unit (host/index.ts); clients read it through
3127
- * `useProjection('contextHeaders')` and fetch an epoch's full content on demand via the session history (historyPage.ts).
3128
- * Contract mirror with a REQUIRED `wire` block (see compat.ts).
3129
- * @param resolve - best-effort tool-to-plugin attribution (see toolSources.ts); fills a missing `plugin` at view time so
3130
- * epochs folded without attribution still render a tag when the source is known.
3131
- */
3132
2661
  function createContextHeadersDefinition(resolve) {
3133
2662
  const view = (state) => ({ headers: state.headers.map((h) => {
3134
2663
  const record = {
@@ -3168,382 +2697,8 @@ function createContextHeadersDefinition(resolve) {
3168
2697
  };
3169
2698
  }
3170
2699
  //#endregion
3171
- //#region src/host/timeline.ts
3172
- /**
3173
- * The `contextTimeline` session projection unit — the plugin's data plane.
3174
- *
3175
- * This is the whole Host half after the v0.9 data-path migration: instead of
3176
- * serving snapshots over a custom `/dsh-context` RPC channel, the plugin
3177
- * registers one pure projection unit on the harness's
3178
- * `ctx.sessionProjections` registry. The framework then:
3179
- * - drives the fold per committed `session/event` (eager, incremental),
3180
- * - persists the unit state through `ctx.sessionProjectionCache`
3181
- * (checkpointed rows, cold-read ladder, resume-safe),
3182
- * - delivers finished values to the browser as a `session/projection` push
3183
- * frame plus a tail-page baseline, where the Client reads them through
3184
- * the framework-standard `useProjection('contextTimeline')` seat.
3185
- *
3186
- * The unit is pure mathematics (init/apply/view) — it holds no subscriptions
3187
- * and never touches the client. The wire value is the same Snapshot the UI
3188
- * has always rendered (shared/types.ts), so the Client renders unchanged.
3189
- */
3190
- /** Validate the wire payload before it leaves the host (strict: no drift). */
3191
- const surfaceNodeSchema = z.object({
3192
- seq: z.number().int().nonnegative(),
3193
- time: z.number().optional(),
3194
- cat: z.enum([
3195
- "user",
3196
- "inject",
3197
- "skill",
3198
- "assistant",
3199
- "tool"
3200
- ]),
3201
- tokens: z.number().int().nonnegative(),
3202
- imgs: z.number().int().nonnegative().optional(),
3203
- gone: z.number().int().nonnegative().optional(),
3204
- form: z.string().optional(),
3205
- name: z.string().optional(),
3206
- text: z.string().optional(),
3207
- tool: z.string().optional(),
3208
- err: z.boolean().optional(),
3209
- skill: z.string().optional(),
3210
- calls: z.array(z.string()).optional()
3211
- }).strict();
3212
- /** One live system-prompt node (shared/types.ts SystemPromptNode). */
3213
- const systemPromptNodeSchema = z.object({
3214
- seq: z.number().int().nonnegative(),
3215
- time: z.number(),
3216
- tokens: z.number().int().nonnegative()
3217
- }).strict();
3218
- const requestRecordSchema = z.object({
3219
- turn: z.number().optional(),
3220
- step: z.number().optional(),
3221
- time: z.number(),
3222
- seq: z.number(),
3223
- system: z.number().int().nonnegative(),
3224
- tools: z.number().int().nonnegative(),
3225
- user: z.number().int().nonnegative(),
3226
- inject: z.number().int().nonnegative(),
3227
- assistant: z.number().int().nonnegative(),
3228
- tool: z.number().int().nonnegative(),
3229
- total: z.number().int().nonnegative(),
3230
- prompt: z.number().int().nonnegative().optional(),
3231
- /**
3232
- * Skill-machinery tokens (issue #66). The fold writes it on every record;
3233
- * optional so rows folded before the category existed still parse.
3234
- */
3235
- skill: z.number().int().nonnegative().optional(),
3236
- cacheRead: z.number().int().nonnegative().optional(),
3237
- output: z.number().int().nonnegative().optional(),
3238
- /**
3239
- * The request's step active milliseconds (see RequestRecord.activeMs).
3240
- * Optional so a record whose step never closed (the live tail) still parses.
3241
- */
3242
- activeMs: z.number().nonnegative().optional(),
3243
- stepCount: z.number().int().positive().optional()
3244
- }).strict();
3245
- const contextEventSchema = z.object({
3246
- seq: z.number(),
3247
- time: z.number(),
3248
- kind: z.enum([
3249
- "compaction",
3250
- "prune",
3251
- "inject",
3252
- "model",
3253
- "mode"
3254
- ]),
3255
- form: z.string().optional(),
3256
- tokens: z.number().optional(),
3257
- count: z.number().optional(),
3258
- sub: z.string().optional(),
3259
- name: z.string().optional(),
3260
- detail: z.string().optional(),
3261
- from: z.string().optional(),
3262
- to: z.string().optional(),
3263
- fromTurn: z.number().optional(),
3264
- fromStep: z.number().optional(),
3265
- turn: z.number().optional(),
3266
- step: z.number().optional()
3267
- }).strict();
3268
- /** The fold-derived file-operation record (shared/types.ts FileOpRecord). */
3269
- const fileOpSchema = z.object({
3270
- seq: z.number().int().nonnegative(),
3271
- path: z.string(),
3272
- kind: z.enum([
3273
- "read",
3274
- "write",
3275
- "search"
3276
- ]),
3277
- tool: z.string(),
3278
- time: z.number().optional(),
3279
- err: z.boolean(),
3280
- added: z.number().int().nonnegative(),
3281
- removed: z.number().int().nonnegative(),
3282
- detail: z.string().optional(),
3283
- hits: z.number().int().positive().optional(),
3284
- read: z.union([z.object({
3285
- start: z.number().int().positive(),
3286
- count: z.number().int().nonnegative()
3287
- }).strict(), z.object({
3288
- count: z.number().int().positive(),
3289
- est: z.literal(true)
3290
- }).strict()]).optional(),
3291
- parent: z.number().int().nonnegative().optional(),
3292
- program: z.string().optional(),
3293
- pattern: z.literal(true).optional(),
3294
- gone: z.number().int().nonnegative().optional()
3295
- }).strict();
3296
- /** One painted span of the timing strip (shared/types.ts TimingSpan). */
3297
- const timingSpanSchema = z.object({
3298
- kind: z.enum([
3299
- "ttft",
3300
- "reasoning",
3301
- "text",
3302
- "toolarg",
3303
- "tools",
3304
- "other"
3305
- ]),
3306
- start: z.number(),
3307
- end: z.number()
3308
- }).strict();
3309
- const currentSchema = z.object({
3310
- system: z.number().int().nonnegative(),
3311
- tools: z.number().int().nonnegative(),
3312
- user: z.number().int().nonnegative(),
3313
- inject: z.number().int().nonnegative(),
3314
- skill: z.number().int().nonnegative(),
3315
- assistant: z.number().int().nonnegative(),
3316
- tool: z.number().int().nonnegative(),
3317
- total: z.number().int().nonnegative()
3318
- }).strict();
3319
- const costBucketsSchema = z.object({
3320
- uncached: z.number().int().nonnegative(),
3321
- cacheRead: z.number().int().nonnegative(),
3322
- cacheWrite: z.number().int().nonnegative(),
3323
- output: z.number().int().nonnegative()
3324
- }).strict();
3325
- const costModelSchema = z.object({
3326
- peak: costBucketsSchema.optional(),
3327
- off: costBucketsSchema.optional()
3328
- }).strict();
3329
- const costModelsSchema = z.record(z.string(), costModelSchema);
3330
- const costUsageSchema = z.record(z.string(), costModelsSchema);
3331
- const toolTimingSchema = z.object({
3332
- calls: z.number().int().nonnegative(),
3333
- ms: z.number().nonnegative()
3334
- }).strict();
3335
- const timingTotalsSchema = z.object({
3336
- wallMs: z.number().nonnegative(),
3337
- ttftMs: z.number().nonnegative(),
3338
- genMs: z.number().nonnegative(),
3339
- reasoningMs: z.number().nonnegative().optional(),
3340
- reasoningBlocks: z.number().int().nonnegative().optional(),
3341
- textMs: z.number().nonnegative().optional(),
3342
- textBlocks: z.number().int().nonnegative().optional(),
3343
- toolArgMs: z.number().nonnegative().optional(),
3344
- toolArgBlocks: z.number().int().nonnegative().optional(),
3345
- speedTokens: z.number().nonnegative().optional(),
3346
- speedMs: z.number().nonnegative().optional(),
3347
- calls: z.number().int().nonnegative(),
3348
- toolsMs: z.number().nonnegative(),
3349
- toolCalls: z.number().int().nonnegative(),
3350
- tools: z.record(z.string(), toolTimingSchema)
3351
- }).strict();
3352
- /** The baseline-gate record the fallback unit serves (see fallback.ts). */
3353
- const unsupportedSchema = z.object({
3354
- current: z.string(),
3355
- minimum: z.string()
3356
- }).strict();
3357
- /** The stats board's precomputed count figures (the split head — see Snapshot.counts). */
3358
- const countsSchema = z.object({
3359
- turns: z.number().int().nonnegative(),
3360
- steps: z.number().int().nonnegative(),
3361
- injects: z.number().int().nonnegative(),
3362
- compactions: z.number().int().nonnegative(),
3363
- prunes: z.number().int().nonnegative()
3364
- }).strict();
3365
- /** The newest retained request's billing summary (the split head's headline anchor). */
3366
- const lastSchema = z.object({
3367
- seq: z.number(),
3368
- total: z.number().int().nonnegative(),
3369
- prompt: z.number().int().nonnegative().optional()
3370
- }).strict();
3371
- /**
3372
- * One wire contract for both generations: the SPLIT head (envelope scalars +
3373
- * counts/last/detailRev; the heavy collections stay absent — they ride the
3374
- * on-demand detail channel, host/detail.ts) and the INLINE value
3375
- * (channel-less hosts and the fallback unit carry the collections in place).
3376
- * The collections are therefore optional on the schema; the split marker is
3377
- * `detailRev` (present ⟺ split).
3378
- */
3379
- const contextTimelineSchema = z.object({
3380
- ok: z.literal(true),
3381
- unsupported: unsupportedSchema.optional(),
3382
- model: z.string().optional(),
3383
- provider: z.string().optional(),
3384
- contextWindow: z.number().optional(),
3385
- current: currentSchema,
3386
- images: z.number().int().nonnegative().optional(),
3387
- toolCalls: z.number().int().nonnegative().optional(),
3388
- humanInputs: z.number().int().nonnegative().optional(),
3389
- lastUser: z.string().optional(),
3390
- counts: countsSchema.optional(),
3391
- last: lastSchema.optional(),
3392
- detailRev: z.number().int().nonnegative().optional(),
3393
- requests: z.array(requestRecordSchema).optional(),
3394
- events: z.array(contextEventSchema).optional(),
3395
- cost: costUsageSchema.optional(),
3396
- timing: timingTotalsSchema.optional(),
3397
- systems: z.array(systemPromptNodeSchema).optional(),
3398
- nodes: z.array(surfaceNodeSchema).optional(),
3399
- droppedNodes: z.number().int().nonnegative().optional(),
3400
- archive: z.array(surfaceNodeSchema).optional(),
3401
- surfaceFloor: z.number().int().nonnegative().optional(),
3402
- archiveFloor: z.number().int().nonnegative().optional(),
3403
- fileOps: z.array(fileOpSchema).optional(),
3404
- fileOpsFloor: z.number().int().nonnegative().optional(),
3405
- spans: z.array(timingSpanSchema).optional()
3406
- }).strict();
3407
- /**
3408
- * The persisted fold-state schema (the registry's `stateSchema`
3409
- * contract). Validates the plain-JSON `TimelineState` before a checkpoint
3410
- * row seeds a fold — the same shape guarantee the projection cache's
3411
- * plain-JSON precondition already enforces at write time.
3412
- */
3413
- const timelineStateSchema = z.object({
3414
- surface: z.array(surfaceNodeSchema),
3415
- sums: z.object({
3416
- user: z.number().int().nonnegative(),
3417
- inject: z.number().int().nonnegative(),
3418
- skill: z.number().int().nonnegative(),
3419
- assistant: z.number().int().nonnegative(),
3420
- tool: z.number().int().nonnegative()
3421
- }).strict(),
3422
- systemTokens: z.number().int().nonnegative(),
3423
- systems: z.array(systemPromptNodeSchema).optional(),
3424
- toolsTokens: z.number().int().nonnegative(),
3425
- model: z.string().optional(),
3426
- provider: z.string().optional(),
3427
- lastModel: z.string().optional(),
3428
- contextWindow: z.number().optional(),
3429
- requests: z.array(requestRecordSchema),
3430
- turnRuns: z.number().int().nonnegative().optional(),
3431
- events: z.array(contextEventSchema),
3432
- archived: z.array(surfaceNodeSchema),
3433
- cost: costUsageSchema.optional(),
3434
- archiveFloor: z.number().optional(),
3435
- timing: timingTotalsSchema.optional(),
3436
- humanInputs: z.number().int().nonnegative().optional(),
3437
- lastUser: z.string().optional(),
3438
- stepStart: z.object({
3439
- time: z.number(),
3440
- firstToken: z.number().optional()
3441
- }).strict().optional(),
3442
- stepWaits: z.array(z.object({
3443
- start: z.number(),
3444
- end: z.number()
3445
- }).strict()).optional(),
3446
- stepApprovals: z.record(z.string(), z.number()).optional(),
3447
- stepRequestSeq: z.number().optional(),
3448
- callNames: z.record(z.string(), z.object({
3449
- name: z.string(),
3450
- start: z.number(),
3451
- argsRaw: z.string().optional()
3452
- }).strict()),
3453
- pendingShadowedSeqs: z.array(z.number()).optional(),
3454
- pendingShadowEventSeq: z.number().optional(),
3455
- detailRev: z.number().int().nonnegative().optional(),
3456
- fileOps: z.array(fileOpSchema),
3457
- fileOpsFloor: z.number().int().nonnegative().optional(),
3458
- spans: z.array(timingSpanSchema),
3459
- stepSpans: z.array(timingSpanSchema).optional(),
3460
- pendingCodeOps: z.record(z.string(), z.array(fileOpSchema)).optional()
3461
- });
3462
- /**
3463
- * The context-timeline projection unit, created per plugin instance with its
3464
- * config-resolved retention bounds (config.ts), and registered on
3465
- * `ctx.sessionProjections`. Registry lifecycle notes (mirrored from the
3466
- * harness contract): registration is an effect on the caller's fiber — an
3467
- * unloaded Host half removes the key, and clients read it as capability
3468
- * absence. `stateVersion` must be bumped whenever the persisted state shape
3469
- * or fold semantics change (invalidation of cached rows); config-only
3470
- * changes never require it (bounds tune retention, not state shape).
3471
- *
3472
- * The definition carries the session-projection contract served by every
3473
- * supported harness (see compat.ts): `stateSchema` + a REQUIRED `wire` block.
3474
- * (The return type is the mirrored contract, not the installed dts
3475
- * `ProjectionDefinition`: the registry's wired-register overload demands
3476
- * `wire` PRESENT, which the dts's optional `wire?` fails.) Without the
3477
- * `wire` block the registry treats the unit as host-only and never delivers
3478
- * `contextTimeline` to the browser (the Context tab would stay on its
3479
- * loading screen forever).
3480
- *
3481
- * `slim` selects the wire generation PER SERVE (a liveness probe, not a
3482
- * fixed flag): while the on-demand detail channel is live (host/detail.ts),
3483
- * the wire value is the SLIM head (buildTimelineHead) — the heavy
3484
- * collections no longer ride every session.list row, control baseline,
3485
- * follow snapshot, and push frame. Before the channel arms (the connection
3486
- * service may activate after this plugin) or on a deployment whose
3487
- * connection/sessions services never compose, the unit serves the INLINE
3488
- * value so the tab keeps working end to end. Both generations validate
3489
- * against the same schema (the collections are optional on it), and both
3490
- * fold the SAME state — the split is view-only, so no `stateVersion` bump
3491
- * and no cached-row invalidation comes with it (the `detailRev` state field
3492
- * is additive-optional: older rows restore without it and read as revision
3493
- * 0).
3494
- */
3495
- function createContextTimelineDefinition(config, slim) {
3496
- const bounds = resolveBounds(config);
3497
- const view = (state) => slim() ? buildTimelineHead(state) : buildTimelineView(state, bounds);
3498
- return {
3499
- key: "contextTimeline",
3500
- stateSchema: timelineStateSchema,
3501
- wire: {
3502
- viewSchema: contextTimelineSchema,
3503
- view
3504
- },
3505
- init: () => createTimelineState(),
3506
- apply: (state, event) => applyTimeline(state, event, bounds),
3507
- stateVersion: 26
3508
- };
3509
- }
3510
- //#endregion
3511
2700
  //#region src/host/fallback.ts
3512
- /**
3513
- * The baseline gate's fallback projection units (see host/index.ts).
3514
- *
3515
- * On a harness below the supported baseline (shared/version.ts) the plugin
3516
- * registers these INSTEAD of the real folds: the log's event shapes on such
3517
- * a harness are outside the compat matrix, so nothing is parsed at all —
3518
- * `apply` is the identity over an opaque empty state, and `view` serves a
3519
- * fixed value whose timeline snapshot carries the gate record (`unsupported`:
3520
- * the detected harness version and the baseline). The client's cards render
3521
- * the blank data and its gate modal urges the upgrade; the registry pipeline
3522
- * (fold, cache, push feed) keeps working end to end, so nothing hangs on a
3523
- * loading screen.
3524
- *
3525
- * The definition carries BOTH registry contract generations: the modern
3526
- * `stateSchema` + `wire` block (dsh 0.1.1-rc.1+, the realistic below-baseline
3527
- * case) and the pre-0.1.1 top-level `schema` + `view` aliases that line's
3528
- * registry reads instead. Each registry ignores the other generation's
3529
- * fields, so one definition serves the gate on every harness that can
3530
- * deliver projections to clients at all.
3531
- *
3532
- * `stateVersion` is pinned at 1. Downgrade/upgrade cache choreography: a
3533
- * downgrade to a gated harness refolds the timeline key from scratch (no
3534
- * ver-1 rows exist for it) and seeds the headers key from the real unit's
3535
- * ver-1 rows — stripped to the empty state, which the gate never reads. An
3536
- * upgrade back discards the fallback's ver-1 timeline rows and REJECTS its
3537
- * ver-1 headers rows at the real unit's stricter state schema, refolding
3538
- * both from the log. Nothing stale survives in either direction.
3539
- */
3540
- /**
3541
- * The opaque fold state: the gate folds nothing, so any cached row seeds it
3542
- * (strip-mode object — never a discard, never a throw), and the plain-JSON
3543
- * cache-write gate trivially holds.
3544
- */
3545
2701
  const fallbackStateSchema = z.object({});
3546
- /** One gate unit: identity fold over the opaque state, constant view, both contract generations. */
3547
2702
  function fallbackDefinition(key, wireSchema, value) {
3548
2703
  const view = () => value;
3549
2704
  return {
@@ -3560,10 +2715,6 @@ function fallbackDefinition(key, wireSchema, value) {
3560
2715
  stateVersion: 1
3561
2716
  };
3562
2717
  }
3563
- /**
3564
- * The fallback `contextTimeline` unit: serves the fixed zeroed snapshot
3565
- * naming the detected harness `current` version against the baseline.
3566
- */
3567
2718
  function createFallbackTimelineDefinition(current) {
3568
2719
  return fallbackDefinition("contextTimeline", contextTimelineSchema, {
3569
2720
  ok: true,
@@ -3588,103 +2739,20 @@ function createFallbackTimelineDefinition(current) {
3588
2739
  archive: []
3589
2740
  });
3590
2741
  }
3591
- /**
3592
- * The fallback `contextHeaders` unit: an empty epoch list — the browser's
3593
- * header sections degrade to their metadata-free rendering.
3594
- */
3595
2742
  function createFallbackHeadersDefinition() {
3596
2743
  return fallbackDefinition("contextHeaders", contextHeadersSchema, { headers: [] });
3597
2744
  }
3598
- /**
3599
- * The fallback `contextActivity` unit: an empty ledger — the overview's
3600
- * heatmap renders its empty note instead of waiting on a key that the gate
3601
- * would never deliver.
3602
- */
3603
2745
  function createFallbackActivityDefinition() {
3604
2746
  return fallbackDefinition("contextActivity", contextActivitySchema, { days: {} });
3605
2747
  }
3606
2748
  //#endregion
3607
- //#region src/host/settings.ts
3608
- /** The namespace is the join key between the Host registration and the browser card. */
3609
- const SETTINGS_NAMESPACE = "dsh-context";
3610
- /** Section schema: also the wire envelope the browser scope validates against. */
3611
- const SettingsSchema = z$1.object({
3612
- defaultPlacement: z$1.union([
3613
- "all",
3614
- "tab",
3615
- "sidebar"
3616
- ]).default("all").loose(),
3617
- defaultGranularity: z$1.union(["step", "turn"]).default("step"),
3618
- defaultTrendMode: z$1.union(["total", "delta"]).default("total").loose(),
3619
- defaultDeltaBase: z$1.union(["step", "turn"]).default("step").loose(),
3620
- defaultToolSort: z$1.union([
3621
- "size",
3622
- "count",
3623
- "name"
3624
- ]).default("count").loose(),
3625
- defaultFileSort: z$1.union([
3626
- "count",
3627
- "latest",
3628
- "path"
3629
- ]).default("count").loose(),
3630
- insightsEntry: z$1.union(["show", "hide"]).default("show").loose(),
3631
- defaultDurationCurve: z$1.union(["show", "hide"]).default("show").loose()
3632
- });
3633
- /** Serve the namespace while a settings provider with the register face is composed; inert otherwise. */
3634
- function installSettings(ctx) {
3635
- ctx.inject(["settings"], (sctx) => {
3636
- const service = sctx.settings;
3637
- if (typeof service.register !== "function") return;
3638
- service.register(SETTINGS_NAMESPACE, SettingsSchema);
3639
- });
3640
- }
3641
- //#endregion
3642
2749
  //#region src/host/stepIdentity.ts
3643
- /**
3644
- * Step-boundary message identity guard (issue #51 compatibility).
3645
- *
3646
- * The harness refuses to LOAD any session whose durable log carries a
3647
- * `user/message` event whose message lacks a non-empty string `id`
3648
- * (`assertMessageEventShape`) — while the runtime append path runs no such
3649
- * check. One unidentified message therefore persists silently and permanently
3650
- * bricks the session at its next load (`... failed validation: session event
3651
- * at seq N lacks an identified message`). The observed archive held a wrap-up
3652
- * notice whose `id` (and `summary`) had been stripped upstream of the append —
3653
- * a shape no shipped harness producer emits, i.e. a delivery-boundary rebuild
3654
- * outside the audited paths of both this plugin and the harness.
3655
- *
3656
- * This guard hardens the durability boundary from the plugin side. The agent
3657
- * loop appends every step-boundary `user/message` from the `agent/pre-step`
3658
- * decision's `messages` — the one seam all claimed inbox input (prompts,
3659
- * steering, tool-deferred context) flows through before it persists. A
3660
- * prepended listener sits OUTERMOST in that waterfall, so after `next()`
3661
- * resolves it sees the final message list: any message that would persist
3662
- * unidentified gets a fresh id; everything else passes by reference, and the
3663
- * decision object is copied only when a mint happened.
3664
- *
3665
- * Fail-open by contract: a hostile entry (one that throws on property access),
3666
- * a missing list, or any unexpected shape leaves the decision verbatim — a
3667
- * guard must never break the turn it protects. Minted ids carry the `dshctx-`
3668
- * prefix so a backfilled message stays identifiable in the wild.
3669
- */
3670
- /** Prefix marking an id this guard minted (forensically distinguishable). */
3671
2750
  const MINTED_ID_PREFIX = "dshctx-";
3672
- /**
3673
- * Whether the message fails the harness's restore-time identity check (a
3674
- * non-empty string id). Non-object entries are unfixable (an id needs a
3675
- * container) and stay verbatim.
3676
- */
3677
2751
  function lacksId(message) {
3678
2752
  if (typeof message !== "object" || message === null) return false;
3679
2753
  const id = message.id;
3680
2754
  return typeof id !== "string" || id === "";
3681
2755
  }
3682
- /**
3683
- * Mint ids for the messages that would persist unidentified.
3684
- * @param messages - the decision's message list, in append order.
3685
- * @returns the rewritten list (untouched entries by reference), or undefined
3686
- * when every entry already carries an identity.
3687
- */
3688
2756
  function identifiedMessages(messages) {
3689
2757
  let copy;
3690
2758
  for (const [index, message] of messages.entries()) {
@@ -3700,7 +2768,6 @@ function identifiedMessages(messages) {
3700
2768
  }
3701
2769
  return copy;
3702
2770
  }
3703
- /** Arm the prepended `agent/pre-step` guard on the plugin's context. */
3704
2771
  function watchStepIdentity(ctx) {
3705
2772
  ctx.on("agent/pre-step", async (_input, next) => {
3706
2773
  const decision = await next();
@@ -3717,80 +2784,155 @@ function watchStepIdentity(ctx) {
3717
2784
  }, { prepend: true });
3718
2785
  }
3719
2786
  //#endregion
2787
+ //#region src/host/skills.ts
2788
+ const SKILLS_ROUTE = "/api/dsh-context/skills";
2789
+ const MAX_SKILLS = 500;
2790
+ function asRecord(value) {
2791
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2792
+ }
2793
+ function stringOf(value) {
2794
+ return typeof value === "string" && value !== "" ? value : void 0;
2795
+ }
2796
+ async function disposeOf(resource) {
2797
+ if (resource === null || typeof resource !== "object" && typeof resource !== "function") return;
2798
+ try {
2799
+ const asyncDispose = resource[Symbol.asyncDispose];
2800
+ if (typeof asyncDispose === "function") {
2801
+ await asyncDispose.call(resource);
2802
+ return;
2803
+ }
2804
+ const syncDispose = resource[Symbol.dispose];
2805
+ if (typeof syncDispose === "function") syncDispose.call(resource);
2806
+ } catch {}
2807
+ }
2808
+ function skillInfoOf(value) {
2809
+ const record = asRecord(value);
2810
+ if (record === null || typeof record.name !== "string" || record.name === "") return null;
2811
+ return {
2812
+ name: record.name,
2813
+ description: typeof record.description === "string" ? record.description : "",
2814
+ ...typeof record.path === "string" && record.path !== "" ? { path: record.path } : {},
2815
+ ...typeof record.source === "string" && record.source !== "" ? { source: record.source } : {}
2816
+ };
2817
+ }
2818
+ async function sessionViewOf(ctx, sessionId) {
2819
+ if (sessionId === void 0) return {};
2820
+ const query = asRecord(ctx.get("sessionQuery"));
2821
+ if (typeof query?.observeSession !== "function") return {};
2822
+ let observation;
2823
+ try {
2824
+ observation = await query.observeSession.call(query, sessionId);
2825
+ } catch {
2826
+ return {};
2827
+ }
2828
+ try {
2829
+ const obs = asRecord(observation);
2830
+ const cwd = stringOf(asRecord(obs?.header)?.cwd);
2831
+ const agentPreset = stringOf(asRecord(asRecord(obs?.projections)?.values)?.agentPreset);
2832
+ return {
2833
+ ...cwd === void 0 ? {} : { cwd },
2834
+ ...agentPreset === void 0 ? {} : { agentPreset }
2835
+ };
2836
+ } finally {
2837
+ await disposeOf(observation);
2838
+ }
2839
+ }
2840
+ async function resolveRegistry(ctx, sessionId, agentPreset) {
2841
+ const presets = asRecord(ctx.get("agentPresets"));
2842
+ let live;
2843
+ if (sessionId !== void 0) {
2844
+ const agents = asRecord(ctx.get("agents"));
2845
+ if (typeof agents?.get === "function") try {
2846
+ live = agents.get.call(agents, sessionId);
2847
+ } catch {
2848
+ live = void 0;
2849
+ }
2850
+ }
2851
+ if (live !== void 0 && live !== null && typeof presets?.serviceFor === "function") {
2852
+ const scoped = asRecord(presets.serviceFor.call(presets, live, "skills"));
2853
+ if (scoped !== null && typeof scoped.list === "function") return {
2854
+ registry: scoped,
2855
+ scope: live,
2856
+ lease: void 0
2857
+ };
2858
+ }
2859
+ const registry = asRecord(ctx.get("skills"));
2860
+ if (registry === null || typeof registry.list !== "function") return null;
2861
+ let scope;
2862
+ let lease;
2863
+ if (typeof presets?.acquireScope === "function") try {
2864
+ lease = await presets.acquireScope.call(presets, agentPreset);
2865
+ scope = asRecord(lease)?.key;
2866
+ } catch {
2867
+ lease = void 0;
2868
+ }
2869
+ return {
2870
+ registry,
2871
+ scope,
2872
+ lease
2873
+ };
2874
+ }
2875
+ function watchSkillCatalog(ctx) {
2876
+ ctx.inject(["connection"], (c) => {
2877
+ const register = fetchRouteRegistrar(c.get("connection"));
2878
+ if (register === void 0) return;
2879
+ const handler = async (request) => {
2880
+ let value = null;
2881
+ let lease;
2882
+ try {
2883
+ const body = asRecord(await request.json());
2884
+ const sessionId = stringOf(body?.sessionId);
2885
+ const view = await sessionViewOf(ctx, sessionId);
2886
+ const cwd = view.cwd ?? stringOf(body?.cwd);
2887
+ const resolved = await resolveRegistry(ctx, sessionId, view.agentPreset);
2888
+ if (resolved !== null) {
2889
+ lease = resolved.lease;
2890
+ const listed = await resolved.registry.list({
2891
+ ...cwd === void 0 ? {} : { cwd },
2892
+ ...resolved.scope === void 0 ? {} : { scope: resolved.scope }
2893
+ });
2894
+ const rows = Array.isArray(listed) ? listed : [];
2895
+ const skills = [];
2896
+ for (const row of rows) {
2897
+ const info = skillInfoOf(row);
2898
+ if (info !== null) skills.push(info);
2899
+ if (skills.length >= MAX_SKILLS) break;
2900
+ }
2901
+ value = { skills };
2902
+ }
2903
+ } catch {} finally {
2904
+ await disposeOf(lease);
2905
+ }
2906
+ return Response.json({
2907
+ ok: true,
2908
+ value
2909
+ }, { headers: { "cache-control": "no-store" } });
2910
+ };
2911
+ try {
2912
+ c.effect(() => register({
2913
+ path: SKILLS_ROUTE,
2914
+ methods: ["POST"],
2915
+ requestBody: "buffered",
2916
+ fetch: handler
2917
+ }), "dsh-context: skills route");
2918
+ } catch {
2919
+ return;
2920
+ }
2921
+ });
2922
+ }
2923
+ //#endregion
3720
2924
  //#region src/host/version.ts
3721
- /**
3722
- * Runtime harness-version probe behind the baseline gate (host/index.ts).
3723
- *
3724
- * The probe never imports a harness package for its version; it reads
3725
- * manifests through Node resolution from two anchors, in order:
3726
- *
3727
- * 1. This module's own ESM resolution of the library packages the Host half
3728
- * imports at runtime. That is the very pipeline the process resolves those
3729
- * imports through, so an embedding shell that redirects plugin imports —
3730
- * the packaged Desktop's ASAR resolver bridge does exactly this — redirects
3731
- * the probe identically, and the answer names the module instances the
3732
- * harness is actually running. A witness that lands inside this package's
3733
- * own tree is the plugin's dependency closure (a `link:`-installed dev
3734
- * checkout's pinned devDependencies), never the harness, so it is discarded
3735
- * for the home anchor.
3736
- * 2. The harness home's healed `profiles/node_modules` mirror (located via
3737
- * the app-boot `dshHomePath` service). It names the installation that last
3738
- * healed it: authoritative for a dev checkout, whose own tree holds pinned
3739
- * devDependencies, but only as fresh as the last CLI profile boot. A
3740
- * packaged Desktop never heals it, so a stale global-CLI mirror can outlive
3741
- * the CLI and misname the running harness — that is why it answers only
3742
- * when the running anchor has nothing it can trust.
3743
- *
3744
- * The `@deepseek-ai/dsh` CLI package is probed ONLY through the home anchor:
3745
- * the plugin never imports it, so a hit from the running anchor can only be an
3746
- * ambient install above the plugin's tree (e.g. a global copy under
3747
- * ~/node_modules) — not necessarily the RUNNING harness.
3748
- *
3749
- * Every step is guarded: any failure (absent service, unresolvable package,
3750
- * non-file URL, unreadable/invalid manifest, non-string version) degrades to
3751
- * `undefined`, and the gate treats an unknown version as SATISFIED — a probe
3752
- * misfire must never blank a working deployment.
3753
- */
3754
- /**
3755
- * Library packages whose manifest version IS the harness release version (the
3756
- * dsh monorepo versions every package in lockstep). The home anchor probes
3757
- * them after the CLI package; the running anchor probes the two first- and
3758
- * second-ordered.
3759
- */
3760
2925
  const LIBRARY_PROBE_PACKAGES = ["@deepseek-ai/dsh-session-projection", "@deepseek-ai/dsh-session"];
3761
- /**
3762
- * Running-anchor probe order: the package the Host half genuinely imports at
3763
- * runtime first (host/fold.ts imports it, so it MUST resolve whenever this
3764
- * plugin runs), then its co-versioned sibling.
3765
- */
3766
2926
  const RUNNING_PROBE_PACKAGES = ["@deepseek-ai/dsh-session", "@deepseek-ai/dsh-session-projection"];
3767
- /** Home-anchor probe order: the user-facing CLI version first, then the libraries. */
3768
2927
  const HOME_PROBE_PACKAGES = ["@deepseek-ai/dsh", ...LIBRARY_PROBE_PACKAGES];
3769
- /**
3770
- * This package's root. A running-anchor witness under it belongs to the
3771
- * plugin's own dependency closure, not to the harness.
3772
- *
3773
- * Located through the package's own `./package.json` self-reference: Node
3774
- * resolves that against the nearest package.json, so the answer is the package
3775
- * root in the source tree and in the bundled profile install alike — a fixed
3776
- * number of `..` steps cannot be, since `src/host/` and `lib/` differ by a
3777
- * level. A loader that cannot self-resolve (an exotic loader, or a fork renamed
3778
- * without a matching `exports`) falls back to this module's own directory: the
3779
- * guard then covers less of the package, but it still can never mistake the
3780
- * plugin's own `node_modules` for the harness.
3781
- */
3782
2928
  function ownPackageRoot() {
3783
2929
  try {
3784
2930
  return dirname(fileURLToPath(import.meta.resolve("dsh-context/package.json")));
3785
2931
  } catch {
3786
- /* v8 ignore next -- a loader that cannot resolve one of its own package's
3787
- published subpaths is out of the suite's reach; the fallback keeps one
3788
- probe path (an unanswered probe fails open either way). */
3789
2932
  return dirname(fileURLToPath(import.meta.url));
3790
2933
  }
3791
2934
  }
3792
2935
  const PLUGIN_ROOT = ownPackageRoot();
3793
- /** One manifest's `version`, or undefined on any read/shape failure. */
3794
2936
  function versionOfManifest(manifestPath, expectedName) {
3795
2937
  try {
3796
2938
  const parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
@@ -3802,7 +2944,6 @@ function versionOfManifest(manifestPath, expectedName) {
3802
2944
  return;
3803
2945
  }
3804
2946
  }
3805
- /** Probe the package's published `./package.json` subpath. */
3806
2947
  function versionViaManifest(resolve, packageName) {
3807
2948
  try {
3808
2949
  return versionOfManifest(resolve(packageName + "/package.json"));
@@ -3810,12 +2951,6 @@ function versionViaManifest(resolve, packageName) {
3810
2951
  return;
3811
2952
  }
3812
2953
  }
3813
- /**
3814
- * Probe via the package's entry point, ascending to its owning manifest.
3815
- * Covers the packaged-executable module proxies: their generated manifests
3816
- * carry the real version but export only entry stubs, and Node's exports
3817
- * gate refuses the direct `./package.json` subpath.
3818
- */
3819
2954
  function versionViaEntry(resolve, packageName) {
3820
2955
  let dir;
3821
2956
  try {
@@ -3831,33 +2966,16 @@ function versionViaEntry(resolve, packageName) {
3831
2966
  dir = parent;
3832
2967
  }
3833
2968
  }
3834
- /** One anchor's answer: the first probe package that yields a version. */
3835
2969
  function probeAnchor(resolve, packageNames) {
3836
2970
  for (const packageName of packageNames) {
3837
2971
  const version = versionViaManifest(resolve, packageName) ?? versionViaEntry(resolve, packageName);
3838
2972
  if (version !== void 0) return version;
3839
2973
  }
3840
2974
  }
3841
- /**
3842
- * Whether `candidate` lies inside `root`'s tree. A `..`-prefixed relative path
3843
- * escapes it; a sibling such as `dsh-context-extra` is reached through `..` too,
3844
- * so it never counts as a child. On Windows, a cross-drive `relative()` returns
3845
- * the candidate's ABSOLUTE path (no `..` prefix) — a different drive is never
3846
- * inside this tree, so the absolute answer is rejected the same way. A
3847
- * pathological child literally named `..foo` would degrade to the home anchor,
3848
- * which is the safe direction.
3849
- */
3850
2975
  function isInside(root, candidate) {
3851
2976
  const rel = relative(root, candidate);
3852
2977
  return !rel.startsWith("..") && !isAbsolute(rel);
3853
2978
  }
3854
- /**
3855
- * The running module tree's answer, or undefined when it cannot be trusted.
3856
- * Each probe package is checked on its own: a witness inside the plugin's own
3857
- * closure is the plugin's dependency, not the harness, and a trusted witness
3858
- * whose manifest cannot be read is no answer either — the next package may
3859
- * still resolve to one.
3860
- */
3861
2979
  function probeRunningTree(resolve, pluginRoot) {
3862
2980
  for (const packageName of RUNNING_PROBE_PACKAGES) {
3863
2981
  let witness;
@@ -3871,16 +2989,9 @@ function probeRunningTree(resolve, pluginRoot) {
3871
2989
  if (version !== void 0) return version;
3872
2990
  }
3873
2991
  }
3874
- /** The default running anchor: this module's own ESM resolution. */
3875
2992
  function resolveRunningModule(specifier) {
3876
2993
  return import.meta.resolve(specifier);
3877
2994
  }
3878
- /**
3879
- * The running harness's version string, or undefined when nothing answers
3880
- * (the gate fails open on it — see the header note).
3881
- * @param resolveUrl - the running anchor's URL resolver (injectable for hermetic tests).
3882
- * @param pluginRoot - this package's root for the own-closure check (injectable for hermetic tests).
3883
- */
3884
2995
  function detectHarnessVersion(ctx, resolveUrl = resolveRunningModule, pluginRoot = PLUGIN_ROOT) {
3885
2996
  const running = probeRunningTree((specifier) => {
3886
2997
  const url = resolveUrl(specifier);
@@ -3903,6 +3014,7 @@ const name = "dsh-context";
3903
3014
  const inject = ["sessionProjections"];
3904
3015
  function apply(ctx, config) {
3905
3016
  watchBalanceChannel(ctx);
3017
+ watchSkillCatalog(ctx);
3906
3018
  const harnessVersion = detectHarnessVersion(ctx);
3907
3019
  if (harnessVersion !== void 0 && !meetsBaseline(harnessVersion)) {
3908
3020
  ctx.sessionProjections.register(createFallbackTimelineDefinition(harnessVersion));
@@ -3912,12 +3024,12 @@ function apply(ctx, config) {
3912
3024
  }
3913
3025
  const attribution = createToolAttribution(ctx);
3914
3026
  watchStepIdentity(ctx);
3915
- const gate = watchDetailChannel(ctx, resolveBounds(config));
3027
+ const coldReads = makeColdReadGate();
3028
+ const gate = watchDetailChannel(ctx, resolveBounds(config), coldReads);
3916
3029
  ctx.sessionProjections.register(createContextTimelineDefinition(config, () => gate.live));
3917
3030
  ctx.sessionProjections.register(createContextHeadersDefinition((name) => attribution.ownerOf(name)));
3918
3031
  ctx.sessionProjections.register(createContextActivityDefinition());
3919
- watchActivityBackfill(ctx);
3920
- installSettings(ctx);
3032
+ watchActivityBackfill(ctx, coldReads);
3921
3033
  }
3922
3034
  //#endregion
3923
3035
  export { Config, apply, inject, name };