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/src/index.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dsh-fast` — read-only performance diagnostics for DeepSeek Harness. Folds
|
|
3
|
+
* the `session/event` stream into session-load timing, spill-hit counts,
|
|
4
|
+
* compaction count and trigger, context-injection volume (AGENTS.md / skill
|
|
5
|
+
* directory / tool schema / surface token share), and LLM cache hit rate;
|
|
6
|
+
* surfaces them via the `/fast` slash command and the `fast_report` tool; and
|
|
7
|
+
* persists them to the harness storage domain on an async sampling timer
|
|
8
|
+
* (never on the append hot path).
|
|
9
|
+
*
|
|
10
|
+
* Function plugin — no default export (the Loader unwraps
|
|
11
|
+
* `exports.default ?? exports`, and a stray default would discard
|
|
12
|
+
* `name`/`inject`/`Config`/`apply`).
|
|
13
|
+
* @module dsh-fast
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
17
|
+
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
|
|
18
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
19
|
+
// Type-only: registers the `ctx.commands` Context merge for the inject.
|
|
20
|
+
import type {} from '@deepseek-ai/dsh-commands'
|
|
21
|
+
import { Config, resolveConfig } from './config.ts'
|
|
22
|
+
import { FastCollector } from './collector.ts'
|
|
23
|
+
import type { MeasureFn, TokenMeasurement } from './collector.ts'
|
|
24
|
+
import { buildReport, renderFastText } from './analyze.ts'
|
|
25
|
+
import type { FastReport } from './model.ts'
|
|
26
|
+
import { fastDomainSpec, appendSample } from './store.ts'
|
|
27
|
+
import { VERSION } from './version.ts'
|
|
28
|
+
|
|
29
|
+
export const name = 'fast'
|
|
30
|
+
/** The `/fast` command, the `fast_report` tool, and the durable metric domain. */
|
|
31
|
+
export const inject = ['commands', 'tools', 'storageDomain']
|
|
32
|
+
|
|
33
|
+
export { Config, resolveConfig } from './config.ts'
|
|
34
|
+
export type { Config as FastConfig, ResolvedConfig } from './config.ts'
|
|
35
|
+
export { VERSION } from './version.ts'
|
|
36
|
+
export { stripControl, truncate, sanitizeText, sanitizePath } from './sanitize.ts'
|
|
37
|
+
export type {
|
|
38
|
+
FastReport,
|
|
39
|
+
FastSnapshot,
|
|
40
|
+
LoadStats,
|
|
41
|
+
SpillStats,
|
|
42
|
+
CompactionStats,
|
|
43
|
+
ContextStats,
|
|
44
|
+
CacheStats,
|
|
45
|
+
StoredSample,
|
|
46
|
+
} from './model.ts'
|
|
47
|
+
export { FastCollector, detectSpilledResult, flattenToolResultText, sharesOf, hitRateOf } from './collector.ts'
|
|
48
|
+
export { buildReport, buildSuggestions, renderFastText } from './analyze.ts'
|
|
49
|
+
export { fastDomainSpec, appendSample, historySchema } from './store.ts'
|
|
50
|
+
|
|
51
|
+
/** The structural surface of the optional `ctx.tokenMeter` service. */
|
|
52
|
+
interface TokenMeterService {
|
|
53
|
+
measure(session: Session): TokenMeasurement
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Mount the diagnostics. The resolved config is validated first (fail loud);
|
|
58
|
+
* with `enabled: false` the plugin registers nothing and stays inert.
|
|
59
|
+
* @param ctx - the plugin context (host).
|
|
60
|
+
* @param config - raw plugin config.
|
|
61
|
+
*/
|
|
62
|
+
export async function apply(ctx: Context, config: Config = {}): Promise<void> {
|
|
63
|
+
const resolved = resolveConfig(config)
|
|
64
|
+
const logger = ctx.logger('fast')
|
|
65
|
+
if (!resolved.enabled) {
|
|
66
|
+
logger.info('disabled: enabled is false — no diagnostics are collected')
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const collector = new FastCollector(resolved)
|
|
71
|
+
const domain = await ctx.storageDomain.open(fastDomainSpec)
|
|
72
|
+
const sessions = domain.table('sessions')
|
|
73
|
+
|
|
74
|
+
/** Lazy, contained lookup of the optional token meter. */
|
|
75
|
+
const measure: MeasureFn = (session) => {
|
|
76
|
+
const meter = ctx.get('tokenMeter') as unknown as TokenMeterService | undefined
|
|
77
|
+
if (meter === undefined) return undefined
|
|
78
|
+
try {
|
|
79
|
+
const measurement = meter.measure(session)
|
|
80
|
+
return { totalTokens: measurement.totalTokens, surfaceTokens: measurement.surfaceTokens }
|
|
81
|
+
} catch (error) {
|
|
82
|
+
logger.warn(`token meter measurement failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
83
|
+
return undefined
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Build the complete report for one session. */
|
|
88
|
+
const reportFor = (session: Session): FastReport => {
|
|
89
|
+
const snapshot = collector.snapshot(session, measure)
|
|
90
|
+
return buildReport(
|
|
91
|
+
snapshot,
|
|
92
|
+
{
|
|
93
|
+
sessionId: session.id,
|
|
94
|
+
...(resolved.includeCwd && session.header.cwd !== undefined ? { cwd: session.header.cwd } : {}),
|
|
95
|
+
generatedAt: Date.now(),
|
|
96
|
+
},
|
|
97
|
+
resolved,
|
|
98
|
+
VERSION,
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Append one snapshot to the session's durable history (fire-and-forget). */
|
|
103
|
+
const persist = (session: Session): void => {
|
|
104
|
+
const snapshot = collector.snapshot(session, measure)
|
|
105
|
+
const next = appendSample(sessions.get(session.id), { at: Date.now(), snapshot }, resolved.maxHistorySamples)
|
|
106
|
+
void sessions.put(session.id, next).catch((error: unknown) => {
|
|
107
|
+
logger.warn(`session "${session.id}": persist failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Human slash command: the on-demand report.
|
|
112
|
+
ctx.commands.register({
|
|
113
|
+
name: 'fast',
|
|
114
|
+
description: 'Print the dsh-fast performance report for the active session.',
|
|
115
|
+
handler: (invocation) => {
|
|
116
|
+
const report = reportFor(invocation.agent.session)
|
|
117
|
+
return { kind: 'success', text: renderFastText(report) }
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// Model tool: the same report as structured data.
|
|
122
|
+
ctx.tools.register(defineTool({
|
|
123
|
+
name: 'fast_report',
|
|
124
|
+
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.',
|
|
125
|
+
parameters: {},
|
|
126
|
+
output: {
|
|
127
|
+
schema: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
generator: { type: 'string', required: true },
|
|
131
|
+
version: { type: 'string', required: true },
|
|
132
|
+
sessionId: { type: 'string', required: true },
|
|
133
|
+
generatedAt: { type: 'number', required: true },
|
|
134
|
+
load: {
|
|
135
|
+
type: 'object',
|
|
136
|
+
properties: {
|
|
137
|
+
kind: { type: 'string', enum: ['open', 'restore'], required: true },
|
|
138
|
+
seedEvents: { type: 'number', required: true },
|
|
139
|
+
timeToFirstRequestMs: { oneOf: [{ type: 'number' }, { type: 'null' }], required: true },
|
|
140
|
+
},
|
|
141
|
+
additionalProperties: false,
|
|
142
|
+
required: true,
|
|
143
|
+
},
|
|
144
|
+
spill: {
|
|
145
|
+
type: 'object',
|
|
146
|
+
properties: {
|
|
147
|
+
detectedSpilledResults: { type: 'number', required: true },
|
|
148
|
+
heuristic: { type: 'boolean', required: true },
|
|
149
|
+
},
|
|
150
|
+
additionalProperties: false,
|
|
151
|
+
required: true,
|
|
152
|
+
},
|
|
153
|
+
compaction: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
properties: {
|
|
156
|
+
count: { type: 'number', required: true },
|
|
157
|
+
manual: { type: 'number', required: true },
|
|
158
|
+
automatic: { type: 'number', required: true },
|
|
159
|
+
shadowedTokens: { type: 'number', required: true },
|
|
160
|
+
},
|
|
161
|
+
additionalProperties: false,
|
|
162
|
+
required: true,
|
|
163
|
+
},
|
|
164
|
+
context: {
|
|
165
|
+
type: 'object',
|
|
166
|
+
properties: {
|
|
167
|
+
totalTokens: { type: 'number', required: true },
|
|
168
|
+
systemTokens: { type: 'number', required: true },
|
|
169
|
+
toolSchemaTokens: { type: 'number', required: true },
|
|
170
|
+
surfaceTokens: { type: 'number', required: true },
|
|
171
|
+
systemShare: { type: 'number', required: true },
|
|
172
|
+
toolsShare: { type: 'number', required: true },
|
|
173
|
+
surfaceShare: { type: 'number', required: true },
|
|
174
|
+
},
|
|
175
|
+
additionalProperties: false,
|
|
176
|
+
required: true,
|
|
177
|
+
},
|
|
178
|
+
cache: {
|
|
179
|
+
type: 'object',
|
|
180
|
+
properties: {
|
|
181
|
+
inputTokens: { type: 'number', required: true },
|
|
182
|
+
cacheReadTokens: { type: 'number', required: true },
|
|
183
|
+
cacheWriteTokens: { type: 'number', required: true },
|
|
184
|
+
outputTokens: { type: 'number', required: true },
|
|
185
|
+
hitRate: { oneOf: [{ type: 'number' }, { type: 'null' }], required: true },
|
|
186
|
+
},
|
|
187
|
+
additionalProperties: false,
|
|
188
|
+
required: true,
|
|
189
|
+
},
|
|
190
|
+
suggestions: { type: 'array', items: { type: 'string' }, required: true },
|
|
191
|
+
cwd: { type: 'string' },
|
|
192
|
+
},
|
|
193
|
+
additionalProperties: false,
|
|
194
|
+
},
|
|
195
|
+
render: (_args, value) => [{ type: 'text', text: renderFastText(value as FastReport) }],
|
|
196
|
+
},
|
|
197
|
+
async execute(_args, exec) {
|
|
198
|
+
const session = exec.agent?.session
|
|
199
|
+
if (session === undefined) {
|
|
200
|
+
throw new Error('fast_report requires an agent-owned session')
|
|
201
|
+
}
|
|
202
|
+
return reportFor(session)
|
|
203
|
+
},
|
|
204
|
+
}))
|
|
205
|
+
|
|
206
|
+
// Session lifecycle: adopt and fold.
|
|
207
|
+
ctx.on('session/created', (session: Session) => {
|
|
208
|
+
collector.handleSessionCreated(session)
|
|
209
|
+
})
|
|
210
|
+
ctx.on('session/disposed', (session: Session) => {
|
|
211
|
+
collector.handleSessionDisposed(session)
|
|
212
|
+
})
|
|
213
|
+
ctx.on('session/event', (session: Session, event: SessionEvent) => {
|
|
214
|
+
try {
|
|
215
|
+
collector.handleEvent(session, event)
|
|
216
|
+
} catch (error) {
|
|
217
|
+
logger.warn(`session "${session.id}": event handling failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
// Async sampling: one effect owns the timer and the domain teardown.
|
|
222
|
+
ctx.effect(() => {
|
|
223
|
+
const timer = setInterval(() => {
|
|
224
|
+
for (const session of collector.liveSessions()) {
|
|
225
|
+
if (!collector.isDirty(session)) continue
|
|
226
|
+
collector.markClean(session)
|
|
227
|
+
persist(session)
|
|
228
|
+
}
|
|
229
|
+
}, resolved.snapshotIntervalMs)
|
|
230
|
+
return async () => {
|
|
231
|
+
clearInterval(timer)
|
|
232
|
+
await domain.close()
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
}
|
package/src/model.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owned JSON data model shared by the `/fast` command, the `fast_report` tool,
|
|
3
|
+
* and the `fast/*` session events. Everything here is plain lossless-JSON data
|
|
4
|
+
* already detached from live harness objects, so a `FastSnapshot` is exactly
|
|
5
|
+
* what gets appended to the session log and a `FastReport` is exactly what the
|
|
6
|
+
* model-facing tool returns. No Cordis/Session reference crosses the tool or
|
|
7
|
+
* the session-log boundary.
|
|
8
|
+
* @module dsh-fast/model
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** One session-load measurement: how a session became ready to serve. */
|
|
12
|
+
export interface LoadStats {
|
|
13
|
+
/** `open` for a fresh session, `restore` for a resumed/seeded one. */
|
|
14
|
+
kind: 'open' | 'restore'
|
|
15
|
+
/** Number of restored/replayed seed events at construction (0 for a fresh open). */
|
|
16
|
+
seedEvents: number
|
|
17
|
+
/** Publication-to-first-request latency in milliseconds; `null` before the first request. */
|
|
18
|
+
timeToFirstRequestMs: number | null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** One durable sample persisted to the storage domain, with its capture time. */
|
|
22
|
+
export interface StoredSample {
|
|
23
|
+
at: number
|
|
24
|
+
snapshot: FastSnapshot
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Spill-hit statistics derived from the durable tool results. */
|
|
28
|
+
export interface SpillStats {
|
|
29
|
+
/** Tool results detected as spilled to a session-scoped artifact (best-effort heuristic). */
|
|
30
|
+
detectedSpilledResults: number
|
|
31
|
+
/** Always true: spill detection reads the durable notice marker, not a dedicated event. */
|
|
32
|
+
heuristic: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Compaction count and trigger attribution over the durable log. */
|
|
36
|
+
export interface CompactionStats {
|
|
37
|
+
/** Total compaction attempts (one `compaction/start` each). */
|
|
38
|
+
count: number
|
|
39
|
+
/** Compactions started by a slash command (`sourceCommandId` present). */
|
|
40
|
+
manual: number
|
|
41
|
+
/** Compactions started by automatic pressure (`sourceCommandId` absent). */
|
|
42
|
+
automatic: number
|
|
43
|
+
/** Sum of `shadowedTokenCount` across completed summaries. */
|
|
44
|
+
shadowedTokens: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Context-injection volume: where the request context's tokens live. */
|
|
48
|
+
export interface ContextStats {
|
|
49
|
+
/** Canonical current pressure (token-meter total), or the heuristic header+surface sum. */
|
|
50
|
+
totalTokens: number
|
|
51
|
+
/** Assembled system-prompt tokens — AGENTS.md, skill directory, persona, and harness instructions. */
|
|
52
|
+
systemTokens: number
|
|
53
|
+
/** Tool-schema tokens. */
|
|
54
|
+
toolSchemaTokens: number
|
|
55
|
+
/** Conversation-surface tokens (message history). */
|
|
56
|
+
surfaceTokens: number
|
|
57
|
+
/** `systemTokens / totalTokens` (0 when the total is 0). */
|
|
58
|
+
systemShare: number
|
|
59
|
+
/** `toolSchemaTokens / totalTokens` (0 when the total is 0). */
|
|
60
|
+
toolsShare: number
|
|
61
|
+
/** `surfaceTokens / totalTokens` (0 when the total is 0). */
|
|
62
|
+
surfaceShare: number
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** LLM cache accounting aggregated from `assistant/message` usage. */
|
|
66
|
+
export interface CacheStats {
|
|
67
|
+
inputTokens: number
|
|
68
|
+
cacheReadTokens: number
|
|
69
|
+
cacheWriteTokens: number
|
|
70
|
+
outputTokens: number
|
|
71
|
+
/** `cacheRead / (input + cacheRead)`; `null` when no input-plus-cache-read tokens exist. */
|
|
72
|
+
hitRate: number | null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The complete metric snapshot persisted to the storage domain. */
|
|
76
|
+
export interface FastSnapshot {
|
|
77
|
+
load: LoadStats
|
|
78
|
+
spill: SpillStats
|
|
79
|
+
compaction: CompactionStats
|
|
80
|
+
context: ContextStats
|
|
81
|
+
cache: CacheStats
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The complete model- and human-facing report (`/fast` and `fast_report`). */
|
|
85
|
+
export interface FastReport extends FastSnapshot {
|
|
86
|
+
/** `dsh-fast`. */
|
|
87
|
+
generator: string
|
|
88
|
+
/** Plugin version. */
|
|
89
|
+
version: string
|
|
90
|
+
/** Sanitized session identity. */
|
|
91
|
+
sessionId: string
|
|
92
|
+
/** Epoch milliseconds of report generation. */
|
|
93
|
+
generatedAt: number
|
|
94
|
+
/** Optimization suggestions, already localized to plain strings. */
|
|
95
|
+
suggestions: string[]
|
|
96
|
+
/** Sanitized session working directory; present only when `privacy.includeCwd` is enabled. */
|
|
97
|
+
cwd?: string
|
|
98
|
+
}
|
package/src/sanitize.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure display/durable-boundary sanitization. Any free-form string that can
|
|
3
|
+
* reach a session event or a model-facing report (session identity, working
|
|
4
|
+
* directory, labels) passes through these functions first, so control
|
|
5
|
+
* characters never enter the log and no string exceeds its budget. These are
|
|
6
|
+
* pure functions of their inputs.
|
|
7
|
+
* @module dsh-fast/sanitize
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** C0 control characters plus DEL, replaced before any output. */
|
|
11
|
+
const CONTROL_CHARS = /[\u0000-\u001f\u007f]/gu
|
|
12
|
+
|
|
13
|
+
/** Ellipsis appended when a string is truncated. */
|
|
14
|
+
const ELLIPSIS = '…'
|
|
15
|
+
|
|
16
|
+
/** Remove control characters from a string. */
|
|
17
|
+
export function stripControl(value: string): string {
|
|
18
|
+
return value.replace(CONTROL_CHARS, '')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Truncate a string to `maxChars`, appending an ellipsis when it is cut.
|
|
23
|
+
* @param value - the string to bound.
|
|
24
|
+
* @param maxChars - non-negative budget; 0 yields the empty string.
|
|
25
|
+
* @returns the bounded string.
|
|
26
|
+
*/
|
|
27
|
+
export function truncate(value: string, maxChars: number): string {
|
|
28
|
+
if (!Number.isSafeInteger(maxChars) || maxChars < 0) {
|
|
29
|
+
throw new TypeError(`maxChars must be a non-negative safe integer, got ${String(maxChars)}`)
|
|
30
|
+
}
|
|
31
|
+
if (value.length <= maxChars) return value
|
|
32
|
+
if (maxChars === 0) return ''
|
|
33
|
+
return value.slice(0, maxChars) + ELLIPSIS
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Sanitize a free-form label: strip control characters, then bound length.
|
|
38
|
+
* @param value - the label (e.g. a session id).
|
|
39
|
+
* @param maxChars - non-negative budget.
|
|
40
|
+
* @returns the sanitized label.
|
|
41
|
+
*/
|
|
42
|
+
export function sanitizeText(value: string, maxChars: number): string {
|
|
43
|
+
return truncate(stripControl(value), maxChars)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Sanitize a path or filename, preserving its tail (basename) when truncating
|
|
48
|
+
* so the most diagnostic part survives. Control characters are stripped first.
|
|
49
|
+
* @param value - the path or filename.
|
|
50
|
+
* @param maxChars - non-negative budget.
|
|
51
|
+
* @returns the sanitized path.
|
|
52
|
+
*/
|
|
53
|
+
export function sanitizePath(value: string, maxChars: number): string {
|
|
54
|
+
const clean = stripControl(value)
|
|
55
|
+
if (clean.length <= maxChars) return clean
|
|
56
|
+
if (maxChars <= 1) return ELLIPSIS
|
|
57
|
+
const head = Math.ceil(maxChars / 2)
|
|
58
|
+
const tail = Math.floor(maxChars / 2)
|
|
59
|
+
return clean.slice(0, head) + ELLIPSIS + clean.slice(-tail)
|
|
60
|
+
}
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable metric storage over the harness storage domain. The `dsh-fast`
|
|
3
|
+
* domain keeps one bounded history per session, so `/fast` and `fast_report`
|
|
4
|
+
* metrics survive a restart and the trend stays queryable without touching the
|
|
5
|
+
* session log (the rc.6 `Session.append` offers no `ignorable` marker and no
|
|
6
|
+
* external event-registration surface, so a custom session event would make
|
|
7
|
+
* the persistence coordinator refuse the log on restore).
|
|
8
|
+
* @module dsh-fast/store
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import z from 'zod'
|
|
12
|
+
import { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain'
|
|
13
|
+
import type { FastSnapshot, StoredSample } from './model.ts'
|
|
14
|
+
|
|
15
|
+
/** Zod schema for the load section. */
|
|
16
|
+
const loadSchema = z.object({
|
|
17
|
+
kind: z.enum(['open', 'restore']),
|
|
18
|
+
seedEvents: z.number().int().nonnegative(),
|
|
19
|
+
timeToFirstRequestMs: z.number().int().nonnegative().nullable(),
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
/** Zod schema for the spill section. */
|
|
23
|
+
const spillSchema = z.object({
|
|
24
|
+
detectedSpilledResults: z.number().int().nonnegative(),
|
|
25
|
+
heuristic: z.boolean(),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
/** Zod schema for the compaction section. */
|
|
29
|
+
const compactionSchema = z.object({
|
|
30
|
+
count: z.number().int().nonnegative(),
|
|
31
|
+
manual: z.number().int().nonnegative(),
|
|
32
|
+
automatic: z.number().int().nonnegative(),
|
|
33
|
+
shadowedTokens: z.number().int().nonnegative(),
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
/** Zod schema for the context section. */
|
|
37
|
+
const contextSchema = z.object({
|
|
38
|
+
totalTokens: z.number().nonnegative(),
|
|
39
|
+
systemTokens: z.number().nonnegative(),
|
|
40
|
+
toolSchemaTokens: z.number().nonnegative(),
|
|
41
|
+
surfaceTokens: z.number().nonnegative(),
|
|
42
|
+
systemShare: z.number(),
|
|
43
|
+
toolsShare: z.number(),
|
|
44
|
+
surfaceShare: z.number(),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
/** Zod schema for the cache section. */
|
|
48
|
+
const cacheSchema = z.object({
|
|
49
|
+
inputTokens: z.number().int().nonnegative(),
|
|
50
|
+
cacheReadTokens: z.number().int().nonnegative(),
|
|
51
|
+
cacheWriteTokens: z.number().int().nonnegative(),
|
|
52
|
+
outputTokens: z.number().int().nonnegative(),
|
|
53
|
+
hitRate: z.number().nullable(),
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
/** Zod schema for one {@link FastSnapshot}. */
|
|
57
|
+
const snapshotSchema = z.object({
|
|
58
|
+
load: loadSchema,
|
|
59
|
+
spill: spillSchema,
|
|
60
|
+
compaction: compactionSchema,
|
|
61
|
+
context: contextSchema,
|
|
62
|
+
cache: cacheSchema,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
/** Zod schema for one {@link StoredSample}. */
|
|
66
|
+
const sampleSchema = z.object({
|
|
67
|
+
at: z.number().int().nonnegative(),
|
|
68
|
+
snapshot: snapshotSchema,
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
/** The per-session value: a bounded history of samples. */
|
|
72
|
+
export const historySchema = z.object({
|
|
73
|
+
samples: z.array(sampleSchema),
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
/** The per-session history value type. */
|
|
77
|
+
export interface HistoryValue {
|
|
78
|
+
samples: StoredSample[]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The `dsh-fast` storage-domain declaration. */
|
|
82
|
+
export const fastDomainSpec = defineDomain({
|
|
83
|
+
name: 'dsh_fast',
|
|
84
|
+
version: 1,
|
|
85
|
+
tables: {
|
|
86
|
+
sessions: domainTable<string, HistoryValue>(historySchema),
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Append one snapshot to a history, keeping only the newest `maxSamples`.
|
|
92
|
+
* @param history - the current history (may be absent).
|
|
93
|
+
* @param sample - the sample to append.
|
|
94
|
+
* @param maxSamples - the bounded length.
|
|
95
|
+
* @returns the new history.
|
|
96
|
+
*/
|
|
97
|
+
export function appendSample(history: HistoryValue | undefined, sample: StoredSample, maxSamples: number): HistoryValue {
|
|
98
|
+
const samples = [...(history?.samples ?? []), sample]
|
|
99
|
+
return { samples: samples.slice(-maxSamples) }
|
|
100
|
+
}
|
package/src/version.ts
ADDED