dsh-context 0.46.1 → 0.48.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.d.ts CHANGED
@@ -56,6 +56,15 @@ interface HeadersState {
56
56
  headers: StoredHeaderRecord[];
57
57
  }
58
58
  //#endregion
59
+ //#region src/host/logShapes.d.ts
60
+ /**
61
+ * The decode bucket a stream's `block-start.blockType` names: the model's
62
+ * thinking, the answer text, or the tool-call arguments. Undefined for an
63
+ * unknown/hostile marker, whose interval then stays unattributed rather than
64
+ * poisoning a bucket.
65
+ */
66
+ type DecodeKind = 'reasoning' | 'text' | 'toolarg';
67
+ //#endregion
59
68
  //#region src/host/fold.d.ts
60
69
  /**
61
70
  * History retention bounds (configurable since 0.11 — see config.ts; these
@@ -72,6 +81,27 @@ interface TimelineState {
72
81
  surface: SurfaceNode[];
73
82
  sums: Record<Category, number>;
74
83
  systemTokens: number;
84
+ /**
85
+ * The live system-prompt nodes, oldest first — a V3 log's `system/message`
86
+ * surface nodes, or the single entry a V0/V2 `request/header.header.system`
87
+ * envelope defines. `systemTokens` is the LAST entry with tokens > 0 (the
88
+ * harness's own "last nonempty surviving system" rule), so an empty dormant
89
+ * node keeps its position without clearing the prompt. Bounded by
90
+ * SYSTEM_NODES_MAX. ABSENT on rows folded before this field existed — the
91
+ * wire then serves no `systems` and the client falls back to the header
92
+ * epoch's own envelope figure.
93
+ */
94
+ systems?: SystemPromptNode[];
95
+ /**
96
+ * Whether `systems` was built from the V0/V2 request ENVELOPE
97
+ * (`header.system`) rather than from V3 `system/message` events. Only then
98
+ * may a system-less header CLEAR the list: its canonical V0 meaning is
99
+ * "this request has no system prompt", while a V3 header never carries one
100
+ * (its prompt lives in the message history). Absent = log-sourced, and
101
+ * never materialized as an `undefined`-valued property (plain-JSON
102
+ * precondition — see the note above `model`).
103
+ */
104
+ systemsFromHeader?: true;
75
105
  toolsTokens: number;
76
106
  /**
77
107
  * The projection-cache precondition is plain JSON: a property whose value
@@ -132,10 +162,21 @@ interface TimelineState {
132
162
  * one those events close — a hostile interleaved log degrades to skipped
133
163
  * durations, never to unbounded state. Same arm/remove lifecycle as
134
164
  * `pendingShadowedSeqs`.
165
+ *
166
+ * `decode` and `block` carry the generation split (reasoning / answer text /
167
+ * tool arguments — see TimingTotals): a V0 log's `assistant/chunk`
168
+ * `block-start` markers open `block` and close the previous one into
169
+ * `decode`; a V2+ log carries no such events, so `decode` stays absent and
170
+ * `assistant/message` reads the spans off its embedded stream instead.
135
171
  */
136
172
  stepStart?: {
137
173
  time: number;
138
174
  firstToken?: number;
175
+ decode?: Record<DecodeKind, number>;
176
+ block?: {
177
+ kind: DecodeKind;
178
+ since: number;
179
+ };
139
180
  };
140
181
  /**
141
182
  * Tool callId → the call's name, start instant, and raw arguments, armed by
@@ -221,6 +262,19 @@ declare module '@deepseek-ai/dsh-session-projection/types' {
221
262
  }
222
263
  }
223
264
  type Category = 'user' | 'inject' | 'assistant' | 'tool';
265
+ /**
266
+ * One live system-prompt node (Snapshot.systems) — the harness models the
267
+ * system prompt as a surface node, so its TEXT is fetched on demand from the
268
+ * event at `seq`: a V3 `system/message` event, or the V0/V2 `request/header`
269
+ * whose envelope carried `header.system`. `tokens` is the node's heuristic
270
+ * price (0 for a dormant empty node, which the harness reads as "no system
271
+ * prompt"); the effective figure is the LAST node with `tokens > 0`.
272
+ */
273
+ interface SystemPromptNode {
274
+ seq: number;
275
+ time: number;
276
+ tokens: number;
277
+ }
224
278
  /**
225
279
  * The stats board's count figures, precomputed host-side over the RETAINED
226
280
  * request/event records (the same set the detail payload serves). Carried by
@@ -316,6 +370,13 @@ interface Snapshot {
316
370
  * one — clients treat absence as an empty timing card).
317
371
  */
318
372
  timing?: TimingTotals;
373
+ /**
374
+ * The live system-prompt nodes, oldest first — the browser's per-step source
375
+ * for the System section. Absent when the log carried no system prompt, and
376
+ * on older plugin builds (the client then falls back to the header epoch's
377
+ * own `systemTokens`, the pre-V3 shape).
378
+ */
379
+ systems?: SystemPromptNode[];
319
380
  /**
320
381
  * The served live surface: the newest `maxNodes` tail PLUS every live inject node older than the tail (injections land first and are
321
382
  * few,
@@ -450,14 +511,30 @@ interface ToolTimingTotals {
450
511
  }
451
512
  /**
452
513
  * Whole-session timing totals, host-folded from the durable `step/start` /
453
- `step/end` / `assistant/chunk` / `tool/call` / `tool/result` lifecycle
454
- * (running totals over the COMPLETE session log — the same never-trimmed
455
- * framing as `cost`). Durations are wall-clock milliseconds: `wallMs` sums
514
+ * `step/end` / `tool/call` / `tool/result` lifecycle plus the model call's
515
+ * first token (running totals over the COMPLETE session log — the same
516
+ * never-trimmed framing as `cost`). The first token comes from a V0
517
+ * `assistant/chunk` delta or from the call's own embedded stream
518
+ * (`assistant/message.data.stream` / `assistant/attempt.data.stream`, the
519
+ * V2+ settlement) — whichever the log carries, matching the harness's own
520
+ * session-stats fold. Durations are wall-clock milliseconds: `wallMs` sums
456
521
  * whole steps, `ttftMs` the step-start → first-token slice (the model wait)
457
522
  * and `genMs` the first-token → assistant-message slice (the generation) —
458
523
  * both only over calls whose stream carried a token delta, `toolsMs` the sum
459
524
  * of per-call tool durations (parallel calls each count, so it can overlap).
460
525
  * Absent until the first step lifecycle completes in the log.
526
+ *
527
+ * The generation window itself splits by WHAT was being decoded, off the
528
+ * stream's `block-start` framing (`blockType`): `reasoningMs` (the model's
529
+ * thinking), `textMs` (the answer text), and `toolArgMs` (the tool-call
530
+ * arguments). Each marker owns the interval up to the next one (the last one
531
+ * up to the assistant message), so the three tile the marker span and together
532
+ * account for essentially all of `genMs` — the span opens at the first marker,
533
+ * which can sit marginally before the first token, so it is not an exact
534
+ * partition. They are ADDITIVE-OPTIONAL: cached projection rows written before
535
+ * the split carry `genMs` without them, so the card falls back to the
536
+ * un-split shape instead of the cache row being discarded (the
537
+ * stateVersion-15 rationale in host/timeline.ts).
461
538
  */
462
539
  interface TimingTotals {
463
540
  /** Summed wall time of completed steps (the session's active time). */
@@ -466,6 +543,12 @@ interface TimingTotals {
466
543
  ttftMs: number;
467
544
  /** Summed first-token → assistant-message time (the generation). */
468
545
  genMs: number;
546
+ /** Reasoning-decode slice of `genMs` (the model's thinking). */
547
+ reasoningMs?: number;
548
+ /** Answer-text decode slice of `genMs`. */
549
+ textMs?: number;
550
+ /** Tool-call-argument decode slice of `genMs`. */
551
+ toolArgMs?: number;
469
552
  /** Completed model calls (assistant messages folded). */
470
553
  calls: number;
471
554
  /** Summed per-call durations of completed tool calls. */
@@ -485,7 +568,7 @@ interface CostFamilyUsage {
485
568
  * token totals per DeepSeek V4 model family (matched on the model NAME,
486
569
  * provider-agnostic) and pricing period. The Client prices these with its
487
570
  * hardcoded list-price table in the locale's currency. Absent until a
488
- * deepseek-v4-flash / deepseek-v4-pro request reports usage.
571
+ * deepseek-v4.1-flash / deepseek-v4-pro request reports usage.
489
572
  */
490
573
  interface SessionCostUsage {
491
574
  flash?: CostFamilyUsage;
package/lib/index.js CHANGED
@@ -308,6 +308,30 @@ function estimateSystemTokens(text) {
308
308
  if (typeof text !== "string" || text.length === 0) return 0;
309
309
  return Math.ceil(text.length / CHARS_PER_TOKEN$1) + ROLE_OVERHEAD$1;
310
310
  }
311
+ /**
312
+ * Price a `system/message` payload's content exactly like the harness's
313
+ * token-meter (`estimateSystemMessage`): text density over EVERY text block
314
+ * plus role framing, with no per-block overhead — an adapter serializes the
315
+ * prompt as plain text, so a text block costs its characters alone. Any other
316
+ * block (or a hostile element) falls back to its JSON length. 0 for empty
317
+ * content, which the harness reads as "no system prompt".
318
+ */
319
+ function estimateSystemContent(blocks) {
320
+ if (!Array.isArray(blocks) || blocks.length === 0) return 0;
321
+ let characters = 0;
322
+ for (const block of blocks) {
323
+ const text = block !== null && typeof block === "object" && block.type === "text" ? block.text : void 0;
324
+ if (typeof text === "string") {
325
+ characters += text.length;
326
+ continue;
327
+ }
328
+ try {
329
+ const json = JSON.stringify(block);
330
+ if (typeof json === "string") characters += json.length;
331
+ } catch {}
332
+ }
333
+ return Math.ceil(characters / CHARS_PER_TOKEN$1) + ROLE_OVERHEAD$1;
334
+ }
311
335
  //#endregion
312
336
  //#region src/shared/imageTokens.ts
313
337
  /**
@@ -565,6 +589,150 @@ function isInjection(source) {
565
589
  return source !== null && source !== void 0 && (typeof source.kind === "string" && source.kind !== "" && source.kind !== "user" || typeof source.form === "string");
566
590
  }
567
591
  //#endregion
592
+ //#region src/host/logShapes.ts
593
+ /**
594
+ * Shape-driven readers over the durable session-event vocabulary — the ONE
595
+ * place the plugin reconciles the two supported log generations:
596
+ *
597
+ * - V0 (dsh 0.1.2-rc.1): `request/header.header.system`, `assistant/chunk`
598
+ * stream events, `SurfaceOp { start, end }`, `tool/code-dispatch`.
599
+ * - V3 (dsh 0.1.5-alpha.x+): `system/message` surface nodes,
600
+ * `assistant/message.data.stream` / `assistant/attempt.data.stream`,
601
+ * `SurfaceOp { startSeq, endSeq }`, `tool/ptc-dispatch`.
602
+ *
603
+ * The fold reads SHAPES, never a detected harness version: a session log is
604
+ * written by exactly one generation, the two spellings are mutually
605
+ * exclusive within it, and a deployment's version probe can be wrong (a
606
+ * healed profile mirror may name a different release than the running
607
+ * harness). Every reader is total over untrusted input — a malformed record
608
+ * yields "nothing here", never a throw (the projection registry drives the
609
+ * fold without an error boundary; one throw stalls the unit's push feed and
610
+ * the browser waits on "loading" forever).
611
+ *
612
+ * @module dsh-context/host/log-shapes
613
+ */
614
+ /**
615
+ * Whether one raw stream chunk carries a token delta — the first-token marker
616
+ * both generations share. Mirrors dsh-llm's `isTokenDelta` (a non-empty text
617
+ * or reasoning fragment, or any Tool-call delta carrying arguments or a name);
618
+ * a malformed chunk is simply not a token.
619
+ */
620
+ function isTokenChunk(chunk) {
621
+ if (chunk === null || typeof chunk !== "object") return false;
622
+ const c = chunk;
623
+ switch (c.type) {
624
+ case "text-delta":
625
+ case "reasoning-delta": return typeof c.text === "string" && c.text !== "";
626
+ case "tool-call-delta": return typeof c.argumentsDelta === "string" && c.argumentsDelta !== "" || c.name !== void 0;
627
+ default: return false;
628
+ }
629
+ }
630
+ /** Map one `blockType` to its timing bucket (see {@link DecodeKind}). */
631
+ function decodeKindOfBlock(blockType) {
632
+ if (blockType === "reasoning") return "reasoning";
633
+ if (blockType === "text") return "text";
634
+ if (blockType === "tool-call") return "toolarg";
635
+ }
636
+ /**
637
+ * Per-kind decode spans inside one embedded assistant stream, tiling
638
+ * [first block-start, endTime]: each `block-start` record owns the interval up
639
+ * to the next one, the last one up to `endTime`. This is the V2+ shape, whose
640
+ * timed stream rides the settlement (`assistant/message.data.stream`) instead
641
+ * of separate `assistant/chunk` events. Total over untrusted input — a
642
+ * malformed record is skipped, a non-finite boundary yields zero, and a stream
643
+ * with no marker (or not an array) yields all zeros.
644
+ */
645
+ function decodeSpansOfStream(stream, endTime) {
646
+ const spans = {
647
+ reasoning: 0,
648
+ text: 0,
649
+ toolarg: 0
650
+ };
651
+ if (!Array.isArray(stream) || !Number.isFinite(endTime)) return spans;
652
+ let kind;
653
+ let since = 0;
654
+ for (const record of stream) {
655
+ if (record === null || typeof record !== "object") continue;
656
+ const r = record;
657
+ if (r.type !== "chunk" || r.chunk === null || typeof r.chunk !== "object") continue;
658
+ const chunk = r.chunk;
659
+ if (chunk.type !== "block-start") continue;
660
+ const time = r.time;
661
+ if (typeof time !== "number" || !Number.isFinite(time)) continue;
662
+ if (kind !== void 0) spans[kind] += Math.max(0, time - since);
663
+ kind = decodeKindOfBlock(chunk.blockType);
664
+ since = time;
665
+ }
666
+ if (kind !== void 0) spans[kind] += Math.max(0, endTime - since);
667
+ return spans;
668
+ }
669
+ /**
670
+ * The first token's instant inside one PACKED delta run (`text-chunks` /
671
+ * `reasoning-chunks` / `tool-call-chunks`): the run's base time plus the
672
+ * accumulated inter-member deltas, taken at the first qualifying member —
673
+ * a name-bearing Tool-call run starts at its first member. Mirrors dsh-llm's
674
+ * `runFirstTokenTime`; a non-finite base or delta yields undefined rather
675
+ * than a NaN instant.
676
+ */
677
+ function runFirstTokenTime(record) {
678
+ const time0 = record.time0;
679
+ if (typeof time0 !== "number" || !Number.isFinite(time0)) return void 0;
680
+ if (record.type === "tool-call-chunks" && record.name !== void 0) return time0;
681
+ const fragments = record.type === "tool-call-chunks" ? record.args : record.texts;
682
+ if (!Array.isArray(fragments)) return void 0;
683
+ const dt = Array.isArray(record.dt) ? record.dt : [];
684
+ let time = time0;
685
+ for (const [index, fragment] of fragments.entries()) {
686
+ if (index > 0) {
687
+ const step = dt[index - 1];
688
+ if (typeof step !== "number" || !Number.isFinite(step)) return void 0;
689
+ time += step;
690
+ }
691
+ if (typeof fragment === "string" && fragment !== "") return time;
692
+ }
693
+ }
694
+ /**
695
+ * The first token's instant inside an embedded assistant stream
696
+ * (`assistant/message.data.stream`, `assistant/attempt.data.stream` — the V2+
697
+ * settlement that replaced the V0 `assistant/chunk` events), or undefined
698
+ * when the stream carries no token. Mirrors dsh-llm's
699
+ * `assistantStreamFirstTokenTime` over the compact record union.
700
+ */
701
+ function firstTokenTimeOfStream(stream) {
702
+ if (!Array.isArray(stream)) return void 0;
703
+ for (const record of stream) {
704
+ if (record === null || typeof record !== "object") continue;
705
+ const r = record;
706
+ if (r.type === "chunk") {
707
+ const time = r.time;
708
+ if (typeof time === "number" && Number.isFinite(time) && isTokenChunk(r.chunk)) return time;
709
+ continue;
710
+ }
711
+ const time = runFirstTokenTime(r);
712
+ if (time !== void 0) return time;
713
+ }
714
+ }
715
+ /**
716
+ * The inclusive surface range a replacement op covers, or null for `append`
717
+ * and for any unrecognized/hostile op (which the fold treats as an append).
718
+ * Reads BOTH endpoint spellings: V3's `startSeq`/`endSeq` first, then V0's
719
+ * `start`/`end` — each accepted only as a finite number, so a hostile op
720
+ * with one good and one malformed endpoint degrades to append.
721
+ */
722
+ function replaceRangeOf(surfaceOp) {
723
+ if (surfaceOp === null || typeof surfaceOp !== "object") return null;
724
+ const op = surfaceOp;
725
+ if (op.op !== "replace") return null;
726
+ const start = typeof op.startSeq === "number" ? op.startSeq : op.start;
727
+ const end = typeof op.endSeq === "number" ? op.endSeq : op.end;
728
+ if (typeof start !== "number" || !Number.isFinite(start)) return null;
729
+ if (typeof end !== "number" || !Number.isFinite(end)) return null;
730
+ return {
731
+ start,
732
+ end
733
+ };
734
+ }
735
+ //#endregion
568
736
  //#region src/shared/fileOps.ts
569
737
  /** Parse a call's raw JSON arguments; non-string/malformed/non-record inputs yield null. */
570
738
  function parseCallArgs(raw) {
@@ -873,6 +1041,24 @@ function bumpDetailRev(st) {
873
1041
  st.detailRev = (st.detailRev ?? 0) + 1;
874
1042
  }
875
1043
  /**
1044
+ * Bound on the live system-prompt nodes (TimelineState.systems). The
1045
+ * effective figure is the LAST nonempty node, so dropping the oldest can only
1046
+ * under-report a pathological log whose newest SYSTEM_NODES_MAX nodes are all
1047
+ * empty while an older one still carried text.
1048
+ */
1049
+ const SYSTEM_NODES_MAX = 8;
1050
+ /** The effective system-prompt price: the last nonempty node, else 0 (the harness's own rule). */
1051
+ function systemTokensOf(systems) {
1052
+ for (let i = systems.length - 1; i >= 0; i--) if (systems[i].tokens > 0) return systems[i].tokens;
1053
+ return 0;
1054
+ }
1055
+ /** Append one system-prompt node, bounding the list (see SYSTEM_NODES_MAX). */
1056
+ function pushSystem(st, node) {
1057
+ const systems = [...st.systems ?? [], node];
1058
+ st.systems = systems.length > SYSTEM_NODES_MAX ? systems.slice(-8) : systems;
1059
+ st.systemTokens = systemTokensOf(st.systems);
1060
+ }
1061
+ /**
876
1062
  * Bound on the buffered nested Code-Mode ops (TimelineState.pendingCodeOps)
877
1063
  * — a hostile log that dispatches without settling the parent run_code
878
1064
  * cannot grow the persisted state past this.
@@ -919,6 +1105,35 @@ function archiveRemoved(st, removed, goneSeq) {
919
1105
  });
920
1106
  }
921
1107
  /**
1108
+ * Remove every live surface node whose seq the replacement claims, keeping the
1109
+ * per-category sums equal to the surviving nodes and archiving the removals.
1110
+ * Removal follows the SEQ list, not the declared range: pruned replacement
1111
+ * nodes keep their own seqs beyond the range end, so a range-based removal
1112
+ * would leave them behind and overcount. Returns the removed nodes.
1113
+ */
1114
+ function removeSurfaceSeqs(st, claimed, goneSeq) {
1115
+ if (claimed.size === 0) return [];
1116
+ const kept = [];
1117
+ const removed = [];
1118
+ for (const n of st.surface) if (claimed.has(n.seq)) {
1119
+ st.sums[n.cat] -= n.tokens;
1120
+ removed.push(n);
1121
+ } else kept.push(n);
1122
+ archiveRemoved(st, removed, goneSeq);
1123
+ st.surface = kept;
1124
+ return removed;
1125
+ }
1126
+ /**
1127
+ * The message nested under an event payload's `message` field
1128
+ * (`system/message`, `assistant/message`, `tool/result`) — read structurally
1129
+ * rather than through `deriveEventMessage`, whose 0.1.2-rc.1 generation knows
1130
+ * nothing of the V3 `system/message` variant. A malformed payload reads null.
1131
+ */
1132
+ function messageOf(data) {
1133
+ const message = data?.message;
1134
+ return message !== null && typeof message === "object" ? message : null;
1135
+ }
1136
+ /**
922
1137
  * The first full text block, recursing through nested content blocks (a tool
923
1138
  * result wraps its text in a `tool-result` block). Unlike `firstText` this
924
1139
  * must NOT truncate/normalize: the skill name is matched off the raw
@@ -1002,18 +1217,10 @@ function applySurface(st, ev, type, data, message) {
1002
1217
  const shadowEventSeq = st.pendingShadowEventSeq;
1003
1218
  delete st.pendingShadowedSeqs;
1004
1219
  delete st.pendingShadowEventSeq;
1005
- const op = ev.surfaceOp;
1006
- if (op !== null && typeof op === "object" && op.op === "replace") {
1220
+ const op = replaceRangeOf(ev.surfaceOp);
1221
+ if (op !== null) {
1007
1222
  if (Array.isArray(shadowedSeqs) && shadowedSeqs.length > 0) {
1008
- const shadowed = new Set(shadowedSeqs);
1009
- const kept = [];
1010
- const removed = [];
1011
- for (const n of st.surface) if (shadowed.has(n.seq)) {
1012
- st.sums[n.cat] -= n.tokens;
1013
- removed.push(n);
1014
- } else kept.push(n);
1015
- archiveRemoved(st, removed, ev.seq);
1016
- st.surface = kept;
1223
+ const removed = removeSurfaceSeqs(st, new Set(shadowedSeqs), ev.seq);
1017
1224
  st.sums[cat] += node.tokens;
1018
1225
  st.surface.push(node);
1019
1226
  if (shadowEventSeq !== void 0) {
@@ -1069,7 +1276,7 @@ function tokenCountOf(value) {
1069
1276
  /**
1070
1277
  * The DeepSeek V4 model family a model name prices as — matched on the NAME
1071
1278
  * alone (provider-agnostic: official API, proxies, OpenRouter spellings like
1072
- * `deepseek/deepseek-v4-flash` all land here). Null for any other model:
1279
+ * `deepseek/deepseek-v4.1-flash` all land here). Null for any other model:
1073
1280
  * non-V4 usage is simply not priced.
1074
1281
  */
1075
1282
  function costFamilyOf(model) {
@@ -1132,27 +1339,18 @@ function accumulateCost(st, time, usage) {
1132
1339
  */
1133
1340
  /** The timing card's per-tool ranking cap: the busiest 16 names are kept. */
1134
1341
  const TOOL_TIMING_CAP = 16;
1342
+ /** The decode buckets of the generation split, in card order (see TimingTotals). */
1343
+ const DECODE_KINDS = [
1344
+ "reasoning",
1345
+ "text",
1346
+ "toolarg"
1347
+ ];
1135
1348
  /** Non-negative, NaN-proof duration between two instants (hostile times degrade to 0). */
1136
1349
  function durOf(from, to) {
1137
1350
  if (!Number.isFinite(from) || !Number.isFinite(to)) return 0;
1138
1351
  return Math.max(0, to - from);
1139
1352
  }
1140
1353
  /**
1141
- * Whether a stream chunk carries a non-empty token delta — the first-token
1142
- * marker the TTFT fold waits for (the same rule as the harness's own
1143
- * session-stats fold). Shape-guarded: a malformed chunk is just not a token.
1144
- */
1145
- function isTokenDelta(chunk) {
1146
- if (chunk === null || typeof chunk !== "object") return false;
1147
- const c = chunk;
1148
- switch (c.type) {
1149
- case "text-delta":
1150
- case "reasoning-delta": return typeof c.text === "string" && c.text !== "";
1151
- case "tool-call-delta": return typeof c.argumentsDelta === "string" && c.argumentsDelta !== "" || c.name !== void 0;
1152
- default: return false;
1153
- }
1154
- }
1155
- /**
1156
1354
  * The fold's private timing accumulator: created on first use, and CLONED on
1157
1355
  * every later ensure() (see `applyTimeline`) — the object left in the
1158
1356
  * persisted previous state is never written into in place.
@@ -1170,6 +1368,18 @@ function ensureTiming(st) {
1170
1368
  return st.timing;
1171
1369
  }
1172
1370
  /**
1371
+ * Fold one block's decode span into the totals' generation split (see
1372
+ * TimingTotals). A zero span stays ABSENT — the field then carries the
1373
+ * "no time was decoded in this bucket" fact without adding dead properties to
1374
+ * every pre-split-shaped state, and the card reads absence as 0.
1375
+ */
1376
+ function addDecode(timing, kind, ms) {
1377
+ if (!(ms > 0)) return;
1378
+ if (kind === "reasoning") timing.reasoningMs = (timing.reasoningMs ?? 0) + ms;
1379
+ else if (kind === "text") timing.textMs = (timing.textMs ?? 0) + ms;
1380
+ else timing.toolArgMs = (timing.toolArgMs ?? 0) + ms;
1381
+ }
1382
+ /**
1173
1383
  * Tally one completed tool call into the per-name ranking, bounded to
1174
1384
  * TOOL_TIMING_CAP names: repeated names update in place, a new name beyond
1175
1385
  * the cap evicts the smallest tally first (the ranking's tail), so state
@@ -1225,7 +1435,20 @@ function applyTimeline(state, event, bounds) {
1225
1435
  const tools = Array.isArray(header.tools) ? header.tools : [];
1226
1436
  const s = ensure();
1227
1437
  s.toolsTokens = estimateToolsTotal(tools);
1228
- s.systemTokens = estimateSystemTokens(header.system);
1438
+ const systemText = header.system;
1439
+ if (typeof systemText === "string" && systemText !== "") {
1440
+ s.systems = [{
1441
+ seq: event.seq,
1442
+ time: event.time,
1443
+ tokens: estimateSystemTokens(systemText)
1444
+ }];
1445
+ s.systemsFromHeader = true;
1446
+ s.systemTokens = systemTokensOf(s.systems);
1447
+ } else if (s.systemsFromHeader === true) {
1448
+ s.systems = [];
1449
+ delete s.systemsFromHeader;
1450
+ s.systemTokens = 0;
1451
+ }
1229
1452
  if (header.config && typeof header.config.model === "string") s.model = header.config.model;
1230
1453
  if (header.config && typeof header.config.provider === "string") s.provider = header.config.provider;
1231
1454
  if ((data?.reason === "change" || data?.reason === "resume") && s.model && s.lastModel && s.model !== s.lastModel) {
@@ -1241,6 +1464,25 @@ function applyTimeline(state, event, bounds) {
1241
1464
  if (s.model) s.lastModel = s.model;
1242
1465
  break;
1243
1466
  }
1467
+ case "system/message": {
1468
+ const s = ensure();
1469
+ delete s.pendingShadowedSeqs;
1470
+ delete s.pendingShadowEventSeq;
1471
+ const op = replaceRangeOf(event.surfaceOp);
1472
+ if (op !== null) {
1473
+ s.systems = (s.systems ?? []).filter((n) => n.seq < op.start || n.seq > op.end);
1474
+ const claimed = /* @__PURE__ */ new Set();
1475
+ for (const n of s.surface) if (n.seq >= op.start && n.seq <= op.end) claimed.add(n.seq);
1476
+ if (removeSurfaceSeqs(s, claimed, event.seq).length > 0) bumpDetailRev(s);
1477
+ }
1478
+ delete s.systemsFromHeader;
1479
+ pushSystem(s, {
1480
+ seq: event.seq,
1481
+ time: event.time,
1482
+ tokens: estimateSystemContent(messageOf(data)?.content)
1483
+ });
1484
+ break;
1485
+ }
1244
1486
  case "request/context": {
1245
1487
  const s = ensure();
1246
1488
  if (data && typeof data.contextWindow === "number") s.contextWindow = data.contextWindow;
@@ -1259,7 +1501,8 @@ function applyTimeline(state, event, bounds) {
1259
1501
  };
1260
1502
  }
1261
1503
  break;
1262
- case "tool/code-dispatch": {
1504
+ case "tool/code-dispatch":
1505
+ case "tool/ptc-dispatch": {
1263
1506
  const rootCallId = data?.rootCallId;
1264
1507
  const name = data?.name;
1265
1508
  if (typeof rootCallId === "string" && typeof name === "string") {
@@ -1275,13 +1518,48 @@ function applyTimeline(state, event, bounds) {
1275
1518
  break;
1276
1519
  }
1277
1520
  case "assistant/chunk": {
1521
+ const start = state.stepStart;
1522
+ if (start === void 0) return state;
1523
+ const chunk = data?.chunk;
1524
+ if (chunk !== null && typeof chunk === "object" && chunk.type === "block-start") {
1525
+ const kind = decodeKindOfBlock(chunk.blockType);
1526
+ if (start.block === void 0 && kind === void 0) return state;
1527
+ const s = ensure();
1528
+ const decode = { ...start.decode ?? {
1529
+ reasoning: 0,
1530
+ text: 0,
1531
+ toolarg: 0
1532
+ } };
1533
+ if (start.block !== void 0) decode[start.block.kind] += durOf(start.block.since, event.time);
1534
+ s.stepStart = {
1535
+ time: start.time,
1536
+ ...start.firstToken !== void 0 ? { firstToken: start.firstToken } : {},
1537
+ decode,
1538
+ ...kind !== void 0 ? { block: {
1539
+ kind,
1540
+ since: event.time
1541
+ } } : {}
1542
+ };
1543
+ break;
1544
+ }
1545
+ if (start.firstToken !== void 0) return state;
1546
+ if (!isTokenChunk(data?.chunk)) return state;
1547
+ const s = ensure();
1548
+ s.stepStart = {
1549
+ ...start,
1550
+ firstToken: event.time
1551
+ };
1552
+ break;
1553
+ }
1554
+ case "assistant/attempt": {
1278
1555
  const start = state.stepStart;
1279
1556
  if (start === void 0 || start.firstToken !== void 0) return state;
1280
- if (!isTokenDelta(data?.chunk)) return state;
1557
+ const first = firstTokenTimeOfStream(data?.stream);
1558
+ if (first === void 0) return state;
1281
1559
  const s = ensure();
1282
1560
  s.stepStart = {
1283
1561
  time: start.time,
1284
- firstToken: event.time
1562
+ firstToken: first
1285
1563
  };
1286
1564
  break;
1287
1565
  }
@@ -1410,9 +1688,20 @@ function applyTimeline(state, event, bounds) {
1410
1688
  const timing = ensureTiming(s);
1411
1689
  timing.calls += 1;
1412
1690
  const stepStart = state.stepStart;
1413
- if (stepStart !== void 0 && stepStart.firstToken !== void 0) {
1414
- timing.ttftMs += durOf(stepStart.time, stepStart.firstToken);
1415
- timing.genMs += durOf(stepStart.firstToken, event.time);
1691
+ if (stepStart !== void 0) {
1692
+ const firstToken = stepStart.firstToken ?? firstTokenTimeOfStream(data?.stream);
1693
+ if (firstToken !== void 0) {
1694
+ timing.ttftMs += durOf(stepStart.time, firstToken);
1695
+ timing.genMs += durOf(firstToken, event.time);
1696
+ if (stepStart.decode !== void 0) {
1697
+ const decode = { ...stepStart.decode };
1698
+ if (stepStart.block !== void 0) decode[stepStart.block.kind] += durOf(stepStart.block.since, event.time);
1699
+ for (const kind of DECODE_KINDS) addDecode(timing, kind, decode[kind]);
1700
+ } else {
1701
+ const spans = decodeSpansOfStream(data?.stream, event.time);
1702
+ for (const kind of DECODE_KINDS) addDecode(timing, kind, spans[kind]);
1703
+ }
1704
+ }
1416
1705
  }
1417
1706
  const asstMsg = deriveEventMessage(event);
1418
1707
  applySurface(s, event, event.type, data, asstMsg);
@@ -1514,6 +1803,7 @@ function headFieldsOf(state) {
1514
1803
  tools
1515
1804
  };
1516
1805
  }
1806
+ if (state.systems !== void 0 && state.systems.length > 0) result.systems = state.systems.map((n) => ({ ...n }));
1517
1807
  return result;
1518
1808
  }
1519
1809
  /**
@@ -1951,6 +2241,12 @@ const surfaceNodeSchema = z.object({
1951
2241
  skill: z.string().optional(),
1952
2242
  calls: z.array(z.string()).optional()
1953
2243
  }).strict();
2244
+ /** One live system-prompt node (shared/types.ts SystemPromptNode). */
2245
+ const systemPromptNodeSchema = z.object({
2246
+ seq: z.number().int().nonnegative(),
2247
+ time: z.number(),
2248
+ tokens: z.number().int().nonnegative()
2249
+ }).strict();
1954
2250
  const requestRecordSchema = z.object({
1955
2251
  turn: z.number().optional(),
1956
2252
  step: z.number().optional(),
@@ -2046,6 +2342,9 @@ const timingTotalsSchema = z.object({
2046
2342
  wallMs: z.number().nonnegative(),
2047
2343
  ttftMs: z.number().nonnegative(),
2048
2344
  genMs: z.number().nonnegative(),
2345
+ reasoningMs: z.number().nonnegative().optional(),
2346
+ textMs: z.number().nonnegative().optional(),
2347
+ toolArgMs: z.number().nonnegative().optional(),
2049
2348
  calls: z.number().int().nonnegative(),
2050
2349
  toolsMs: z.number().nonnegative(),
2051
2350
  toolCalls: z.number().int().nonnegative(),
@@ -2097,6 +2396,7 @@ const contextTimelineSchema = z.object({
2097
2396
  pro: costFamilySchema.optional()
2098
2397
  }).strict().optional(),
2099
2398
  timing: timingTotalsSchema.optional(),
2399
+ systems: z.array(systemPromptNodeSchema).optional(),
2100
2400
  nodes: z.array(surfaceNodeSchema).optional(),
2101
2401
  droppedNodes: z.number().int().nonnegative().optional(),
2102
2402
  archive: z.array(surfaceNodeSchema).optional(),
@@ -2120,6 +2420,8 @@ const timelineStateSchema = z.object({
2120
2420
  tool: z.number().int().nonnegative()
2121
2421
  }).strict(),
2122
2422
  systemTokens: z.number().int().nonnegative(),
2423
+ systems: z.array(systemPromptNodeSchema).optional(),
2424
+ systemsFromHeader: z.literal(true).optional(),
2123
2425
  toolsTokens: z.number().int().nonnegative(),
2124
2426
  model: z.string().optional(),
2125
2427
  provider: z.string().optional(),
@@ -2136,7 +2438,20 @@ const timelineStateSchema = z.object({
2136
2438
  timing: timingTotalsSchema.optional(),
2137
2439
  stepStart: z.object({
2138
2440
  time: z.number(),
2139
- firstToken: z.number().optional()
2441
+ firstToken: z.number().optional(),
2442
+ decode: z.object({
2443
+ reasoning: z.number(),
2444
+ text: z.number(),
2445
+ toolarg: z.number()
2446
+ }).strict().optional(),
2447
+ block: z.object({
2448
+ kind: z.enum([
2449
+ "reasoning",
2450
+ "text",
2451
+ "toolarg"
2452
+ ]),
2453
+ since: z.number()
2454
+ }).strict().optional()
2140
2455
  }).strict().optional(),
2141
2456
  callNames: z.record(z.string(), z.object({
2142
2457
  name: z.string(),