dsh-context 0.37.0 → 0.38.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
@@ -85,12 +85,35 @@ interface TimelineState {
85
85
  cost?: SessionCostUsage;
86
86
  archiveFloor?: number;
87
87
  /**
88
- * Tool callId name, armed by `tool/call` and DELETED when its
89
- * `tool/result` folds in (one result per call, in log order) the map
90
- * stays at pending-call size instead of growing for the session's whole
91
- * lifetime (it is persisted state, shallow-copied by every fold step).
88
+ * Whole-session timing totals (see TimingTotals) running sums over the
89
+ * COMPLETE session log, like `cost`. Absent until the first step or tool
90
+ * lifecycle folds in; created once and cloned-on-touch afterwards (the
91
+ * object is shared with the persisted previous state see `ensure`).
92
92
  */
93
- callNames: Record<string, string>;
93
+ timing?: TimingTotals;
94
+ /**
95
+ * The open step's start instant, armed by `step/start` and consumed by the
96
+ * `assistant/message` (LM-call time) and `step/end` (wall time) that follow
97
+ * it. One slot, not a map: steps are sequential in the log, so the newest
98
+ * `step/start` is the one those events close — a hostile interleaved log
99
+ * degrades to skipped durations, never to unbounded state. Same
100
+ * arm/remove lifecycle as `pendingShadowedSeqs`.
101
+ */
102
+ stepStart?: {
103
+ time: number;
104
+ };
105
+ /**
106
+ * Tool callId → the call's name and start instant, armed by `tool/call` and
107
+ * DELETED when its `tool/result` folds in (one result per call, in log
108
+ * order) — the map stays at pending-call size instead of growing for the
109
+ * session's whole lifetime (it is persisted state, shallow-copied by every
110
+ * fold step). The start instant prices the call's duration into
111
+ * `timing.toolsMs` when the result arrives.
112
+ */
113
+ callNames: Record<string, {
114
+ name: string;
115
+ start: number;
116
+ }>;
94
117
  /**
95
118
  * Seq list of the surface nodes the next replacement will shadow, armed by
96
119
  * the metering event (`compaction/summary` | `compaction/prune`) and
@@ -187,6 +210,12 @@ interface Snapshot {
187
210
  * request reports usage.
188
211
  */
189
212
  cost?: SessionCostUsage;
213
+ /**
214
+ * Whole-session timing totals (see TimingTotals). Absent until the first
215
+ * step lifecycle completes in the log (older plugin builds never folded
216
+ * one — clients treat absence as an empty timing card).
217
+ */
218
+ timing?: TimingTotals;
190
219
  /**
191
220
  * The served live surface: the newest `maxNodes` tail PLUS every live inject node older than the tail (injections land first and are
192
221
  * few,
@@ -230,6 +259,37 @@ interface CostBucketTotals {
230
259
  cacheWrite: number;
231
260
  output: number;
232
261
  }
262
+ /**
263
+ * One completed tool name's whole-session call tally behind the timing
264
+ * card's top-tools ranking (running totals, never trimmed).
265
+ */
266
+ interface ToolTimingTotals {
267
+ calls: number;
268
+ ms: number;
269
+ }
270
+ /**
271
+ * Whole-session timing totals, host-folded from the durable `step/start` /
272
+ `step/end` / `tool/call` / `tool/result` lifecycle (running totals over the
273
+ * COMPLETE session log — the same never-trimmed framing as `cost`). Durations
274
+ * are wall-clock milliseconds: `wallMs` sums whole steps, `lmMs` the
275
+ * step-start → assistant-message slice (the model call), `toolsMs` the sum of
276
+ * per-call tool durations (parallel calls each count, so it can overlap).
277
+ * Absent until the first step lifecycle completes in the log.
278
+ */
279
+ interface TimingTotals {
280
+ /** Summed wall time of completed steps (the session's active time). */
281
+ wallMs: number;
282
+ /** Summed step-start → assistant-message time (model-call time). */
283
+ lmMs: number;
284
+ /** Completed model calls (assistant messages folded). */
285
+ calls: number;
286
+ /** Summed per-call durations of completed tool calls. */
287
+ toolsMs: number;
288
+ /** Completed tool calls (call/result pairs folded). */
289
+ toolCalls: number;
290
+ /** Per-tool-name tallies behind the timing card's ranking (bounded). */
291
+ tools: Record<string, ToolTimingTotals>;
292
+ }
233
293
  /** One model family's totals split by DeepSeek's pricing period (Beijing Time). */
234
294
  interface CostFamilyUsage {
235
295
  peak?: CostBucketTotals;
package/lib/index.js CHANGED
@@ -786,10 +786,18 @@ function applySurface(st, ev, type, data, message) {
786
786
  }
787
787
  } else if (type === "tool/result") {
788
788
  const srcId = source?.callId;
789
- const srcName = typeof srcId === "string" ? st.callNames[srcId] : void 0;
790
789
  const blockId = (message?.content?.[0])?.toolCallId;
791
- if (srcName !== void 0) node.tool = srcName;
792
- else if (typeof blockId === "string" && Object.hasOwn(st.callNames, blockId)) node.tool = st.callNames[blockId];
790
+ const srcEntry = typeof srcId === "string" ? st.callNames[srcId] : void 0;
791
+ const blockEntry = srcEntry === void 0 && typeof blockId === "string" ? st.callNames[blockId] : void 0;
792
+ const toolEntry = srcEntry ?? blockEntry;
793
+ if (toolEntry !== void 0) {
794
+ node.tool = toolEntry.name;
795
+ const timing = ensureTiming(st);
796
+ const dur = durOf(toolEntry.start, ev.time);
797
+ timing.toolsMs += dur;
798
+ timing.toolCalls += 1;
799
+ bumpToolTotals(timing, toolEntry.name, dur);
800
+ }
793
801
  if (typeof srcId === "string" || typeof blockId === "string") {
794
802
  const kept = {};
795
803
  for (const k in st.callNames) if (k !== srcId && k !== blockId) kept[k] = st.callNames[k];
@@ -922,6 +930,60 @@ function accumulateCost(st, time, usage) {
922
930
  * `bounds` come from the plugin config (config.ts) — retention only, they
923
931
  * never change the state shape.
924
932
  */
933
+ /** The timing card's per-tool ranking cap: the busiest 16 names are kept. */
934
+ const TOOL_TIMING_CAP = 16;
935
+ /** Non-negative, NaN-proof duration between two instants (hostile times degrade to 0). */
936
+ function durOf(from, to) {
937
+ if (!Number.isFinite(from) || !Number.isFinite(to)) return 0;
938
+ return Math.max(0, to - from);
939
+ }
940
+ /**
941
+ * The fold's private timing accumulator: created on first use, and CLONED on
942
+ * every later ensure() (see `applyTimeline`) — the object left in the
943
+ * persisted previous state is never written into in place.
944
+ */
945
+ function ensureTiming(st) {
946
+ if (st.timing === void 0) st.timing = {
947
+ wallMs: 0,
948
+ lmMs: 0,
949
+ calls: 0,
950
+ toolsMs: 0,
951
+ toolCalls: 0,
952
+ tools: {}
953
+ };
954
+ return st.timing;
955
+ }
956
+ /**
957
+ * Tally one completed tool call into the per-name ranking, bounded to
958
+ * TOOL_TIMING_CAP names: repeated names update in place, a new name beyond
959
+ * the cap evicts the smallest tally first (the ranking's tail), so state
960
+ * stays bounded even over a hostile log of unique names.
961
+ */
962
+ function bumpToolTotals(timing, name, ms) {
963
+ if (!Object.hasOwn(timing.tools, name)) {
964
+ if (Object.keys(timing.tools).length >= TOOL_TIMING_CAP) {
965
+ let minKey = "";
966
+ let minMs = Infinity;
967
+ for (const k in timing.tools) if (timing.tools[k].ms < minMs) {
968
+ minMs = timing.tools[k].ms;
969
+ minKey = k;
970
+ }
971
+ const kept = {};
972
+ for (const k in timing.tools) if (k !== minKey) kept[k] = timing.tools[k];
973
+ timing.tools = kept;
974
+ }
975
+ timing.tools[name] = {
976
+ calls: 1,
977
+ ms
978
+ };
979
+ return;
980
+ }
981
+ const cur = timing.tools[name];
982
+ timing.tools[name] = {
983
+ calls: cur.calls + 1,
984
+ ms: cur.ms + ms
985
+ };
986
+ }
925
987
  function applyTimeline(state, event, bounds) {
926
988
  let st;
927
989
  const ensure = () => st ??= {
@@ -931,7 +993,11 @@ function applyTimeline(state, event, bounds) {
931
993
  requests: [...state.requests],
932
994
  events: [...state.events],
933
995
  archived: [...state.archived],
934
- callNames: { ...state.callNames }
996
+ callNames: { ...state.callNames },
997
+ ...state.timing !== void 0 ? { timing: {
998
+ ...state.timing,
999
+ tools: { ...state.timing.tools }
1000
+ } } : {}
935
1001
  };
936
1002
  const data = event.data;
937
1003
  try {
@@ -964,9 +1030,25 @@ function applyTimeline(state, event, bounds) {
964
1030
  case "tool/call":
965
1031
  if (data && typeof data.callId === "string" && typeof data.name === "string") {
966
1032
  const s = ensure();
967
- s.callNames[data.callId] = data.name;
1033
+ s.callNames[data.callId] = {
1034
+ name: data.name,
1035
+ start: event.time
1036
+ };
968
1037
  }
969
1038
  break;
1039
+ case "step/start": {
1040
+ const s = ensure();
1041
+ s.stepStart = { time: event.time };
1042
+ break;
1043
+ }
1044
+ case "step/end": {
1045
+ const start = state.stepStart;
1046
+ if (start === void 0) return state;
1047
+ const s = ensure();
1048
+ ensureTiming(s).wallMs += durOf(start.time, event.time);
1049
+ delete s.stepStart;
1050
+ break;
1051
+ }
970
1052
  case "user/message": {
971
1053
  const msg = deriveEventMessage(event);
972
1054
  const s = ensure();
@@ -1037,6 +1119,10 @@ function applyTimeline(state, event, bounds) {
1037
1119
  accumulateCost(s, event.time, usage);
1038
1120
  }
1039
1121
  s.requests.push(record);
1122
+ const timing = ensureTiming(s);
1123
+ timing.calls += 1;
1124
+ const stepStart = state.stepStart;
1125
+ if (stepStart !== void 0) timing.lmMs += durOf(stepStart.time, event.time);
1040
1126
  const asstMsg = deriveEventMessage(event);
1041
1127
  applySurface(s, event, event.type, data, asstMsg);
1042
1128
  break;
@@ -1120,6 +1206,14 @@ function buildTimelineView(state, bounds) {
1120
1206
  if (pro !== void 0) cost.pro = pro;
1121
1207
  result.cost = cost;
1122
1208
  }
1209
+ if (state.timing !== void 0) {
1210
+ const tools = {};
1211
+ for (const k in state.timing.tools) tools[k] = { ...state.timing.tools[k] };
1212
+ result.timing = {
1213
+ ...state.timing,
1214
+ tools
1215
+ };
1216
+ }
1123
1217
  const overflowCount = Math.max(0, state.surface.length - bounds.maxNodes);
1124
1218
  const overflow = state.surface.slice(0, overflowCount);
1125
1219
  const tail = state.surface.slice(overflowCount);
@@ -1249,6 +1343,18 @@ const costFamilySchema = z.object({
1249
1343
  peak: costBucketsSchema.optional(),
1250
1344
  off: costBucketsSchema.optional()
1251
1345
  }).strict();
1346
+ const toolTimingSchema = z.object({
1347
+ calls: z.number().int().nonnegative(),
1348
+ ms: z.number().nonnegative()
1349
+ }).strict();
1350
+ const timingTotalsSchema = z.object({
1351
+ wallMs: z.number().nonnegative(),
1352
+ lmMs: z.number().nonnegative(),
1353
+ calls: z.number().int().nonnegative(),
1354
+ toolsMs: z.number().nonnegative(),
1355
+ toolCalls: z.number().int().nonnegative(),
1356
+ tools: z.record(z.string(), toolTimingSchema)
1357
+ }).strict();
1252
1358
  const contextTimelineSchema = z.object({
1253
1359
  ok: z.literal(true),
1254
1360
  model: z.string().optional(),
@@ -1263,6 +1369,7 @@ const contextTimelineSchema = z.object({
1263
1369
  flash: costFamilySchema.optional(),
1264
1370
  pro: costFamilySchema.optional()
1265
1371
  }).strict().optional(),
1372
+ timing: timingTotalsSchema.optional(),
1266
1373
  nodes: z.array(surfaceNodeSchema),
1267
1374
  droppedNodes: z.number().int().nonnegative(),
1268
1375
  archive: z.array(surfaceNodeSchema),
@@ -1297,7 +1404,12 @@ const timelineStateSchema = z.object({
1297
1404
  pro: costFamilySchema.optional()
1298
1405
  }).strict().optional(),
1299
1406
  archiveFloor: z.number().optional(),
1300
- callNames: z.record(z.string(), z.string()),
1407
+ timing: timingTotalsSchema.optional(),
1408
+ stepStart: z.object({ time: z.number() }).strict().optional(),
1409
+ callNames: z.record(z.string(), z.object({
1410
+ name: z.string(),
1411
+ start: z.number()
1412
+ }).strict()),
1301
1413
  pendingShadowedSeqs: z.array(z.number()).optional(),
1302
1414
  pendingShadowEventSeq: z.number().optional()
1303
1415
  });
@@ -1335,7 +1447,7 @@ function createContextTimelineDefinition(config) {
1335
1447
  },
1336
1448
  init: () => createTimelineState(),
1337
1449
  apply: (state, event) => applyTimeline(state, event, bounds),
1338
- stateVersion: 10
1450
+ stateVersion: 11
1339
1451
  };
1340
1452
  }
1341
1453
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-context",
3
- "version": "0.37.0",
3
+ "version": "0.38.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": {
@@ -34,7 +34,7 @@
34
34
  "build": "tsdown",
35
35
  "lint": "oxlint",
36
36
  "lint:fix": "oxlint --fix",
37
- "watch": "tsdown --watch",
37
+ "watch": "bash scripts/watch.sh",
38
38
  "register": "bash scripts/register.sh",
39
39
  "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.tests.json",
40
40
  "test": "pnpm run typecheck && vitest run --coverage",