dsh-context 0.39.0 → 0.40.1

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
@@ -30,8 +30,27 @@ declare const Config: z.ZodPreprocess<z.ZodObject<{
30
30
  }, z.core.$strict>>;
31
31
  //#endregion
32
32
  //#region src/host/headers.d.ts
33
+ /**
34
+ * One stored tool: the v1 row shape carried the producer description and the
35
+ * raw schema; folds since the #37 slim-down append metadata-only entries.
36
+ */
37
+ interface StoredHeaderTool {
38
+ name: string;
39
+ tokens: number;
40
+ description?: string;
41
+ plugin?: string;
42
+ schema?: unknown;
43
+ }
44
+ /** One stored epoch: v1 rows carried `system`; folds since carry `systemTokens`. */
45
+ interface StoredHeaderRecord {
46
+ seq: number;
47
+ time: number;
48
+ system?: string;
49
+ systemTokens?: number;
50
+ tools: StoredHeaderTool[];
51
+ }
33
52
  interface HeadersState {
34
- headers: HeaderRecord[];
53
+ headers: StoredHeaderRecord[];
35
54
  }
36
55
  //#endregion
37
56
  //#region src/host/fold.d.ts
@@ -382,12 +401,10 @@ interface ContextEventRecord {
382
401
  turn?: number;
383
402
  step?: number;
384
403
  }
385
- /** One tool schema as assembled into a request header, with its display price. */
404
+ /** One tool of a request-header epoch, with its display price. */
386
405
  interface HeaderTool {
387
406
  name: string;
388
407
  tokens: number;
389
- /** Producer-declared description (may be long; the browser truncates). */
390
- description?: string;
391
408
  /**
392
409
  * The registering plugin's label, when attribution is known: either a
393
410
  * `plugin` field carried by the raw header entry (harness-provided) or the
@@ -397,16 +414,21 @@ interface HeaderTool {
397
414
  * the browser shows no tag.
398
415
  */
399
416
  plugin?: string;
400
- /** The raw JSON schema object the model received (plain JSON). */
401
- schema?: unknown;
402
417
  }
403
418
  /**
404
- * One request-header epoch: the full system prompt and tool schemas in force from this event's seq until the next epoch.
419
+ * One request-header epoch's METADATA: the epoch boundaries and token prices
420
+ * in force from this event's seq until the next epoch. The epoch CONTENT
421
+ * (full system prompt text, tool descriptions/schemas) is not projected —
422
+ * every session.list row, control baseline, push frame, and projection-cache
423
+ * checkpoint would otherwise carry it per session × epoch. The client
424
+ * fetches one epoch's content on demand (a seq-anchored history read off
425
+ * `seq`) as a {@link HeaderEpochContent}.
405
426
  */
406
427
  interface HeaderRecord {
407
428
  seq: number;
408
429
  time: number;
409
- system?: string;
430
+ /** The epoch's estimated system-prompt tokens; absent when it logged no system prompt. */
431
+ systemTokens?: number;
410
432
  tools: HeaderTool[];
411
433
  }
412
434
  /** The `contextHeaders` projection value: the bounded epoch list (newest last). */
package/lib/index.js CHANGED
@@ -433,11 +433,11 @@ function estimateImageTokens(width, height) {
433
433
  * (shared/imageTokens.ts), falling back to the meter's JSON price when the
434
434
  * attachment's dimensions are unknown.
435
435
  */
436
- const CHARS_PER_TOKEN = 4;
436
+ const CHARS_PER_TOKEN$1 = 4;
437
437
  const BLOCK_OVERHEAD = 4;
438
- const ROLE_OVERHEAD = 4;
438
+ const ROLE_OVERHEAD$1 = 4;
439
439
  function estimateToolsTotal(tools) {
440
- return tools.length > 0 ? Math.ceil(JSON.stringify(tools).length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD : 0;
440
+ return tools.length > 0 ? Math.ceil(JSON.stringify(tools).length / CHARS_PER_TOKEN$1) + BLOCK_OVERHEAD : 0;
441
441
  }
442
442
  /** The `ContentBlock` walkers take `unknown`: block arrays ride the untrusted
443
443
  * log, so their element shapes (null and primitives included) are re-proved
@@ -454,10 +454,10 @@ function estimateBlocks(blocks) {
454
454
  switch (block.type) {
455
455
  case "text":
456
456
  case "reasoning":
457
- tokens += Math.ceil((block.text || "").length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD;
457
+ tokens += Math.ceil((block.text || "").length / CHARS_PER_TOKEN$1) + BLOCK_OVERHEAD;
458
458
  break;
459
459
  case "tool-call":
460
- tokens += Math.ceil((block.name || "").length / CHARS_PER_TOKEN) + Math.ceil((block.arguments || "").length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD;
460
+ tokens += Math.ceil((block.name || "").length / CHARS_PER_TOKEN$1) + Math.ceil((block.arguments || "").length / CHARS_PER_TOKEN$1) + BLOCK_OVERHEAD;
461
461
  break;
462
462
  case "tool-result":
463
463
  tokens += estimateBlocks(block.content) + BLOCK_OVERHEAD;
@@ -465,10 +465,10 @@ function estimateBlocks(blocks) {
465
465
  case "image": {
466
466
  const ref = block.attachment;
467
467
  const priced = ref !== null && typeof ref === "object" && typeof ref.width === "number" && typeof ref.height === "number" ? estimateImageTokens(ref.width, ref.height) : null;
468
- tokens += (priced ?? Math.ceil(JSON.stringify(block).length / CHARS_PER_TOKEN)) + BLOCK_OVERHEAD;
468
+ tokens += (priced ?? Math.ceil(JSON.stringify(block).length / CHARS_PER_TOKEN$1)) + BLOCK_OVERHEAD;
469
469
  break;
470
470
  }
471
- default: tokens += BLOCK_OVERHEAD + Math.ceil(JSON.stringify(block).length / CHARS_PER_TOKEN);
471
+ default: tokens += BLOCK_OVERHEAD + Math.ceil(JSON.stringify(block).length / CHARS_PER_TOKEN$1);
472
472
  }
473
473
  }
474
474
  return tokens;
@@ -480,15 +480,12 @@ function estimateBlocks(blocks) {
480
480
  */
481
481
  function estimateMessage(message, emptyIsZero = false) {
482
482
  if (emptyIsZero && (message === null || message === void 0 || !Array.isArray(message.content) || message.content.length === 0)) return 0;
483
- return estimateBlocks(message?.content) + ROLE_OVERHEAD;
484
- }
485
- function estimateSystem(text) {
486
- if (typeof text !== "string" || text.length === 0) return 0;
487
- return Math.ceil(text.length / CHARS_PER_TOKEN) + ROLE_OVERHEAD;
483
+ return estimateBlocks(message?.content) + ROLE_OVERHEAD$1;
488
484
  }
485
+ /** The shared meter heuristic over rendered system-prompt text (shared/estimate.ts). */
489
486
  /** Per-tool price for the top-tools display (the total uses dsh's whole-array price). */
490
487
  function estimateToolSchema(tool) {
491
- return Math.ceil(JSON.stringify(tool).length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD;
488
+ return Math.ceil(JSON.stringify(tool).length / CHARS_PER_TOKEN$1) + BLOCK_OVERHEAD;
492
489
  }
493
490
  /**
494
491
  * Count image blocks in a message payload, recursing into nested content (tool-result blocks carry their inner blocks) — seeds each node's
@@ -547,44 +544,90 @@ function isInjection(source) {
547
544
  return source !== null && source !== void 0 && (typeof source.kind === "string" && source.kind !== "" && source.kind !== "user" || typeof source.form === "string");
548
545
  }
549
546
  //#endregion
547
+ //#region src/shared/estimate.ts
548
+ /**
549
+ * Token heuristics shared by the host fold and the client boundary — the
550
+ * harness token-meter's own fixed-density figure (dsh-token-meter/estimate.ts:
551
+ * ~4 chars ≈ 1 token, +4 role framing). Priced identically on both sides so a
552
+ * legacy value normalized at the client boundary matches what the host view
553
+ * would have served.
554
+ */
555
+ const CHARS_PER_TOKEN = 4;
556
+ const ROLE_OVERHEAD = 4;
557
+ /** Price rendered system-prompt text; 0 for absent/empty/non-string input. */
558
+ function estimateSystemTokens(text) {
559
+ if (typeof text !== "string" || text.length === 0) return 0;
560
+ return Math.ceil(text.length / CHARS_PER_TOKEN) + ROLE_OVERHEAD;
561
+ }
562
+ //#endregion
550
563
  //#region src/host/headers.ts
551
564
  /**
552
- * The `contextHeaders` session projection unit — the request-header CONTENT
553
- * epochs behind the timeline's envelope figures.
565
+ * The `contextHeaders` session projection unit — the request-header EPOCH
566
+ * METADATA behind the timeline's envelope figures.
554
567
  *
555
568
  * The hot `contextTimeline` unit carries only token prices of the system
556
- * prompt and tool schemas; this companion unit keeps the CONTENT (full
557
- * system prompt text, full tool JSON schemas) so the Context browser card
558
- * can show what a picked step's request was actually assembled from. It is
559
- * a separate unit on purpose: the agent loop logs `request/header` only
560
- * when the header changes, so this state (and its pushes to the browser)
561
- * moves rarely — carrying full content costs nothing on the per-event hot
562
- * path.
569
+ * prompt and tool schemas; this companion unit keeps the per-epoch METADATA
570
+ * (epoch seq/time boundaries, per-tool token prices and plugin attribution)
571
+ * so the Context browser can pick the header epoch in force at any step and
572
+ * size its sections immediately. The epoch CONTENT (full system prompt text,
573
+ * full tool JSON schemas) deliberately does NOT ride the projection VALUE:
574
+ * session projections are served whole in every `session.list` row, control
575
+ * baseline, push frame, and change notification, so carrying content here
576
+ * multiplied it by sessions × epochs across every channel. The client
577
+ * fetches one epoch's `request/header` event on demand — a seq-anchored
578
+ * history read off the epoch's `seq`, the same targeted read the browser
579
+ * already uses for message content — and caches it per session (history is
580
+ * immutable).
581
+ *
582
+ * Read-compat over the persisted state (the pinned decision behind keeping
583
+ * `stateVersion` at 1): the harness serves a cold session's projections from
584
+ * its CACHED checkpoint rows and has no refresh channel for an idle session
585
+ * — a version bump invalidates every row and orphans the key until the
586
+ * session goes live again (the #37 regression). The state therefore still
587
+ * ACCEPTS the v1 content-bearing record shape, current folds append
588
+ * metadata-only records alongside any seeded legacy ones, and the view
589
+ * normalizes BOTH to the metadata-only wire shape (pricing the legacy system
590
+ * text at read time). Cached v1 rows keep working, new checkpoint writes
591
+ * shrink as legacy epochs age out of the capped list, and the wire — the
592
+ * part every delivery channel carries — is metadata-only from day one.
563
593
  *
564
594
  * Same projection contract as the timeline unit: pure init/apply/view,
565
595
  * `Object.is` reference stability for uninteresting events, plain-JSON
566
596
  * bounded state (epoch list capped — see HEADERS_MAX).
567
597
  */
568
- /** Retention cap on header epochs (changes are rare; 50 is generous). */
598
+ /** Retention cap on header epochs (metadata only; changes are rare; 50 is generous). */
569
599
  const HEADERS_MAX = 50;
570
- const headerToolSchema = z.object({
600
+ /**
601
+ * The persisted-state schema: the SUPERSET of both record generations, so a
602
+ * cached v1 row (content-bearing) seeds the fold instead of being discarded.
603
+ */
604
+ const storedToolSchema = z.object({
571
605
  name: z.string(),
572
606
  tokens: z.number().int().nonnegative(),
573
607
  description: z.string().optional(),
574
608
  plugin: z.string().optional(),
575
609
  schema: z.unknown().optional()
576
610
  }).strict();
577
- const contextHeadersSchema = z.object({ headers: z.array(z.object({
611
+ const storedEpochSchema = z.object({
578
612
  seq: z.number(),
579
613
  time: z.number(),
580
614
  system: z.string().optional(),
581
- tools: z.array(headerToolSchema)
615
+ systemTokens: z.number().int().nonnegative().optional(),
616
+ tools: z.array(storedToolSchema)
617
+ }).strict();
618
+ const contextHeadersStateSchema = z.object({ headers: z.array(storedEpochSchema) }).strict();
619
+ /** The wire schema: strict metadata — the shape every delivery channel carries. */
620
+ const headerToolWireSchema = z.object({
621
+ name: z.string(),
622
+ tokens: z.number().int().nonnegative(),
623
+ plugin: z.string().optional()
624
+ }).strict();
625
+ const contextHeadersSchema = z.object({ headers: z.array(z.object({
626
+ seq: z.number(),
627
+ time: z.number(),
628
+ systemTokens: z.number().int().nonnegative().optional(),
629
+ tools: z.array(headerToolWireSchema)
582
630
  }).strict()) }).strict();
583
- /**
584
- * State and wire are the same shape (the view only shallow-copies each record), so one schema validates both under the dsh 0.1.1-rc.1+
585
- * `stateSchema`/`wire` contract.
586
- */
587
- const contextHeadersStateSchema = contextHeadersSchema;
588
631
  function recordOf(event) {
589
632
  if (event.type !== "request/header") return null;
590
633
  const rawHeader = event.data.header;
@@ -598,36 +641,41 @@ function recordOf(event) {
598
641
  const tool = t !== null && typeof t === "object" ? t : {};
599
642
  const entry = {
600
643
  name: typeof tool.name === "string" ? tool.name : "?",
601
- tokens: estimateToolSchema(t),
602
- schema: t
644
+ tokens: estimateToolSchema(t)
603
645
  };
604
- if (typeof tool.description === "string" && tool.description !== "") entry.description = tool.description;
605
646
  if (typeof tool.plugin === "string" && tool.plugin !== "") entry.plugin = tool.plugin;
606
647
  return entry;
607
648
  })
608
649
  };
609
- if (typeof header.system === "string" && header.system.length > 0) record.system = header.system;
650
+ if (typeof header.system === "string" && header.system.length > 0) record.systemTokens = estimateSystemTokens(header.system);
610
651
  return record;
611
652
  }
612
653
  /**
613
654
  * The context-headers projection unit; registered alongside the timeline unit (host/index.ts); clients read it through
614
- * `useProjection('contextHeaders')` and degrade to tokens-only header sections when the key is absent. Contract mirror
615
- * with a REQUIRED `wire` block (see compat.ts).
655
+ * `useProjection('contextHeaders')` and fetch an epoch's full content on demand via the session history (historyPage.ts).
656
+ * Contract mirror with a REQUIRED `wire` block (see compat.ts).
616
657
  * @param resolve - best-effort tool-to-plugin attribution (see toolSources.ts); fills a missing `plugin` at view time so
617
658
  * epochs folded without attribution still render a tag when the source is known.
618
659
  */
619
660
  function createContextHeadersDefinition(resolve) {
620
- const view = (state) => ({ headers: state.headers.map((h) => ({
621
- ...h,
622
- tools: h.tools.map((t) => {
623
- if (resolve === void 0 || t.plugin !== void 0) return { ...t };
624
- const plugin = resolve(t.name);
625
- return plugin === void 0 ? { ...t } : {
626
- ...t,
627
- plugin
628
- };
629
- })
630
- })) });
661
+ const view = (state) => ({ headers: state.headers.map((h) => {
662
+ const record = {
663
+ seq: h.seq,
664
+ time: h.time,
665
+ tools: h.tools.map((t) => {
666
+ const entry = {
667
+ name: t.name,
668
+ tokens: t.tokens
669
+ };
670
+ const plugin = t.plugin ?? (resolve !== void 0 ? resolve(t.name) : void 0);
671
+ if (plugin !== void 0) entry.plugin = plugin;
672
+ return entry;
673
+ })
674
+ };
675
+ const systemTokens = h.systemTokens ?? (typeof h.system === "string" && h.system !== "" ? estimateSystemTokens(h.system) : void 0);
676
+ if (systemTokens !== void 0) record.systemTokens = systemTokens;
677
+ return record;
678
+ }) });
631
679
  return {
632
680
  key: "contextHeaders",
633
681
  stateSchema: contextHeadersStateSchema,
@@ -1014,7 +1062,7 @@ function applyTimeline(state, event, bounds) {
1014
1062
  const tools = Array.isArray(header.tools) ? header.tools : [];
1015
1063
  const s = ensure();
1016
1064
  s.toolsTokens = estimateToolsTotal(tools);
1017
- s.systemTokens = estimateSystem(header.system);
1065
+ s.systemTokens = estimateSystemTokens(header.system);
1018
1066
  if (header.config && typeof header.config.model === "string") s.model = header.config.model;
1019
1067
  if (header.config && typeof header.config.provider === "string") s.provider = header.config.provider;
1020
1068
  if ((data?.reason === "change" || data?.reason === "resume") && s.model && s.lastModel && s.model !== s.lastModel) s.events.push({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-context",
3
- "version": "0.39.0",
3
+ "version": "0.40.1",
4
4
  "description": "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.",
5
5
  "author": "bowenliang123",
6
6
  "repository": {