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