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.
- package/CHANGELOG.md +21 -0
- package/LICENSE +201 -0
- package/README.es.md +166 -0
- package/README.hi.md +166 -0
- package/README.md +166 -0
- package/README.pt.md +166 -0
- package/README.zh.md +166 -0
- package/THIRD_PARTY_NOTICES.md +20 -0
- package/cordis.patch.yml +55 -0
- package/lib/index.js +844 -0
- package/lib/types/analyze.d.ts +41 -0
- package/lib/types/analyze.d.ts.map +1 -0
- package/lib/types/analyze.js +121 -0
- package/lib/types/analyze.js.map +1 -0
- package/lib/types/collector.d.ts +82 -0
- package/lib/types/collector.d.ts.map +1 -0
- package/lib/types/collector.js +236 -0
- package/lib/types/collector.js.map +1 -0
- package/lib/types/config.d.ts +76 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/config.js +93 -0
- package/lib/types/config.js.map +1 -0
- package/lib/types/estimate.d.ts +24 -0
- package/lib/types/estimate.d.ts.map +1 -0
- package/lib/types/estimate.js +37 -0
- package/lib/types/estimate.js.map +1 -0
- package/lib/types/index.d.ts +35 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +201 -0
- package/lib/types/index.js.map +1 -0
- package/lib/types/model.d.ts +91 -0
- package/lib/types/model.d.ts.map +1 -0
- package/lib/types/model.js +11 -0
- package/lib/types/model.js.map +1 -0
- package/lib/types/sanitize.d.ts +33 -0
- package/lib/types/sanitize.d.ts.map +1 -0
- package/lib/types/sanitize.js +59 -0
- package/lib/types/sanitize.js.map +1 -0
- package/lib/types/store.d.ts +74 -0
- package/lib/types/store.d.ts.map +1 -0
- package/lib/types/store.js +84 -0
- package/lib/types/store.js.map +1 -0
- package/lib/types/version.d.ts +3 -0
- package/lib/types/version.d.ts.map +1 -0
- package/lib/types/version.js +3 -0
- package/lib/types/version.js.map +1 -0
- package/package.json +147 -0
- package/src/analyze.ts +148 -0
- package/src/collector.ts +289 -0
- package/src/config.ts +163 -0
- package/src/estimate.ts +41 -0
- package/src/index.ts +235 -0
- package/src/model.ts +98 -0
- package/src/sanitize.ts +60 -0
- package/src/store.ts +100 -0
- 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 };
|