dsh-fast 0.1.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +201 -0
  3. package/README.es.md +166 -0
  4. package/README.hi.md +166 -0
  5. package/README.md +166 -0
  6. package/README.pt.md +166 -0
  7. package/README.zh.md +166 -0
  8. package/THIRD_PARTY_NOTICES.md +20 -0
  9. package/cordis.patch.yml +55 -0
  10. package/lib/index.js +844 -0
  11. package/lib/types/analyze.d.ts +41 -0
  12. package/lib/types/analyze.d.ts.map +1 -0
  13. package/lib/types/analyze.js +121 -0
  14. package/lib/types/analyze.js.map +1 -0
  15. package/lib/types/collector.d.ts +82 -0
  16. package/lib/types/collector.d.ts.map +1 -0
  17. package/lib/types/collector.js +236 -0
  18. package/lib/types/collector.js.map +1 -0
  19. package/lib/types/config.d.ts +76 -0
  20. package/lib/types/config.d.ts.map +1 -0
  21. package/lib/types/config.js +93 -0
  22. package/lib/types/config.js.map +1 -0
  23. package/lib/types/estimate.d.ts +24 -0
  24. package/lib/types/estimate.d.ts.map +1 -0
  25. package/lib/types/estimate.js +37 -0
  26. package/lib/types/estimate.js.map +1 -0
  27. package/lib/types/index.d.ts +35 -0
  28. package/lib/types/index.d.ts.map +1 -0
  29. package/lib/types/index.js +201 -0
  30. package/lib/types/index.js.map +1 -0
  31. package/lib/types/model.d.ts +91 -0
  32. package/lib/types/model.d.ts.map +1 -0
  33. package/lib/types/model.js +11 -0
  34. package/lib/types/model.js.map +1 -0
  35. package/lib/types/sanitize.d.ts +33 -0
  36. package/lib/types/sanitize.d.ts.map +1 -0
  37. package/lib/types/sanitize.js +59 -0
  38. package/lib/types/sanitize.js.map +1 -0
  39. package/lib/types/store.d.ts +74 -0
  40. package/lib/types/store.d.ts.map +1 -0
  41. package/lib/types/store.js +84 -0
  42. package/lib/types/store.js.map +1 -0
  43. package/lib/types/version.d.ts +3 -0
  44. package/lib/types/version.d.ts.map +1 -0
  45. package/lib/types/version.js +3 -0
  46. package/lib/types/version.js.map +1 -0
  47. package/package.json +147 -0
  48. package/src/analyze.ts +148 -0
  49. package/src/collector.ts +289 -0
  50. package/src/config.ts +163 -0
  51. package/src/estimate.ts +41 -0
  52. package/src/index.ts +235 -0
  53. package/src/model.ts +98 -0
  54. package/src/sanitize.ts +60 -0
  55. package/src/store.ts +100 -0
  56. package/src/version.ts +2 -0
package/lib/index.js ADDED
@@ -0,0 +1,844 @@
1
+ import { defineTool } from "@deepseek-ai/dsh-tools";
2
+ import z from "@deepseek-ai/schemastery";
3
+ import z$1 from "zod";
4
+ import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
5
+ //#region src/config.ts
6
+ /**
7
+ * Config schema and resolution for `dsh-fast`. Every tunable is a validated
8
+ * {@link Config} field changeable from cordis.yml; the resolution step
9
+ * validates numeric bounds so misconfiguration fails loud at mount. The plugin
10
+ * is read-only and safe, so it defaults to enabled — but `enabled: false`
11
+ * mounts nothing.
12
+ * @module dsh-fast/config
13
+ */
14
+ /** Schemastery schema: the loader validates and fills defaults before `apply`. */
15
+ const Config = z.object({
16
+ enabled: z.boolean().default(true),
17
+ privacy: z.object({ includeCwd: z.boolean().default(false) }).default({ includeCwd: false }),
18
+ sampling: z.object({
19
+ snapshotIntervalMs: z.number().default(6e4),
20
+ maxHistorySamples: z.number().default(20)
21
+ }).default({
22
+ snapshotIntervalMs: 6e4,
23
+ maxHistorySamples: 20
24
+ }),
25
+ thresholds: z.object({
26
+ systemPromptTokens: z.number().default(2e4),
27
+ toolSchemaTokens: z.number().default(8e3),
28
+ surfaceTokens: z.number().default(6e4),
29
+ cacheHitRateFloor: z.number().default(.1),
30
+ compactionCountWarn: z.number().default(10),
31
+ compactionShadowTokens: z.number().default(4e4)
32
+ }).default({
33
+ systemPromptTokens: 2e4,
34
+ toolSchemaTokens: 8e3,
35
+ surfaceTokens: 6e4,
36
+ cacheHitRateFloor: .1,
37
+ compactionCountWarn: 10,
38
+ compactionShadowTokens: 4e4
39
+ }),
40
+ spill: z.object({ detectSpilledResults: z.boolean().default(true) }).default({ detectSpilledResults: true })
41
+ });
42
+ /** Throw unless `value` is a positive safe integer. */
43
+ function assertPositiveInt(name, value) {
44
+ if (!Number.isSafeInteger(value) || value <= 0) throw new TypeError(`${name} must be a positive safe integer, got ${String(value)}`);
45
+ }
46
+ /** Throw unless `value` is a finite number in `[min, max]`. */
47
+ function assertFiniteRange(name, value, min, max) {
48
+ if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) throw new TypeError(`${name} must be a finite number in [${min}, ${max}], got ${String(value)}`);
49
+ }
50
+ /**
51
+ * Validate raw values and fill explicit defaults. Invalid bounds throw here —
52
+ * misconfiguration fails loud at mount even without the Schemastery loader.
53
+ * @param config - raw (possibly partial) plugin config.
54
+ * @returns the fully resolved config.
55
+ */
56
+ function resolveConfig(config = {}) {
57
+ const samplingRaw = config.sampling ?? {};
58
+ const thresholdsRaw = config.thresholds ?? {};
59
+ const spillRaw = config.spill ?? {};
60
+ const snapshotIntervalMs = samplingRaw.snapshotIntervalMs ?? 6e4;
61
+ assertPositiveInt("sampling.snapshotIntervalMs", snapshotIntervalMs);
62
+ const maxHistorySamples = samplingRaw.maxHistorySamples ?? 20;
63
+ assertPositiveInt("sampling.maxHistorySamples", maxHistorySamples);
64
+ const systemPromptTokens = thresholdsRaw.systemPromptTokens ?? 2e4;
65
+ const toolSchemaTokens = thresholdsRaw.toolSchemaTokens ?? 8e3;
66
+ const surfaceTokens = thresholdsRaw.surfaceTokens ?? 6e4;
67
+ const cacheHitRateFloor = thresholdsRaw.cacheHitRateFloor ?? .1;
68
+ const compactionCountWarn = thresholdsRaw.compactionCountWarn ?? 10;
69
+ const compactionShadowTokens = thresholdsRaw.compactionShadowTokens ?? 4e4;
70
+ assertPositiveInt("thresholds.systemPromptTokens", systemPromptTokens);
71
+ assertPositiveInt("thresholds.toolSchemaTokens", toolSchemaTokens);
72
+ assertPositiveInt("thresholds.surfaceTokens", surfaceTokens);
73
+ assertFiniteRange("thresholds.cacheHitRateFloor", cacheHitRateFloor, 0, 1);
74
+ assertPositiveInt("thresholds.compactionCountWarn", compactionCountWarn);
75
+ assertPositiveInt("thresholds.compactionShadowTokens", compactionShadowTokens);
76
+ return {
77
+ enabled: config.enabled ?? true,
78
+ includeCwd: config.privacy?.includeCwd ?? false,
79
+ snapshotIntervalMs,
80
+ maxHistorySamples,
81
+ detectSpilledResults: spillRaw.detectSpilledResults ?? true,
82
+ thresholds: {
83
+ systemPromptTokens,
84
+ toolSchemaTokens,
85
+ surfaceTokens,
86
+ cacheHitRateFloor,
87
+ compactionCountWarn,
88
+ compactionShadowTokens
89
+ }
90
+ };
91
+ }
92
+ //#endregion
93
+ //#region src/estimate.ts
94
+ /** Fixed text-density estimate (chars per token). */
95
+ const CHARS_PER_TOKEN = 4;
96
+ /** Role-field framing overhead added to every priced message. */
97
+ const ROLE_OVERHEAD = 4;
98
+ /** Per-block structural overhead for JSON framing and type tags. */
99
+ const BLOCK_OVERHEAD = 4;
100
+ /**
101
+ * Price the assembled system prompt (AGENTS.md + skill directory + persona +
102
+ * harness instructions).
103
+ * @param header - canonical request envelope, or undefined before any request.
104
+ * @returns heuristic system-prompt tokens; 0 when absent.
105
+ */
106
+ function estimateSystemTokens(header) {
107
+ if (header?.system === void 0) return 0;
108
+ return Math.ceil(header.system.length / CHARS_PER_TOKEN) + ROLE_OVERHEAD;
109
+ }
110
+ /**
111
+ * Price the tool-schema part of the request envelope.
112
+ * @param header - canonical request envelope, or undefined before any request.
113
+ * @returns heuristic tool-schema tokens; 0 when absent or empty.
114
+ */
115
+ function estimateToolsTokens(header) {
116
+ if (header?.tools === void 0 || header.tools.length === 0) return 0;
117
+ return Math.ceil(JSON.stringify(header.tools).length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD;
118
+ }
119
+ //#endregion
120
+ //#region src/collector.ts
121
+ /** The durable marker every spill notice carries (`... Full ... stored at: <locator> ...`). */
122
+ const SPILL_NOTICE_MARKERS = ["Full", "stored at:"];
123
+ /** Flatten a tool result's model-facing text blocks to one string. */
124
+ function flattenToolResultText(message) {
125
+ const block = message.content[0];
126
+ if (block === void 0) return "";
127
+ let text = "";
128
+ for (const inner of block.content) if (inner.type === "text") text += inner.text;
129
+ return text;
130
+ }
131
+ /**
132
+ * Best-effort spill detection: a spilled tool result is one whose durable text
133
+ * carries the spill-policy notice (`Full … stored at: <locator>`). No dedicated
134
+ * session event exists, so this is a documented heuristic, not a hard signal.
135
+ * @param message - the tool result message.
136
+ * @returns true when the result looks spilled.
137
+ */
138
+ function detectSpilledResult(message) {
139
+ const text = flattenToolResultText(message);
140
+ return SPILL_NOTICE_MARKERS.every((marker) => text.includes(marker));
141
+ }
142
+ /** Fraction of `total` each bucket represents (0 when the total is 0). */
143
+ function sharesOf(total, system, tools, surface) {
144
+ if (total <= 0) return {
145
+ systemShare: 0,
146
+ toolsShare: 0,
147
+ surfaceShare: 0
148
+ };
149
+ return {
150
+ systemShare: system / total,
151
+ toolsShare: tools / total,
152
+ surfaceShare: surface / total
153
+ };
154
+ }
155
+ /** Cache hit rate from aggregate tokens: `cacheRead / (input + cacheRead)`. */
156
+ function hitRateOf(input, cacheRead) {
157
+ const denominator = input + cacheRead;
158
+ if (denominator <= 0) return null;
159
+ return cacheRead / denominator;
160
+ }
161
+ /**
162
+ * The event → snapshot collector over real Sessions. State is adopted lazily on
163
+ * the first event so an HMR reload (which does not replay `session/created`)
164
+ * still adopts existing live sessions.
165
+ */
166
+ var FastCollector = class {
167
+ config;
168
+ live = /* @__PURE__ */ new Map();
169
+ /** @param config - the resolved plugin config. */
170
+ constructor(config) {
171
+ this.config = config;
172
+ }
173
+ /** Adopt a session at its creation announcement. */
174
+ handleSessionCreated(session) {
175
+ this.adopt(session);
176
+ }
177
+ /** Drop a session leaving the store. */
178
+ handleSessionDisposed(session) {
179
+ this.live.delete(session);
180
+ }
181
+ /**
182
+ * Fold one appended session event (O(1) per event).
183
+ * @param session - the session the event belongs to.
184
+ * @param event - the appended event.
185
+ */
186
+ handleEvent(session, event) {
187
+ const state = this.adopt(session);
188
+ switch (event.type) {
189
+ case "request/header":
190
+ state.lastHeader = event.data.header;
191
+ if (state.timeToFirstRequestMs === null) {
192
+ state.timeToFirstRequestMs = Math.max(0, event.time - state.createdAtMs);
193
+ state.dirty = true;
194
+ }
195
+ break;
196
+ case "assistant/message":
197
+ this.foldUsage(state, event.data.usage);
198
+ break;
199
+ case "compaction/start":
200
+ state.compactionCount += 1;
201
+ if (event.data.sourceCommandId === void 0) state.compactionAutomatic += 1;
202
+ else state.compactionManual += 1;
203
+ state.dirty = true;
204
+ break;
205
+ case "compaction/summary":
206
+ state.compactionShadowedTokens += event.data.shadowedTokenCount;
207
+ state.dirty = true;
208
+ break;
209
+ case "tool/result": if (this.config.detectSpilledResults && detectSpilledResult(event.data.message)) {
210
+ state.spilledResults += 1;
211
+ state.dirty = true;
212
+ }
213
+ }
214
+ }
215
+ /** The live sessions the sampling timer iterates. */
216
+ liveSessions() {
217
+ return this.live.keys();
218
+ }
219
+ /** Whether a session is still live (adopted and not disposed). */
220
+ has(session) {
221
+ return this.live.has(session);
222
+ }
223
+ /** Whether a session has un-persisted changes. */
224
+ isDirty(session) {
225
+ return this.live.get(session)?.dirty ?? false;
226
+ }
227
+ /** Clear the dirty flag after a snapshot is appended. */
228
+ markClean(session) {
229
+ const state = this.live.get(session);
230
+ if (state !== void 0) state.dirty = false;
231
+ }
232
+ /**
233
+ * Build the current metric snapshot for one session. This is the only place
234
+ * the optional token meter is consulted, so it never runs in the append path.
235
+ * @param session - the session to snapshot.
236
+ * @param measure - optional token-meter measure function.
237
+ * @returns the snapshot.
238
+ */
239
+ snapshot(session, measure) {
240
+ const state = this.live.get(session);
241
+ if (state === void 0) return emptySnapshot();
242
+ const measurement = measure === void 0 ? void 0 : measure(session);
243
+ const systemTokens = estimateSystemTokens(state.lastHeader);
244
+ const toolSchemaTokens = estimateToolsTokens(state.lastHeader);
245
+ const surfaceTokens = measurement?.surfaceTokens ?? 0;
246
+ const totalTokens = measurement?.totalTokens ?? systemTokens + toolSchemaTokens + surfaceTokens;
247
+ return {
248
+ load: {
249
+ kind: state.kind,
250
+ seedEvents: state.firstLiveSeq,
251
+ timeToFirstRequestMs: state.timeToFirstRequestMs
252
+ },
253
+ spill: {
254
+ detectedSpilledResults: state.spilledResults,
255
+ heuristic: true
256
+ },
257
+ compaction: {
258
+ count: state.compactionCount,
259
+ manual: state.compactionManual,
260
+ automatic: state.compactionAutomatic,
261
+ shadowedTokens: state.compactionShadowedTokens
262
+ },
263
+ context: {
264
+ totalTokens,
265
+ systemTokens,
266
+ toolSchemaTokens,
267
+ surfaceTokens,
268
+ ...sharesOf(totalTokens, systemTokens, toolSchemaTokens, surfaceTokens)
269
+ },
270
+ cache: this.cacheStats(state)
271
+ };
272
+ }
273
+ /** Aggregate cache counters into the report shape. */
274
+ cacheStats(state) {
275
+ return {
276
+ inputTokens: state.inputTokens,
277
+ cacheReadTokens: state.cacheReadTokens,
278
+ cacheWriteTokens: state.cacheWriteTokens,
279
+ outputTokens: state.outputTokens,
280
+ hitRate: hitRateOf(state.inputTokens, state.cacheReadTokens)
281
+ };
282
+ }
283
+ /** Fold one provider usage record. */
284
+ foldUsage(state, usage) {
285
+ if (usage === void 0) return;
286
+ state.inputTokens += usage.inputTokens;
287
+ state.outputTokens += usage.outputTokens;
288
+ state.cacheReadTokens += usage.cacheReadTokens ?? 0;
289
+ state.cacheWriteTokens += usage.cacheWriteTokens ?? 0;
290
+ state.dirty = true;
291
+ }
292
+ /** Adopt (or return the existing) live state for one session. */
293
+ adopt(session) {
294
+ const existing = this.live.get(session);
295
+ if (existing !== void 0) return existing;
296
+ const state = {
297
+ session,
298
+ createdAtMs: Date.now(),
299
+ kind: session.firstLiveSeq > 0 ? "restore" : "open",
300
+ firstLiveSeq: session.firstLiveSeq,
301
+ timeToFirstRequestMs: null,
302
+ spilledResults: 0,
303
+ compactionCount: 0,
304
+ compactionManual: 0,
305
+ compactionAutomatic: 0,
306
+ compactionShadowedTokens: 0,
307
+ inputTokens: 0,
308
+ cacheReadTokens: 0,
309
+ cacheWriteTokens: 0,
310
+ outputTokens: 0,
311
+ lastHeader: void 0,
312
+ dirty: true
313
+ };
314
+ this.live.set(session, state);
315
+ return state;
316
+ }
317
+ };
318
+ /** A zeroed snapshot for a session that is no longer live. */
319
+ function emptySnapshot() {
320
+ return {
321
+ load: {
322
+ kind: "open",
323
+ seedEvents: 0,
324
+ timeToFirstRequestMs: null
325
+ },
326
+ spill: {
327
+ detectedSpilledResults: 0,
328
+ heuristic: true
329
+ },
330
+ compaction: {
331
+ count: 0,
332
+ manual: 0,
333
+ automatic: 0,
334
+ shadowedTokens: 0
335
+ },
336
+ context: {
337
+ totalTokens: 0,
338
+ systemTokens: 0,
339
+ toolSchemaTokens: 0,
340
+ surfaceTokens: 0,
341
+ systemShare: 0,
342
+ toolsShare: 0,
343
+ surfaceShare: 0
344
+ },
345
+ cache: {
346
+ inputTokens: 0,
347
+ cacheReadTokens: 0,
348
+ cacheWriteTokens: 0,
349
+ outputTokens: 0,
350
+ hitRate: null
351
+ }
352
+ };
353
+ }
354
+ //#endregion
355
+ //#region src/sanitize.ts
356
+ /**
357
+ * Pure display/durable-boundary sanitization. Any free-form string that can
358
+ * reach a session event or a model-facing report (session identity, working
359
+ * directory, labels) passes through these functions first, so control
360
+ * characters never enter the log and no string exceeds its budget. These are
361
+ * pure functions of their inputs.
362
+ * @module dsh-fast/sanitize
363
+ */
364
+ /** C0 control characters plus DEL, replaced before any output. */
365
+ const CONTROL_CHARS = /[\u0000-\u001f\u007f]/gu;
366
+ /** Ellipsis appended when a string is truncated. */
367
+ const ELLIPSIS = "…";
368
+ /** Remove control characters from a string. */
369
+ function stripControl(value) {
370
+ return value.replace(CONTROL_CHARS, "");
371
+ }
372
+ /**
373
+ * Truncate a string to `maxChars`, appending an ellipsis when it is cut.
374
+ * @param value - the string to bound.
375
+ * @param maxChars - non-negative budget; 0 yields the empty string.
376
+ * @returns the bounded string.
377
+ */
378
+ function truncate(value, maxChars) {
379
+ if (!Number.isSafeInteger(maxChars) || maxChars < 0) throw new TypeError(`maxChars must be a non-negative safe integer, got ${String(maxChars)}`);
380
+ if (value.length <= maxChars) return value;
381
+ if (maxChars === 0) return "";
382
+ return value.slice(0, maxChars) + ELLIPSIS;
383
+ }
384
+ /**
385
+ * Sanitize a free-form label: strip control characters, then bound length.
386
+ * @param value - the label (e.g. a session id).
387
+ * @param maxChars - non-negative budget.
388
+ * @returns the sanitized label.
389
+ */
390
+ function sanitizeText(value, maxChars) {
391
+ return truncate(stripControl(value), maxChars);
392
+ }
393
+ /**
394
+ * Sanitize a path or filename, preserving its tail (basename) when truncating
395
+ * so the most diagnostic part survives. Control characters are stripped first.
396
+ * @param value - the path or filename.
397
+ * @param maxChars - non-negative budget.
398
+ * @returns the sanitized path.
399
+ */
400
+ function sanitizePath(value, maxChars) {
401
+ const clean = stripControl(value);
402
+ if (clean.length <= maxChars) return clean;
403
+ if (maxChars <= 1) return ELLIPSIS;
404
+ const head = Math.ceil(maxChars / 2);
405
+ const tail = Math.floor(maxChars / 2);
406
+ return clean.slice(0, head) + ELLIPSIS + clean.slice(-tail);
407
+ }
408
+ //#endregion
409
+ //#region src/analyze.ts
410
+ /**
411
+ * Build the optimization suggestions for a snapshot against the config
412
+ * thresholds. Suggestions are plain strings, one per distinct finding.
413
+ * @param snapshot - the metric snapshot.
414
+ * @param config - the resolved config (thresholds).
415
+ * @returns the ordered suggestions (may be empty).
416
+ */
417
+ function buildSuggestions(snapshot, config) {
418
+ const suggestions = [];
419
+ const t = config.thresholds;
420
+ const { context, compaction, cache, spill } = snapshot;
421
+ if (context.systemTokens > t.systemPromptTokens) suggestions.push(`System prompt is large (${context.systemTokens} tokens, threshold ${t.systemPromptTokens}); consider trimming AGENTS.md, the skill directory, or persona.`);
422
+ if (context.toolSchemaTokens > t.toolSchemaTokens) suggestions.push(`Tool schema is large (${context.toolSchemaTokens} tokens, threshold ${t.toolSchemaTokens}); consider mounting fewer tools or tightening parameter descriptions.`);
423
+ if (context.surfaceTokens > t.surfaceTokens) suggestions.push(`Session surface is large (${context.surfaceTokens} tokens, threshold ${t.surfaceTokens}); consider /compact or clearing old tool results.`);
424
+ if (compaction.count >= t.compactionCountWarn) suggestions.push(`This session has triggered ${compaction.count} compactions; context pressure is high — compact earlier or raise the compaction threshold.`);
425
+ if (compaction.count > 0) {
426
+ const average = compaction.shadowedTokens / compaction.count;
427
+ if (average > t.compactionShadowTokens) suggestions.push(`Average compaction shadows ${Math.round(average)} tokens (threshold ${t.compactionShadowTokens}); the compaction threshold may be too conservative.`);
428
+ }
429
+ if (cache.hitRate !== null && cache.hitRate < t.cacheHitRateFloor) suggestions.push(`LLM cache hit rate is only ${Math.round(cache.hitRate * 100)}% (threshold ${Math.round(t.cacheHitRateFloor * 100)}%); consider enabling or tuning prompt caching.`);
430
+ if (spill.detectedSpilledResults === 0 && context.surfaceTokens > t.surfaceTokens / 2) suggestions.push("Surface volume is high with no spill hits; if spill-policy is not enabled, consider enabling it to protect the context window.");
431
+ return suggestions;
432
+ }
433
+ /** Format one nullable millisecond duration. */
434
+ function formatMs(value) {
435
+ return value === null ? "n/a" : `${value} ms`;
436
+ }
437
+ /** Format one nullable ratio as a percentage. */
438
+ function formatPercent(value) {
439
+ return value === null ? "n/a" : `${Math.round(value * 100)}%`;
440
+ }
441
+ /**
442
+ * Render a report as the human-readable `/fast` body.
443
+ * @param report - the assembled report.
444
+ * @returns the report text.
445
+ */
446
+ function renderFastText(report) {
447
+ const { load, spill, compaction, context, cache } = report;
448
+ const lines = [
449
+ `dsh-fast ${report.version} — performance report`,
450
+ `session: ${report.sessionId}`,
451
+ "",
452
+ "## Session load",
453
+ `kind: ${load.kind}`,
454
+ `seed events: ${load.seedEvents}`,
455
+ `time to first request: ${formatMs(load.timeToFirstRequestMs)}`,
456
+ "",
457
+ "## Spill",
458
+ `detected spilled results: ${spill.detectedSpilledResults} (heuristic)`,
459
+ "",
460
+ "## Compaction",
461
+ `count: ${compaction.count} (manual: ${compaction.manual}, automatic: ${compaction.automatic})`,
462
+ `shadowed tokens: ${compaction.shadowedTokens}`,
463
+ "",
464
+ "## Context volume",
465
+ `total: ${context.totalTokens} tokens`,
466
+ `system (AGENTS.md/skills/persona): ${context.systemTokens} (${formatPercent(context.systemShare)})`,
467
+ `tool schema: ${context.toolSchemaTokens} (${formatPercent(context.toolsShare)})`,
468
+ `surface: ${context.surfaceTokens} (${formatPercent(context.surfaceShare)})`,
469
+ "",
470
+ "## LLM cache",
471
+ `input: ${cache.inputTokens}, cache read: ${cache.cacheReadTokens}, cache write: ${cache.cacheWriteTokens}, output: ${cache.outputTokens}`,
472
+ `hit rate: ${formatPercent(cache.hitRate)}`
473
+ ];
474
+ if (report.cwd !== void 0) lines.splice(lines.length, 0, `cwd: ${report.cwd}`);
475
+ lines.push("", "## Suggestions");
476
+ if (report.suggestions.length === 0) lines.push("- none");
477
+ else for (const suggestion of report.suggestions) lines.push(`- ${suggestion}`);
478
+ return lines.join("\n");
479
+ }
480
+ /**
481
+ * Assemble the final report from a snapshot plus caller metadata. Suggestions
482
+ * are computed here and the identity/cwd fields are sanitized before any model
483
+ * or durable surface sees them.
484
+ * @param snapshot - the metric snapshot.
485
+ * @param meta - session identity, optional cwd, and generation time.
486
+ * @param config - the resolved config (thresholds).
487
+ * @param version - the plugin version.
488
+ * @returns the complete report.
489
+ */
490
+ function buildReport(snapshot, meta, config, version) {
491
+ return {
492
+ ...snapshot,
493
+ generator: "dsh-fast",
494
+ version,
495
+ sessionId: sanitizeText(meta.sessionId, 128),
496
+ generatedAt: meta.generatedAt,
497
+ suggestions: buildSuggestions(snapshot, config),
498
+ ...meta.cwd === void 0 ? {} : { cwd: sanitizePath(meta.cwd, 256) }
499
+ };
500
+ }
501
+ //#endregion
502
+ //#region src/store.ts
503
+ /**
504
+ * Durable metric storage over the harness storage domain. The `dsh-fast`
505
+ * domain keeps one bounded history per session, so `/fast` and `fast_report`
506
+ * metrics survive a restart and the trend stays queryable without touching the
507
+ * session log (the rc.6 `Session.append` offers no `ignorable` marker and no
508
+ * external event-registration surface, so a custom session event would make
509
+ * the persistence coordinator refuse the log on restore).
510
+ * @module dsh-fast/store
511
+ */
512
+ /** Zod schema for the load section. */
513
+ const loadSchema = z$1.object({
514
+ kind: z$1.enum(["open", "restore"]),
515
+ seedEvents: z$1.number().int().nonnegative(),
516
+ timeToFirstRequestMs: z$1.number().int().nonnegative().nullable()
517
+ });
518
+ /** Zod schema for the spill section. */
519
+ const spillSchema = z$1.object({
520
+ detectedSpilledResults: z$1.number().int().nonnegative(),
521
+ heuristic: z$1.boolean()
522
+ });
523
+ /** Zod schema for the compaction section. */
524
+ const compactionSchema = z$1.object({
525
+ count: z$1.number().int().nonnegative(),
526
+ manual: z$1.number().int().nonnegative(),
527
+ automatic: z$1.number().int().nonnegative(),
528
+ shadowedTokens: z$1.number().int().nonnegative()
529
+ });
530
+ /** Zod schema for the context section. */
531
+ const contextSchema = z$1.object({
532
+ totalTokens: z$1.number().nonnegative(),
533
+ systemTokens: z$1.number().nonnegative(),
534
+ toolSchemaTokens: z$1.number().nonnegative(),
535
+ surfaceTokens: z$1.number().nonnegative(),
536
+ systemShare: z$1.number(),
537
+ toolsShare: z$1.number(),
538
+ surfaceShare: z$1.number()
539
+ });
540
+ /** Zod schema for the cache section. */
541
+ const cacheSchema = z$1.object({
542
+ inputTokens: z$1.number().int().nonnegative(),
543
+ cacheReadTokens: z$1.number().int().nonnegative(),
544
+ cacheWriteTokens: z$1.number().int().nonnegative(),
545
+ outputTokens: z$1.number().int().nonnegative(),
546
+ hitRate: z$1.number().nullable()
547
+ });
548
+ /** Zod schema for one {@link FastSnapshot}. */
549
+ const snapshotSchema = z$1.object({
550
+ load: loadSchema,
551
+ spill: spillSchema,
552
+ compaction: compactionSchema,
553
+ context: contextSchema,
554
+ cache: cacheSchema
555
+ });
556
+ /** Zod schema for one {@link StoredSample}. */
557
+ const sampleSchema = z$1.object({
558
+ at: z$1.number().int().nonnegative(),
559
+ snapshot: snapshotSchema
560
+ });
561
+ /** The per-session value: a bounded history of samples. */
562
+ const historySchema = z$1.object({ samples: z$1.array(sampleSchema) });
563
+ /** The `dsh-fast` storage-domain declaration. */
564
+ const fastDomainSpec = defineDomain({
565
+ name: "dsh_fast",
566
+ version: 1,
567
+ tables: { sessions: domainTable(historySchema) }
568
+ });
569
+ /**
570
+ * Append one snapshot to a history, keeping only the newest `maxSamples`.
571
+ * @param history - the current history (may be absent).
572
+ * @param sample - the sample to append.
573
+ * @param maxSamples - the bounded length.
574
+ * @returns the new history.
575
+ */
576
+ function appendSample(history, sample, maxSamples) {
577
+ return { samples: [...history?.samples ?? [], sample].slice(-maxSamples) };
578
+ }
579
+ //#endregion
580
+ //#region src/version.ts
581
+ /** Single-source plugin version, bumped by `scripts/release.mjs`. @module dsh-fast/version */
582
+ const VERSION = "0.1.1";
583
+ //#endregion
584
+ //#region src/index.ts
585
+ const name = "fast";
586
+ /** The `/fast` command, the `fast_report` tool, and the durable metric domain. */
587
+ const inject = [
588
+ "commands",
589
+ "tools",
590
+ "storageDomain"
591
+ ];
592
+ /**
593
+ * Mount the diagnostics. The resolved config is validated first (fail loud);
594
+ * with `enabled: false` the plugin registers nothing and stays inert.
595
+ * @param ctx - the plugin context (host).
596
+ * @param config - raw plugin config.
597
+ */
598
+ async function apply(ctx, config = {}) {
599
+ const resolved = resolveConfig(config);
600
+ const logger = ctx.logger("fast");
601
+ if (!resolved.enabled) {
602
+ logger.info("disabled: enabled is false — no diagnostics are collected");
603
+ return;
604
+ }
605
+ const collector = new FastCollector(resolved);
606
+ const domain = await ctx.storageDomain.open(fastDomainSpec);
607
+ const sessions = domain.table("sessions");
608
+ /** Lazy, contained lookup of the optional token meter. */
609
+ const measure = (session) => {
610
+ const meter = ctx.get("tokenMeter");
611
+ if (meter === void 0) return void 0;
612
+ try {
613
+ const measurement = meter.measure(session);
614
+ return {
615
+ totalTokens: measurement.totalTokens,
616
+ surfaceTokens: measurement.surfaceTokens
617
+ };
618
+ } catch (error) {
619
+ logger.warn(`token meter measurement failed: ${error instanceof Error ? error.message : String(error)}`);
620
+ return;
621
+ }
622
+ };
623
+ /** Build the complete report for one session. */
624
+ const reportFor = (session) => {
625
+ return buildReport(collector.snapshot(session, measure), {
626
+ sessionId: session.id,
627
+ ...resolved.includeCwd && session.header.cwd !== void 0 ? { cwd: session.header.cwd } : {},
628
+ generatedAt: Date.now()
629
+ }, resolved, VERSION);
630
+ };
631
+ /** Append one snapshot to the session's durable history (fire-and-forget). */
632
+ const persist = (session) => {
633
+ const snapshot = collector.snapshot(session, measure);
634
+ const next = appendSample(sessions.get(session.id), {
635
+ at: Date.now(),
636
+ snapshot
637
+ }, resolved.maxHistorySamples);
638
+ sessions.put(session.id, next).catch((error) => {
639
+ logger.warn(`session "${session.id}": persist failed: ${error instanceof Error ? error.message : String(error)}`);
640
+ });
641
+ };
642
+ ctx.commands.register({
643
+ name: "fast",
644
+ description: "Print the dsh-fast performance report for the active session.",
645
+ handler: (invocation) => {
646
+ return {
647
+ kind: "success",
648
+ text: renderFastText(reportFor(invocation.agent.session))
649
+ };
650
+ }
651
+ });
652
+ ctx.tools.register(defineTool({
653
+ name: "fast_report",
654
+ description: "Return the current dsh-fast performance report for the active session: session load timing, spill hits, compaction count and trigger, context-injection volume (AGENTS.md/skills/tool-schema token share), LLM cache hit rate, and optimization suggestions.",
655
+ parameters: {},
656
+ output: {
657
+ schema: {
658
+ type: "object",
659
+ properties: {
660
+ generator: {
661
+ type: "string",
662
+ required: true
663
+ },
664
+ version: {
665
+ type: "string",
666
+ required: true
667
+ },
668
+ sessionId: {
669
+ type: "string",
670
+ required: true
671
+ },
672
+ generatedAt: {
673
+ type: "number",
674
+ required: true
675
+ },
676
+ load: {
677
+ type: "object",
678
+ properties: {
679
+ kind: {
680
+ type: "string",
681
+ enum: ["open", "restore"],
682
+ required: true
683
+ },
684
+ seedEvents: {
685
+ type: "number",
686
+ required: true
687
+ },
688
+ timeToFirstRequestMs: {
689
+ oneOf: [{ type: "number" }, { type: "null" }],
690
+ required: true
691
+ }
692
+ },
693
+ additionalProperties: false,
694
+ required: true
695
+ },
696
+ spill: {
697
+ type: "object",
698
+ properties: {
699
+ detectedSpilledResults: {
700
+ type: "number",
701
+ required: true
702
+ },
703
+ heuristic: {
704
+ type: "boolean",
705
+ required: true
706
+ }
707
+ },
708
+ additionalProperties: false,
709
+ required: true
710
+ },
711
+ compaction: {
712
+ type: "object",
713
+ properties: {
714
+ count: {
715
+ type: "number",
716
+ required: true
717
+ },
718
+ manual: {
719
+ type: "number",
720
+ required: true
721
+ },
722
+ automatic: {
723
+ type: "number",
724
+ required: true
725
+ },
726
+ shadowedTokens: {
727
+ type: "number",
728
+ required: true
729
+ }
730
+ },
731
+ additionalProperties: false,
732
+ required: true
733
+ },
734
+ context: {
735
+ type: "object",
736
+ properties: {
737
+ totalTokens: {
738
+ type: "number",
739
+ required: true
740
+ },
741
+ systemTokens: {
742
+ type: "number",
743
+ required: true
744
+ },
745
+ toolSchemaTokens: {
746
+ type: "number",
747
+ required: true
748
+ },
749
+ surfaceTokens: {
750
+ type: "number",
751
+ required: true
752
+ },
753
+ systemShare: {
754
+ type: "number",
755
+ required: true
756
+ },
757
+ toolsShare: {
758
+ type: "number",
759
+ required: true
760
+ },
761
+ surfaceShare: {
762
+ type: "number",
763
+ required: true
764
+ }
765
+ },
766
+ additionalProperties: false,
767
+ required: true
768
+ },
769
+ cache: {
770
+ type: "object",
771
+ properties: {
772
+ inputTokens: {
773
+ type: "number",
774
+ required: true
775
+ },
776
+ cacheReadTokens: {
777
+ type: "number",
778
+ required: true
779
+ },
780
+ cacheWriteTokens: {
781
+ type: "number",
782
+ required: true
783
+ },
784
+ outputTokens: {
785
+ type: "number",
786
+ required: true
787
+ },
788
+ hitRate: {
789
+ oneOf: [{ type: "number" }, { type: "null" }],
790
+ required: true
791
+ }
792
+ },
793
+ additionalProperties: false,
794
+ required: true
795
+ },
796
+ suggestions: {
797
+ type: "array",
798
+ items: { type: "string" },
799
+ required: true
800
+ },
801
+ cwd: { type: "string" }
802
+ },
803
+ additionalProperties: false
804
+ },
805
+ render: (_args, value) => [{
806
+ type: "text",
807
+ text: renderFastText(value)
808
+ }]
809
+ },
810
+ async execute(_args, exec) {
811
+ const session = exec.agent?.session;
812
+ if (session === void 0) throw new Error("fast_report requires an agent-owned session");
813
+ return reportFor(session);
814
+ }
815
+ }));
816
+ ctx.on("session/created", (session) => {
817
+ collector.handleSessionCreated(session);
818
+ });
819
+ ctx.on("session/disposed", (session) => {
820
+ collector.handleSessionDisposed(session);
821
+ });
822
+ ctx.on("session/event", (session, event) => {
823
+ try {
824
+ collector.handleEvent(session, event);
825
+ } catch (error) {
826
+ logger.warn(`session "${session.id}": event handling failed: ${error instanceof Error ? error.message : String(error)}`);
827
+ }
828
+ });
829
+ ctx.effect(() => {
830
+ const timer = setInterval(() => {
831
+ for (const session of collector.liveSessions()) {
832
+ if (!collector.isDirty(session)) continue;
833
+ collector.markClean(session);
834
+ persist(session);
835
+ }
836
+ }, resolved.snapshotIntervalMs);
837
+ return async () => {
838
+ clearInterval(timer);
839
+ await domain.close();
840
+ };
841
+ });
842
+ }
843
+ //#endregion
844
+ export { Config, FastCollector, VERSION, appendSample, apply, buildReport, buildSuggestions, detectSpilledResult, fastDomainSpec, flattenToolResultText, historySchema, hitRateOf, inject, name, renderFastText, resolveConfig, sanitizePath, sanitizeText, sharesOf, stripControl, truncate };