dsh-context 0.22.2 → 0.24.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
@@ -329,6 +329,12 @@ interface TimelineState {
329
329
  cost?: SessionCostUsage;
330
330
  /** Newest `gone` among archive entries dropped by the retention bounds. */
331
331
  archiveFloor?: number;
332
+ /**
333
+ * Tool callId → name, armed by `tool/call` and DELETED when its
334
+ * `tool/result` folds in (one result per call, in log order) — the map
335
+ * stays at pending-call size instead of growing for the session's whole
336
+ * lifetime (it is persisted state, shallow-copied by every fold step).
337
+ */
332
338
  callNames: Record<string, string>;
333
339
  /**
334
340
  * Seq list of the surface nodes the next replacement will shadow, armed by
package/lib/index.js CHANGED
@@ -39,11 +39,149 @@ function resolveBounds(config) {
39
39
  return Config.parse(config ?? {});
40
40
  }
41
41
  //#endregion
42
+ //#region src/shared/imageTokens.ts
43
+ /**
44
+ * Per-image token estimate for DeepSeek's vision model — a faithful port of
45
+ * the official "图片 Token 计算器" (Image Token Calculator) shipped on the
46
+ * DeepSeek API docs (https://api-docs.deepseek.com/zh-cn/quick_start/token_usage),
47
+ * which implements the provider's own image→token conversion:
48
+ *
49
+ * - every image is aspect-preserved rescaled before entering the model:
50
+ * below ~384×384 total pixels it is enlarged, above it is shrunk;
51
+ * - tokens follow the patch grid (patch 14px, downsample 3), so every
52
+ * image costs at least 117 and at most ~384 tokens (the documented cap).
53
+ *
54
+ * Verified against the docs calculator itself: 2048×1365→313, 800×600→341,
55
+ * 2048×2048→349, 512×512→201, 100×100→117, 1920×1080→369, 400×900→249.
56
+ * The DSH request pipeline's own 640k-pixel pre-resize does not change the
57
+ * result (the provider formula rescales to the same patch grid), so the
58
+ * durable attachment dimensions can be fed in directly.
59
+ *
60
+ * Pure math shared by the Host fold (message pricing) and the Client
61
+ * (attachment card token badges) — no dependencies, never mutates.
62
+ */
63
+ const PATCH_SIZE = 14;
64
+ const DOWNSAMPLE_RATIO = 3;
65
+ const MAX_WH_RATIO = 8;
66
+ /** ~384×384 total pixels: smaller images are enlarged before patching. */
67
+ const MIN_PIXELS = 147456;
68
+ const floorDiv = (a, b) => Math.floor(a / b);
69
+ const ceilDiv = (a, b) => Math.floor((a + b - 1) / b);
70
+ /** Token count of one patch grid (rows×cols) under the v4 layout rule. */
71
+ function gridTokens(rows, cols) {
72
+ let n = rows * (cols + 1) + 2;
73
+ if (rows % 2 === 1) n += cols + 1;
74
+ n += ceilDiv(rows, 2) * (cols + 1) % 2 * 2;
75
+ return n;
76
+ }
77
+ /** Solve the largest in-grid resize whose token count fits `budget`. */
78
+ function solveResizeRatio(height, width, budget) {
79
+ const ratio = height / width;
80
+ const gridW = Math.sqrt((budget - 2) / ratio + .25) - .5;
81
+ const gridH = gridW * ratio;
82
+ const unit = 42;
83
+ let bestHeight;
84
+ let bestWidth;
85
+ if (gridW < 1) {
86
+ let rows = floorDiv(budget - 2, 2);
87
+ if (rows % 2 === 1) rows -= 1;
88
+ bestWidth = unit;
89
+ bestHeight = rows * unit;
90
+ } else if (gridH < 2) {
91
+ const cols = floorDiv(budget - 2, 2) - 1;
92
+ if (cols <= 1) throw new Error("image tokens: budget too small to solve");
93
+ bestHeight = 84;
94
+ bestWidth = cols * unit;
95
+ } else {
96
+ const cols = Math.trunc(gridW);
97
+ let rows = Math.trunc(gridH);
98
+ if (rows % 2 === 1) rows -= 1;
99
+ const scale = Math.min(cols * unit / width, rows * unit / height);
100
+ bestWidth = Math.trunc(width * scale / PATCH_SIZE) * PATCH_SIZE;
101
+ bestHeight = Math.trunc(height * scale / PATCH_SIZE) * PATCH_SIZE;
102
+ }
103
+ const nLlmH = ceilDiv(floorDiv(bestHeight, PATCH_SIZE), DOWNSAMPLE_RATIO);
104
+ const nLlmW = ceilDiv(floorDiv(bestWidth, PATCH_SIZE), DOWNSAMPLE_RATIO);
105
+ return {
106
+ nLlmH,
107
+ nLlmW,
108
+ bestHeight,
109
+ bestWidth,
110
+ numTokens: gridTokens(nLlmH, nLlmW)
111
+ };
112
+ }
113
+ /** Resize so the patch grid fits the cap, then re-add the pad reserve. */
114
+ function safeResize(height, width, paddedHeight, paddedWidth) {
115
+ const nLlmH = ceilDiv(floorDiv(paddedHeight, PATCH_SIZE), DOWNSAMPLE_RATIO);
116
+ const nLlmW = ceilDiv(floorDiv(paddedWidth, PATCH_SIZE), DOWNSAMPLE_RATIO);
117
+ const pad = 3;
118
+ const budget = 381;
119
+ let result = {
120
+ nLlmH,
121
+ nLlmW,
122
+ bestHeight: paddedHeight,
123
+ bestWidth: paddedWidth,
124
+ numTokens: gridTokens(nLlmH, nLlmW)
125
+ };
126
+ if (result.numTokens > budget) {
127
+ result = solveResizeRatio(height, width, budget);
128
+ let nextBudget = budget;
129
+ while (result.numTokens > budget) {
130
+ nextBudget -= 1;
131
+ result = solveResizeRatio(height, width, nextBudget);
132
+ }
133
+ }
134
+ result.numTokens += pad;
135
+ return result;
136
+ }
137
+ function calcResizeInner(width, height) {
138
+ let w = width;
139
+ let h = height;
140
+ if (w > h * MAX_WH_RATIO) w = h * MAX_WH_RATIO;
141
+ const pixels = w * h;
142
+ if (pixels < MIN_PIXELS && pixels > 0) {
143
+ const scale = Math.sqrt(MIN_PIXELS / pixels);
144
+ w = Math.trunc(w * scale);
145
+ h = Math.trunc(h * scale);
146
+ }
147
+ const paddedWidth = ceilDiv(w, PATCH_SIZE) * PATCH_SIZE;
148
+ const paddedHeight = ceilDiv(h, PATCH_SIZE) * PATCH_SIZE;
149
+ return safeResize(h, w, paddedHeight, paddedWidth);
150
+ }
151
+ /**
152
+ * Estimate the tokens one image consumes in a DeepSeek vision request, from
153
+ * its pixel dimensions. Returns null for non-positive/non-finite dimensions
154
+ * or when the official iteration fails to converge — callers fall back to
155
+ * the generic structural price.
156
+ */
157
+ function estimateImageTokens(width, height) {
158
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return null;
159
+ try {
160
+ let result = calcResizeInner(width, height);
161
+ for (let i = 1; i < 10; i++) {
162
+ const next = calcResizeInner(result.bestWidth, result.bestHeight);
163
+ if (next.nLlmH === result.nLlmH && next.nLlmW === result.nLlmW && next.bestHeight === result.bestHeight && next.bestWidth === result.bestWidth && next.numTokens === result.numTokens) return result.numTokens;
164
+ result = next;
165
+ }
166
+ return null;
167
+ } catch {
168
+ return null;
169
+ }
170
+ }
171
+ //#endregion
42
172
  //#region src/host/pricing.ts
43
173
  /**
44
174
  * Token pricing — the same fixed-density heuristic as the harness's own
45
175
  * token-meter (`dsh-token-meter/estimate.ts`): ~4 chars ≈ 1 token, +4 per
46
176
  * content block, +4 role framing. Pure functions over message payloads.
177
+ *
178
+ * One deliberate refinement over the meter: `image` blocks. The meter prices
179
+ * them through its generic JSON branch (~40 tokens for the durable ref),
180
+ * while DeepSeek's vision model actually bills 117-384 tokens per image by
181
+ * pixel dimensions (https://api-docs.deepseek.com/zh-cn/guides/vision/).
182
+ * Image blocks therefore price through the official docs calculator port
183
+ * (shared/imageTokens.ts), falling back to the meter's JSON price when the
184
+ * attachment's dimensions are unknown.
47
185
  */
48
186
  const CHARS_PER_TOKEN = 4;
49
187
  const BLOCK_OVERHEAD = 4;
@@ -66,6 +204,12 @@ function estimateBlocks(blocks) {
66
204
  case "tool-result":
67
205
  tokens += estimateBlocks(block.content) + BLOCK_OVERHEAD;
68
206
  break;
207
+ case "image": {
208
+ const ref = block.attachment;
209
+ const priced = ref !== null && typeof ref === "object" && typeof ref.width === "number" && typeof ref.height === "number" ? estimateImageTokens(ref.width, ref.height) : null;
210
+ tokens += (priced ?? Math.ceil(JSON.stringify(block).length / CHARS_PER_TOKEN)) + BLOCK_OVERHEAD;
211
+ break;
212
+ }
69
213
  default: tokens += BLOCK_OVERHEAD + Math.ceil(JSON.stringify(block).length / CHARS_PER_TOKEN);
70
214
  }
71
215
  return tokens;
@@ -304,6 +448,11 @@ function applySurface(st, ev, type, data, message) {
304
448
  const blockId = (message?.content?.[0])?.toolCallId;
305
449
  if (srcName) node.tool = srcName;
306
450
  else if (typeof blockId === "string") node.tool = st.callNames[blockId];
451
+ if (typeof srcId === "string" || typeof blockId === "string") {
452
+ const kept = {};
453
+ for (const k in st.callNames) if (k !== srcId && k !== blockId) kept[k] = st.callNames[k];
454
+ st.callNames = kept;
455
+ }
307
456
  if (data?.error) node.err = true;
308
457
  } else if (source?.kind === "skill-invocation") node.skill = typeof source.name === "string" ? source.name : "?";
309
458
  else if (source?.kind === "plugin") {
@@ -791,7 +940,7 @@ function createContextTimelineDefinition(config) {
791
940
  },
792
941
  init: () => createTimelineState(),
793
942
  apply: (state, event) => applyTimeline(state, event, bounds),
794
- stateVersion: 5
943
+ stateVersion: 6
795
944
  };
796
945
  }
797
946
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-context",
3
- "version": "0.22.2",
3
+ "version": "0.24.0",
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": {