dsh-context 0.65.0 → 0.66.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -7
- package/lib/client.js +30 -14311
- package/lib/index.d.ts +102 -39
- package/lib/index.js +641 -1606
- package/package.json +16 -15
package/lib/index.js
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-context v0.66.0
|
|
3
|
+
* A DeepSeek Harness plugin for context insight and management, with context dashboard and context command, for understanding how the context is made of, and how it evolves.
|
|
4
|
+
* @author bowenliang123
|
|
5
|
+
* @license Apache-2.0
|
|
6
|
+
* @homepage https://github.com/bowenliang123/dsh-context
|
|
7
|
+
* @built 2026-10-08T17:44:49+00:00
|
|
8
|
+
* @commit f60e1a56439b
|
|
9
|
+
* @commit-date 2026-10-09T01:41:58+08:00
|
|
10
|
+
*/
|
|
11
|
+
|
|
1
12
|
import { createRequire } from "node:module";
|
|
2
13
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
14
|
+
import { deriveEventMessage, interruptedTurnClosers } from "@deepseek-ai/dsh-session";
|
|
15
|
+
import z$1 from "@deepseek-ai/schemastery";
|
|
4
16
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
17
|
import { dirname, isAbsolute, join, normalize, relative } from "node:path";
|
|
6
18
|
import { fileURLToPath } from "node:url";
|
|
7
19
|
import { getHeapStatistics } from "node:v8";
|
|
8
|
-
import z$1 from "@deepseek-ai/schemastery";
|
|
9
20
|
import { randomUUID } from "node:crypto";
|
|
10
21
|
//#region src/shared/providers.ts
|
|
11
|
-
/**
|
|
12
|
-
* The provider-id seam between dsh request envelopes and the models.dev
|
|
13
|
-
* registry: client/cost.ts resolves price-book branches through it, and
|
|
14
|
-
* host/fold.ts uses the DeepSeek resolution to split the session-cost totals
|
|
15
|
-
* into peak/off-peak periods. Only the renames live here — a dsh route key
|
|
16
|
-
* that is not a registry id (user-declared gateways, `claude`/`codex`
|
|
17
|
-
* subscriptions, org-prefixed catalogs) passes through and prices through
|
|
18
|
-
* the model-side resolution index instead (client/cost.ts).
|
|
19
|
-
*/
|
|
20
22
|
const MODELS_DEV_PROVIDER_IDS = {
|
|
21
23
|
"deepseek-official": "deepseek",
|
|
22
24
|
"deepseek-account": "deepseek",
|
|
@@ -24,38 +26,20 @@ const MODELS_DEV_PROVIDER_IDS = {
|
|
|
24
26
|
"minimax-cn": "minimax",
|
|
25
27
|
"zai-coding-cn": "zhipuai"
|
|
26
28
|
};
|
|
27
|
-
/** The models.dev provider id that prices a dsh provider (identity for unmapped ids). */
|
|
28
29
|
function modelsDevProviderOf(dshProviderId) {
|
|
29
30
|
return MODELS_DEV_PROVIDER_IDS[dshProviderId] ?? dshProviderId;
|
|
30
31
|
}
|
|
31
|
-
/** Whether a dsh provider prices through DeepSeek's period-based list (peak / half-price off-peak). */
|
|
32
32
|
function isDeepSeekProvider(dshProviderId) {
|
|
33
33
|
return modelsDevProviderOf(dshProviderId) === "deepseek";
|
|
34
34
|
}
|
|
35
35
|
//#endregion
|
|
36
36
|
//#region src/shared/estimate.ts
|
|
37
|
-
/**
|
|
38
|
-
* Token heuristics shared by the host fold and the client boundary — the
|
|
39
|
-
* harness token-meter's own fixed-density figure (dsh-token-meter/estimate.ts:
|
|
40
|
-
* ~4 chars ≈ 1 token, +4 role framing). Priced identically on both sides so a
|
|
41
|
-
* legacy value normalized at the client boundary matches what the host view
|
|
42
|
-
* would have served.
|
|
43
|
-
*/
|
|
44
37
|
const CHARS_PER_TOKEN$1 = 4;
|
|
45
38
|
const ROLE_OVERHEAD$1 = 4;
|
|
46
|
-
/** Price rendered system-prompt text; 0 for absent/empty/non-string input. */
|
|
47
39
|
function estimateSystemTokens(text) {
|
|
48
40
|
if (typeof text !== "string" || text.length === 0) return 0;
|
|
49
41
|
return Math.ceil(text.length / CHARS_PER_TOKEN$1) + ROLE_OVERHEAD$1;
|
|
50
42
|
}
|
|
51
|
-
/**
|
|
52
|
-
* Price a `system/message` payload's content exactly like the harness's
|
|
53
|
-
* token-meter (`estimateSystemMessage`): text density over EVERY text block
|
|
54
|
-
* plus role framing, with no per-block overhead — an adapter serializes the
|
|
55
|
-
* prompt as plain text, so a text block costs its characters alone. Any other
|
|
56
|
-
* block (or a hostile element) falls back to its JSON length. 0 for empty
|
|
57
|
-
* content, which the harness reads as "no system prompt".
|
|
58
|
-
*/
|
|
59
43
|
function estimateSystemContent(blocks) {
|
|
60
44
|
if (!Array.isArray(blocks) || blocks.length === 0) return 0;
|
|
61
45
|
let characters = 0;
|
|
@@ -74,30 +58,9 @@ function estimateSystemContent(blocks) {
|
|
|
74
58
|
}
|
|
75
59
|
//#endregion
|
|
76
60
|
//#region src/shared/imageTokens.ts
|
|
77
|
-
/**
|
|
78
|
-
* Per-image token estimate for DeepSeek's vision model — a faithful port of
|
|
79
|
-
* the official "图片 Token 计算器" (Image Token Calculator) shipped on the
|
|
80
|
-
* DeepSeek API docs (https://api-docs.deepseek.com/zh-cn/quick_start/token_usage),
|
|
81
|
-
* which implements the provider's own image→token conversion:
|
|
82
|
-
*
|
|
83
|
-
* - every image is aspect-preserved rescaled before entering the model:
|
|
84
|
-
* below ~384×384 total pixels it is enlarged, above it is shrunk;
|
|
85
|
-
* - tokens follow the patch grid (patch 14px, downsample 3), so every
|
|
86
|
-
* image costs at least 117 and at most ~384 tokens (the documented cap).
|
|
87
|
-
*
|
|
88
|
-
* Verified against the docs calculator itself: 2048×1365→313, 800×600→341,
|
|
89
|
-
* 2048×2048→349, 512×512→201, 100×100→117, 1920×1080→369, 400×900→249.
|
|
90
|
-
* The DSH request pipeline's own 640k-pixel pre-resize does not change the
|
|
91
|
-
* result (the provider formula rescales to the same patch grid), so the
|
|
92
|
-
* durable attachment dimensions can be fed in directly.
|
|
93
|
-
*
|
|
94
|
-
* Pure math shared by the Host fold (message pricing) and the Client
|
|
95
|
-
* (attachment card token badges) — no dependencies, never mutates.
|
|
96
|
-
*/
|
|
97
61
|
const PATCH_SIZE = 14;
|
|
98
62
|
const DOWNSAMPLE_RATIO = 3;
|
|
99
63
|
const MAX_WH_RATIO = 8;
|
|
100
|
-
/** ~384×384 total pixels: smaller images are enlarged before patching. */
|
|
101
64
|
const MIN_PIXELS = 147456;
|
|
102
65
|
const floorDiv = (a, b) => Math.floor(a / b);
|
|
103
66
|
const ceilDiv = (a, b) => Math.floor((a + b - 1) / b);
|
|
@@ -107,7 +70,6 @@ function gridTokens(rows, cols) {
|
|
|
107
70
|
n += ceilDiv(rows, 2) * (cols + 1) % 2 * 2;
|
|
108
71
|
return n;
|
|
109
72
|
}
|
|
110
|
-
/** Solve the largest in-grid resize whose token count fits `budget`. */
|
|
111
73
|
function solveResizeRatio(height, width, budget) {
|
|
112
74
|
const ratio = height / width;
|
|
113
75
|
const gridW = Math.sqrt((budget - 2) / ratio + .25) - .5;
|
|
@@ -117,8 +79,6 @@ function solveResizeRatio(height, width, budget) {
|
|
|
117
79
|
let bestWidth;
|
|
118
80
|
if (gridW < 1) {
|
|
119
81
|
let rows = floorDiv(budget - 2, 2);
|
|
120
|
-
/* v8 ignore else -- budget enters at 381 and the tall solve always fits
|
|
121
|
-
(never decrements), so rows is always odd here; parity-defensive. */
|
|
122
82
|
if (rows % 2 === 1) rows -= 1;
|
|
123
83
|
bestWidth = unit;
|
|
124
84
|
bestHeight = rows * unit;
|
|
@@ -145,7 +105,6 @@ function solveResizeRatio(height, width, budget) {
|
|
|
145
105
|
numTokens: gridTokens(nLlmH, nLlmW)
|
|
146
106
|
};
|
|
147
107
|
}
|
|
148
|
-
/** Resize so the patch grid fits the cap, then re-add the pad reserve. */
|
|
149
108
|
function safeResize(height, width, paddedHeight, paddedWidth) {
|
|
150
109
|
const nLlmH = ceilDiv(floorDiv(paddedHeight, PATCH_SIZE), DOWNSAMPLE_RATIO);
|
|
151
110
|
const nLlmW = ceilDiv(floorDiv(paddedWidth, PATCH_SIZE), DOWNSAMPLE_RATIO);
|
|
@@ -183,10 +142,6 @@ function calcResizeInner(width, height) {
|
|
|
183
142
|
const paddedHeight = ceilDiv(h, PATCH_SIZE) * PATCH_SIZE;
|
|
184
143
|
return safeResize(h, w, paddedHeight, paddedWidth);
|
|
185
144
|
}
|
|
186
|
-
/**
|
|
187
|
-
* Estimate the tokens one image consumes in a DeepSeek vision request from its pixel dimensions. Returns null for non-positive/non-finite
|
|
188
|
-
* dimensions or when the official iteration fails to converge — callers fall back to the generic structural price.
|
|
189
|
-
*/
|
|
190
145
|
function estimateImageTokens(width, height) {
|
|
191
146
|
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return null;
|
|
192
147
|
try {
|
|
@@ -198,35 +153,17 @@ function estimateImageTokens(width, height) {
|
|
|
198
153
|
}
|
|
199
154
|
return null;
|
|
200
155
|
} catch {
|
|
201
|
-
/* v8 ignore next -- the only throw site is the clamped-away budget guard
|
|
202
|
-
above; the catch keeps a hostile dimension from ever breaking the fold. */
|
|
203
156
|
return null;
|
|
204
157
|
}
|
|
205
158
|
}
|
|
206
159
|
//#endregion
|
|
207
160
|
//#region src/host/pricing.ts
|
|
208
|
-
/**
|
|
209
|
-
* Token pricing — the same fixed-density heuristic as the harness's own
|
|
210
|
-
* token-meter (`dsh-token-meter/estimate.ts`): ~4 chars ≈ 1 token, +4 per
|
|
211
|
-
* content block, +4 role framing. Pure functions over message payloads.
|
|
212
|
-
*
|
|
213
|
-
* One deliberate refinement over the meter: `image` blocks. The meter prices
|
|
214
|
-
* them through its generic JSON branch (~40 tokens for the durable ref),
|
|
215
|
-
* while DeepSeek's vision model actually bills 117-384 tokens per image by
|
|
216
|
-
* pixel dimensions (https://api-docs.deepseek.com/zh-cn/guides/vision/).
|
|
217
|
-
* Image blocks therefore price through the official docs calculator port
|
|
218
|
-
* (shared/imageTokens.ts), falling back to the meter's JSON price when the
|
|
219
|
-
* attachment's dimensions are unknown.
|
|
220
|
-
*/
|
|
221
161
|
const CHARS_PER_TOKEN = 4;
|
|
222
162
|
const BLOCK_OVERHEAD = 4;
|
|
223
163
|
const ROLE_OVERHEAD = 4;
|
|
224
164
|
function estimateToolsTotal(tools) {
|
|
225
165
|
return tools.length > 0 ? Math.ceil(JSON.stringify(tools).length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD : 0;
|
|
226
166
|
}
|
|
227
|
-
/** The `ContentBlock` walkers take `unknown`: block arrays ride the untrusted
|
|
228
|
-
* log, so their element shapes (null and primitives included) are re-proved
|
|
229
|
-
* here, not trusted from the declared message types. */
|
|
230
167
|
function estimateBlocks(blocks) {
|
|
231
168
|
let tokens = 0;
|
|
232
169
|
if (!Array.isArray(blocks)) return 0;
|
|
@@ -244,9 +181,6 @@ function estimateBlocks(blocks) {
|
|
|
244
181
|
case "tool-call":
|
|
245
182
|
tokens += Math.ceil((block.name || "").length / CHARS_PER_TOKEN) + Math.ceil((block.arguments || "").length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD;
|
|
246
183
|
break;
|
|
247
|
-
case "tool-result":
|
|
248
|
-
tokens += estimateBlocks(block.content) + BLOCK_OVERHEAD;
|
|
249
|
-
break;
|
|
250
184
|
case "image": {
|
|
251
185
|
const ref = block.attachment;
|
|
252
186
|
const priced = ref !== null && typeof ref === "object" && typeof ref.width === "number" && typeof ref.height === "number" ? estimateImageTokens(ref.width, ref.height) : null;
|
|
@@ -258,24 +192,13 @@ function estimateBlocks(blocks) {
|
|
|
258
192
|
}
|
|
259
193
|
return tokens;
|
|
260
194
|
}
|
|
261
|
-
/**
|
|
262
|
-
* Price one surface message exactly like dsh's token-meter estimate:
|
|
263
|
-
* an empty-content assistant/developer event projects to NO message, so it
|
|
264
|
-
* prices 0; every other message pays content + role framing.
|
|
265
|
-
*/
|
|
266
195
|
function estimateMessage(message, emptyIsZero = false) {
|
|
267
196
|
if (emptyIsZero && (message === null || message === void 0 || !Array.isArray(message.content) || message.content.length === 0)) return 0;
|
|
268
197
|
return estimateBlocks(message?.content) + ROLE_OVERHEAD;
|
|
269
198
|
}
|
|
270
|
-
/** The shared meter heuristic over rendered system-prompt text (shared/estimate.ts). */
|
|
271
|
-
/** Per-tool price for the top-tools display (the total uses dsh's whole-array price). */
|
|
272
199
|
function estimateToolSchema(tool) {
|
|
273
200
|
return Math.ceil(JSON.stringify(tool).length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD;
|
|
274
201
|
}
|
|
275
|
-
/**
|
|
276
|
-
* Count image blocks in a message payload, recursing into nested content (tool-result blocks carry their inner blocks) — seeds each node's
|
|
277
|
-
* `imgs`, which the stats board's image cell sums over the LIVE surface (compacted/pruned messages stop counting).
|
|
278
|
-
*/
|
|
279
202
|
function imageCountOf(blocks) {
|
|
280
203
|
let count = 0;
|
|
281
204
|
if (!Array.isArray(blocks)) return 0;
|
|
@@ -306,13 +229,6 @@ function toolCallNames(blocks) {
|
|
|
306
229
|
}
|
|
307
230
|
return names;
|
|
308
231
|
}
|
|
309
|
-
/**
|
|
310
|
-
* Producer label for an injection event, mirroring the dsh transcript's
|
|
311
|
-
* context provenance (client-runtime context-provenance.ts): workspace
|
|
312
|
-
* instructions name the files they were reconciled from, a plugin source its
|
|
313
|
-
* plugin id, and any other producer its own durable kind. Returns '' when
|
|
314
|
-
* the source is missing or carries no readable identity at all.
|
|
315
|
-
*/
|
|
316
232
|
function injectionSourceName(source) {
|
|
317
233
|
if (source?.kind === "agent-instructions" && Array.isArray(source.changes)) {
|
|
318
234
|
const paths = [];
|
|
@@ -332,31 +248,6 @@ function isInjection(source) {
|
|
|
332
248
|
}
|
|
333
249
|
//#endregion
|
|
334
250
|
//#region src/host/logShapes.ts
|
|
335
|
-
/**
|
|
336
|
-
* Shape-driven readers over the durable session-event vocabulary — the ONE
|
|
337
|
-
* place the plugin's log spellings meet. The supported range (dsh
|
|
338
|
-
* 0.1.5-rc.1+, session formats V3/V4) speaks ONE dialect:
|
|
339
|
-
* `system/message` surface nodes, `assistant/message.data.stream` /
|
|
340
|
-
* `assistant/attempt.data.stream` (packed delta runs plus raw chunk
|
|
341
|
-
* records), `SurfaceOp { startSeq, endSeq }`, `tool/ptc-dispatch`.
|
|
342
|
-
*
|
|
343
|
-
* The fold still reads SHAPES, never a detected harness version: the raw log
|
|
344
|
-
* is untrusted input at every layer, and the supported harness refuses —
|
|
345
|
-
* never rewrites — a legacy-format log, so anything that reaches these
|
|
346
|
-
* readers is current-dialect or hostile. Every reader is total over
|
|
347
|
-
* untrusted input — a malformed record yields "nothing here", never a throw
|
|
348
|
-
* (the projection registry drives the fold without an error boundary; one
|
|
349
|
-
* throw stalls the unit's push feed and the browser waits on "loading"
|
|
350
|
-
* forever).
|
|
351
|
-
*
|
|
352
|
-
* @module dsh-context/host/log-shapes
|
|
353
|
-
*/
|
|
354
|
-
/**
|
|
355
|
-
* Whether one raw stream chunk carries a token delta — the first-token
|
|
356
|
-
* marker, mirroring dsh-llm's `isTokenDelta` (a non-empty text or reasoning
|
|
357
|
-
* fragment, or any Tool-call delta carrying arguments or a name); a
|
|
358
|
-
* malformed chunk is simply not a token.
|
|
359
|
-
*/
|
|
360
251
|
function isTokenChunk(chunk) {
|
|
361
252
|
if (chunk === null || typeof chunk !== "object") return false;
|
|
362
253
|
const c = chunk;
|
|
@@ -367,21 +258,11 @@ function isTokenChunk(chunk) {
|
|
|
367
258
|
default: return false;
|
|
368
259
|
}
|
|
369
260
|
}
|
|
370
|
-
/** Map one `blockType` to its timing bucket (see {@link DecodeKind}). */
|
|
371
261
|
function decodeKindOfBlock(blockType) {
|
|
372
262
|
if (blockType === "reasoning") return "reasoning";
|
|
373
263
|
if (blockType === "text") return "text";
|
|
374
264
|
if (blockType === "tool-call") return "toolarg";
|
|
375
265
|
}
|
|
376
|
-
/**
|
|
377
|
-
* Per-kind decode spans AND counted `block-start` markers inside one embedded
|
|
378
|
-
* assistant stream, tiling [first block-start, endTime]: each `block-start`
|
|
379
|
-
* record owns the interval up to the next one, the last one up to `endTime`.
|
|
380
|
-
* Total over untrusted input — a malformed record is skipped whole (a
|
|
381
|
-
* marker with an unusable time anchors no span and counts nothing), a
|
|
382
|
-
* non-finite boundary yields zero, and a stream with no marker (or not an
|
|
383
|
-
* array) yields all zeros.
|
|
384
|
-
*/
|
|
385
266
|
function decodeTallyOfStream(stream, endTime) {
|
|
386
267
|
const tally = {
|
|
387
268
|
spans: {
|
|
@@ -414,16 +295,6 @@ function decodeTallyOfStream(stream, endTime) {
|
|
|
414
295
|
if (kind !== void 0) tally.spans[kind] += Math.max(0, endTime - since);
|
|
415
296
|
return tally;
|
|
416
297
|
}
|
|
417
|
-
/**
|
|
418
|
-
* The ORDERED decode spans of one embedded assistant stream — the same
|
|
419
|
-
* ownership rule as {@link decodeTallyOfStream} (each `block-start` marker
|
|
420
|
-
* owns the interval up to the next one, the last one up to `endTime`), but
|
|
421
|
-
* keeping the sequence: the timing strip paints the decode window in stream
|
|
422
|
-
* order. Total over untrusted input, mirroring the tally: a malformed record
|
|
423
|
-
* is skipped whole, an unknown `blockType` leaves its interval unspanned
|
|
424
|
-
* (the caller's residue fill owns it), a zero/negative-length span drops
|
|
425
|
-
* out, and a stream with no marker (or not an array) yields none.
|
|
426
|
-
*/
|
|
427
298
|
function decodeSpansOfStream(stream, endTime) {
|
|
428
299
|
const spans = [];
|
|
429
300
|
if (!Array.isArray(stream) || !Number.isFinite(endTime)) return spans;
|
|
@@ -451,14 +322,6 @@ function decodeSpansOfStream(stream, endTime) {
|
|
|
451
322
|
close(endTime);
|
|
452
323
|
return spans;
|
|
453
324
|
}
|
|
454
|
-
/**
|
|
455
|
-
* The first token's instant inside one PACKED delta run (`text-chunks` /
|
|
456
|
-
* `reasoning-chunks` / `tool-call-chunks`): the run's base time plus the
|
|
457
|
-
* accumulated inter-member deltas, taken at the first qualifying member —
|
|
458
|
-
* a name-bearing Tool-call run starts at its first member. Mirrors dsh-llm's
|
|
459
|
-
* `runFirstTokenTime`; a non-finite base or delta yields undefined rather
|
|
460
|
-
* than a NaN instant.
|
|
461
|
-
*/
|
|
462
325
|
function runFirstTokenTime(record) {
|
|
463
326
|
const time0 = record.time0;
|
|
464
327
|
if (typeof time0 !== "number" || !Number.isFinite(time0)) return void 0;
|
|
@@ -476,12 +339,6 @@ function runFirstTokenTime(record) {
|
|
|
476
339
|
if (typeof fragment === "string" && fragment !== "") return time;
|
|
477
340
|
}
|
|
478
341
|
}
|
|
479
|
-
/**
|
|
480
|
-
* The first token's instant inside an embedded assistant stream
|
|
481
|
-
* (`assistant/message.data.stream`, `assistant/attempt.data.stream`), or
|
|
482
|
-
* undefined when the stream carries no token. Mirrors dsh-llm's
|
|
483
|
-
* `assistantStreamFirstTokenTime` over the compact record union.
|
|
484
|
-
*/
|
|
485
342
|
function firstTokenTimeOfStream(stream) {
|
|
486
343
|
if (!Array.isArray(stream)) return void 0;
|
|
487
344
|
for (const record of stream) {
|
|
@@ -496,12 +353,6 @@ function firstTokenTimeOfStream(stream) {
|
|
|
496
353
|
if (time !== void 0) return time;
|
|
497
354
|
}
|
|
498
355
|
}
|
|
499
|
-
/**
|
|
500
|
-
* The inclusive surface range a replacement op covers, or null for `append`
|
|
501
|
-
* and for any unrecognized/hostile op (which the fold treats as an append).
|
|
502
|
-
* The `startSeq`/`endSeq` endpoints are each accepted only as a finite
|
|
503
|
-
* number, so a hostile op with one malformed endpoint degrades to append.
|
|
504
|
-
*/
|
|
505
356
|
function replaceRangeOf(surfaceOp) {
|
|
506
357
|
if (surfaceOp === null || typeof surfaceOp !== "object") return null;
|
|
507
358
|
const op = surfaceOp;
|
|
@@ -516,7 +367,6 @@ function replaceRangeOf(surfaceOp) {
|
|
|
516
367
|
}
|
|
517
368
|
//#endregion
|
|
518
369
|
//#region src/shared/fileOps.ts
|
|
519
|
-
/** Parse a call's raw JSON arguments; non-string/malformed/non-record inputs yield null. */
|
|
520
370
|
function parseCallArgs(raw) {
|
|
521
371
|
if (typeof raw !== "string" || raw === "") return null;
|
|
522
372
|
try {
|
|
@@ -534,45 +384,20 @@ const KIND_BY_TOOL = {
|
|
|
534
384
|
grep: "search",
|
|
535
385
|
glob: "search"
|
|
536
386
|
};
|
|
537
|
-
/** The file purpose of a tool, or null for non-file tools (bash, web_search…). */
|
|
538
387
|
function kindOfTool(tool) {
|
|
539
388
|
if (tool === void 0) return null;
|
|
540
389
|
return KIND_BY_TOOL[tool] ?? null;
|
|
541
390
|
}
|
|
542
|
-
/**
|
|
543
|
-
* The file purpose of one executed call. Like {@link kindOfTool} except for
|
|
544
|
-
* the one file tool whose purpose follows its arguments: `str_replace_editor`
|
|
545
|
-
* reads on `view` and writes on every other command (create / str_replace /
|
|
546
|
-
* insert — an unknown command writes too; the call failed and the row keeps
|
|
547
|
-
* its error flag).
|
|
548
|
-
*/
|
|
549
391
|
function kindOfCall(tool, args) {
|
|
550
392
|
if (tool === "str_replace_editor") return args !== null && args.command === "view" ? "read" : "write";
|
|
551
393
|
return kindOfTool(tool);
|
|
552
394
|
}
|
|
553
|
-
/**
|
|
554
|
-
* Whether a settled call's arguments can yield an op at all: the file tools
|
|
555
|
-
* plus `str_replace_editor`, whose purpose follows its `command`. The public
|
|
556
|
-
* face of the gate {@link opsOfCall} short-circuits on — callers that decide
|
|
557
|
-
* whether to parse or to KEEP a call's raw arguments ask here instead of
|
|
558
|
-
* restating the tool list.
|
|
559
|
-
*/
|
|
560
395
|
function opBearingTool(tool) {
|
|
561
396
|
return tool === "str_replace_editor" || kindOfTool(tool) !== null;
|
|
562
397
|
}
|
|
563
|
-
/**
|
|
564
|
-
* Whether a call's raw arguments must ride the fold state
|
|
565
|
-
* (TimelineState.callNames): every op-bearing tool, plus `run_code`, whose
|
|
566
|
-
* `description` labels the ops its nested dispatches book at flush time.
|
|
567
|
-
*/
|
|
568
398
|
function rawArgsNeeded(tool) {
|
|
569
399
|
return opBearingTool(tool) || tool === "run_code";
|
|
570
400
|
}
|
|
571
|
-
/**
|
|
572
|
-
* The operation's target path: the path-ish argument of read/write tools;
|
|
573
|
-
* for searches the narrowing `path`, else the pattern itself (a pathless
|
|
574
|
-
* grep/glob's target IS the pattern — the workspace-wide search text).
|
|
575
|
-
*/
|
|
576
401
|
function pathOfArgs(tool, args) {
|
|
577
402
|
if (args === null) return null;
|
|
578
403
|
if (tool === "grep" || tool === "glob") {
|
|
@@ -591,27 +416,18 @@ function pathOfArgs(tool, args) {
|
|
|
591
416
|
}
|
|
592
417
|
return null;
|
|
593
418
|
}
|
|
594
|
-
/** Rendered line count: '' is 0, a trailing newline closes its own line. */
|
|
595
419
|
function linesOf(s) {
|
|
596
420
|
if (s === "") return 0;
|
|
597
421
|
let n = 0;
|
|
598
422
|
for (let i = 0; i < s.length; i++) if (s[i] === "\n") n++;
|
|
599
423
|
return s.endsWith("\n") ? n : n + 1;
|
|
600
424
|
}
|
|
601
|
-
/** The added/removed pair of one content-bearing argument set, or zeros. */
|
|
602
425
|
function pairOf(added, removed) {
|
|
603
426
|
return {
|
|
604
427
|
added: typeof added === "string" ? linesOf(added) : 0,
|
|
605
428
|
removed: typeof removed === "string" ? linesOf(removed) : 0
|
|
606
429
|
};
|
|
607
430
|
}
|
|
608
|
-
/**
|
|
609
|
-
* The signed line footprint of one call: an edit removes its old string and
|
|
610
|
-
* adds its new one; a write adds its content (the pre-existing body, if any,
|
|
611
|
-
* is unknowable from the arguments — the estimate stays honest about that);
|
|
612
|
-
* `str_replace_editor` splits the same shapes across its commands. Callers
|
|
613
|
-
* reach here only with parsed args (a null parse yields no path).
|
|
614
|
-
*/
|
|
615
431
|
function deltaOf(tool, args) {
|
|
616
432
|
if (tool === "edit") return pairOf(args.new_string, args.old_string);
|
|
617
433
|
if (tool === "write") return pairOf(args.content, void 0);
|
|
@@ -625,9 +441,6 @@ function deltaOf(tool, args) {
|
|
|
625
441
|
removed: 0
|
|
626
442
|
};
|
|
627
443
|
}
|
|
628
|
-
/** The exact window a read's result meta reports: `offset` plus the retained
|
|
629
|
-
* `lines` array (the same bounded payload the read card renders from). Null
|
|
630
|
-
* for a foreign or malformed meta. */
|
|
631
444
|
function readWindowOf(meta) {
|
|
632
445
|
if (meta === null || typeof meta !== "object") return null;
|
|
633
446
|
const m = meta;
|
|
@@ -639,8 +452,6 @@ function readWindowOf(meta) {
|
|
|
639
452
|
count: m.lines.length
|
|
640
453
|
};
|
|
641
454
|
}
|
|
642
|
-
/** The limit estimate: the tool reads up to `limit` lines from `offset`;
|
|
643
|
-
* absent when the call reads unbounded. */
|
|
644
455
|
function readEstimateOf(args) {
|
|
645
456
|
const limit = args.limit;
|
|
646
457
|
return typeof limit === "number" && Number.isFinite(limit) && limit > 0 ? {
|
|
@@ -648,8 +459,6 @@ function readEstimateOf(args) {
|
|
|
648
459
|
est: true
|
|
649
460
|
} : void 0;
|
|
650
461
|
}
|
|
651
|
-
/** What a read op shows: the exact window off the result meta, else the
|
|
652
|
-
* limit estimate, else nothing (an unbounded read names no footprint). */
|
|
653
462
|
function readOf(meta, args) {
|
|
654
463
|
const win = readWindowOf(meta);
|
|
655
464
|
if (win !== null) return {
|
|
@@ -658,23 +467,12 @@ function readOf(meta, args) {
|
|
|
658
467
|
};
|
|
659
468
|
return readEstimateOf(args);
|
|
660
469
|
}
|
|
661
|
-
/**
|
|
662
|
-
* A search op's detail: the pattern, with the include filter appended when
|
|
663
|
-
* one narrowed the call. A patternless (malformed) search has no detail.
|
|
664
|
-
*/
|
|
665
470
|
function searchDetailOf(args) {
|
|
666
471
|
const pattern = args?.pattern;
|
|
667
472
|
if (typeof pattern !== "string" || pattern === "") return void 0;
|
|
668
473
|
const include = args?.include;
|
|
669
474
|
return typeof include === "string" && include !== "" ? `${pattern} (${include})` : pattern;
|
|
670
475
|
}
|
|
671
|
-
/**
|
|
672
|
-
* The files a search demonstrably reached, read off the result's bounded
|
|
673
|
-
* presentation meta (grep groups matched lines by file; glob lists paths).
|
|
674
|
-
* Only the COMPLETE list attributes: a capped search (`truncated`) names a
|
|
675
|
-
* partial file set, and a malformed meta names none — both fall back to the
|
|
676
|
-
* call's own target. Each entry carries the reported match count.
|
|
677
|
-
*/
|
|
678
476
|
function searchFilesOf(meta) {
|
|
679
477
|
if (meta === null || typeof meta !== "object") return null;
|
|
680
478
|
const m = meta;
|
|
@@ -696,16 +494,6 @@ function searchFilesOf(meta) {
|
|
|
696
494
|
}
|
|
697
495
|
return files.length > 0 ? files : null;
|
|
698
496
|
}
|
|
699
|
-
/**
|
|
700
|
-
* The one-shot per-call op assembly, uniform across every producer: the
|
|
701
|
-
* host's call/result pairing (args off the armed call, meta off the
|
|
702
|
-
* result), the nested Code-Mode settle (no meta exists on a dispatch — the
|
|
703
|
-
* read window and per-file search attribution degrade to the argument-only
|
|
704
|
-
* forms), and the client's inline-generation join fallback. Returns zero to
|
|
705
|
-
* N records: a search with the complete matched-file meta rows per file;
|
|
706
|
-
* any other file call rows once; a non-file tool (or a call whose arguments
|
|
707
|
-
* resolve no target) rows nothing.
|
|
708
|
-
*/
|
|
709
497
|
function opsOfCall(input) {
|
|
710
498
|
const args = opBearingTool(input.tool) ? parseCallArgs(input.argsRaw) : null;
|
|
711
499
|
const kind = kindOfCall(input.tool, args);
|
|
@@ -814,12 +602,6 @@ function trimState(st, bounds) {
|
|
|
814
602
|
}
|
|
815
603
|
}
|
|
816
604
|
}
|
|
817
|
-
/**
|
|
818
|
-
* Bound on the timing strip's painted spans (TimelineState.spans): a count
|
|
819
|
-
* cap, newest tail kept — the same hardcoded-cap family as SYSTEM_NODES_MAX.
|
|
820
|
-
* At ~6–8 spans per step the cap covers a few hundred recent steps, far past
|
|
821
|
-
* the strip's pixel resolution on any real pane.
|
|
822
|
-
*/
|
|
823
605
|
const TIMING_SPANS_MAX = 2e3;
|
|
824
606
|
function createTimelineState() {
|
|
825
607
|
return {
|
|
@@ -851,41 +633,20 @@ function categoryOf(type, message) {
|
|
|
851
633
|
if (isInjection(message?.source)) return "inject";
|
|
852
634
|
return "user";
|
|
853
635
|
}
|
|
854
|
-
/**
|
|
855
|
-
* Mark the detail collections dirty (TimelineState.detailRev). Every caller
|
|
856
|
-
* is a fold branch that just mutated the requests/events/surface/archive;
|
|
857
|
-
* branches that touch only the working slots (stepStart, callNames, the
|
|
858
|
-
* shadow claim) or the envelope scalars do NOT bump — the served detail is
|
|
859
|
-
* unchanged, and an open tab has nothing to refetch.
|
|
860
|
-
*/
|
|
861
636
|
function bumpDetailRev(st) {
|
|
862
637
|
st.detailRev = (st.detailRev ?? 0) + 1;
|
|
863
638
|
}
|
|
864
|
-
/**
|
|
865
|
-
* Bound on the live system-prompt nodes (TimelineState.systems). The
|
|
866
|
-
* effective figure is the LAST nonempty node, so dropping the oldest can only
|
|
867
|
-
* under-report a pathological log whose newest SYSTEM_NODES_MAX nodes are all
|
|
868
|
-
* empty while an older one still carried text.
|
|
869
|
-
*/
|
|
870
639
|
const SYSTEM_NODES_MAX = 8;
|
|
871
|
-
/** The effective system-prompt price: the last nonempty node, else 0 (the harness's own rule). */
|
|
872
640
|
function systemTokensOf(systems) {
|
|
873
641
|
for (let i = systems.length - 1; i >= 0; i--) if (systems[i].tokens > 0) return systems[i].tokens;
|
|
874
642
|
return 0;
|
|
875
643
|
}
|
|
876
|
-
/** Append one system-prompt node, bounding the list (see SYSTEM_NODES_MAX). */
|
|
877
644
|
function pushSystem(st, node) {
|
|
878
645
|
const systems = [...st.systems ?? [], node];
|
|
879
646
|
st.systems = systems.length > SYSTEM_NODES_MAX ? systems.slice(-8) : systems;
|
|
880
647
|
st.systemTokens = systemTokensOf(st.systems);
|
|
881
648
|
}
|
|
882
|
-
/**
|
|
883
|
-
* Bound on the buffered nested Code-Mode ops (TimelineState.pendingCodeOps)
|
|
884
|
-
* — a hostile log that dispatches without settling the parent run_code
|
|
885
|
-
* cannot grow the persisted state past this.
|
|
886
|
-
*/
|
|
887
649
|
const PENDING_CODE_OPS_MAX = 200;
|
|
888
|
-
/** JSON-stringify an unknown argument payload; a hostile (cyclic) value yields no args. */
|
|
889
650
|
function argsRawOf(value) {
|
|
890
651
|
if (typeof value === "string") return value;
|
|
891
652
|
if (value === void 0 || value === null) return void 0;
|
|
@@ -895,15 +656,9 @@ function argsRawOf(value) {
|
|
|
895
656
|
return;
|
|
896
657
|
}
|
|
897
658
|
}
|
|
898
|
-
/** Append op records to the fold-derived log (the trim lives in trimState, with the other collections). */
|
|
899
659
|
function pushFileOps(st, ops) {
|
|
900
660
|
for (const op of ops) st.fileOps.push(op);
|
|
901
661
|
}
|
|
902
|
-
/**
|
|
903
|
-
* Buffer nested Code-Mode ops under their top run_code call id (they flush
|
|
904
|
-
* when the parent's result folds — the ops' locate target). A full buffer
|
|
905
|
-
* drops new arrivals wholesale (defensive logs only).
|
|
906
|
-
*/
|
|
907
662
|
function bufferCodeOps(st, rootCallId, ops) {
|
|
908
663
|
const pending = st.pendingCodeOps ?? {};
|
|
909
664
|
let total = 0;
|
|
@@ -914,24 +669,12 @@ function bufferCodeOps(st, rootCallId, ops) {
|
|
|
914
669
|
[rootCallId]: [...pending[rootCallId] ?? [], ...ops]
|
|
915
670
|
};
|
|
916
671
|
}
|
|
917
|
-
/**
|
|
918
|
-
* Archive removed surface nodes as stamped COPIES — the objects leaving
|
|
919
|
-
* `st.surface` are shared with the persisted previous state, so `gone` must
|
|
920
|
-
* never be written onto them directly.
|
|
921
|
-
*/
|
|
922
672
|
function archiveRemoved(st, removed, goneSeq) {
|
|
923
673
|
for (const n of removed) st.archived.push({
|
|
924
674
|
...n,
|
|
925
675
|
gone: goneSeq
|
|
926
676
|
});
|
|
927
677
|
}
|
|
928
|
-
/**
|
|
929
|
-
* Remove every live surface node whose seq the replacement claims, keeping the
|
|
930
|
-
* per-category sums equal to the surviving nodes and archiving the removals.
|
|
931
|
-
* Removal follows the SEQ list, not the declared range: pruned replacement
|
|
932
|
-
* nodes keep their own seqs beyond the range end, so a range-based removal
|
|
933
|
-
* would leave them behind and overcount. Returns the removed nodes.
|
|
934
|
-
*/
|
|
935
678
|
function removeSurfaceSeqs(st, claimed, goneSeq) {
|
|
936
679
|
if (claimed.size === 0) return [];
|
|
937
680
|
const kept = [];
|
|
@@ -944,44 +687,24 @@ function removeSurfaceSeqs(st, claimed, goneSeq) {
|
|
|
944
687
|
st.surface = kept;
|
|
945
688
|
return removed;
|
|
946
689
|
}
|
|
947
|
-
/**
|
|
948
|
-
* A message nested under data.message. A structural read keeps malformed
|
|
949
|
-
* payloads total and handles developer messages absent from the V3
|
|
950
|
-
* dependency's deriveEventMessage. User messages instead live in data itself.
|
|
951
|
-
*/
|
|
952
690
|
function messageOf$1(data) {
|
|
953
691
|
const message = data?.message;
|
|
954
692
|
return message !== null && typeof message === "object" ? message : null;
|
|
955
693
|
}
|
|
956
|
-
|
|
957
|
-
* The first full text block, recursing through nested content blocks (a tool
|
|
958
|
-
* result wraps its text in a `tool-result` block). Unlike `firstText` this
|
|
959
|
-
* must NOT truncate/normalize: the skill name is matched off the raw
|
|
960
|
-
* `<skill_content name="…">` wrapper.
|
|
961
|
-
*/
|
|
962
|
-
function nestedText(blocks) {
|
|
694
|
+
function firstFullText(blocks) {
|
|
963
695
|
if (!Array.isArray(blocks)) return "";
|
|
964
696
|
for (const item of blocks) {
|
|
965
697
|
if (item === null || typeof item !== "object") continue;
|
|
966
698
|
const block = item;
|
|
967
699
|
if (block.type === "text" && typeof block.text === "string" && block.text !== "") return block.text;
|
|
968
|
-
if (block.content !== void 0) {
|
|
969
|
-
const nested = nestedText(block.content);
|
|
970
|
-
if (nested !== "") return nested;
|
|
971
|
-
}
|
|
972
700
|
}
|
|
973
701
|
return "";
|
|
974
702
|
}
|
|
975
|
-
/**
|
|
976
|
-
* The skill name a `skill`-tool result carries. Loaded skills are rendered as
|
|
977
|
-
* `<skill_content name="…">…</skill_content>` in the result's text, so the name
|
|
978
|
-
* is recovered from the content rather than trusted from the call envelope.
|
|
979
|
-
*/
|
|
980
703
|
function skillNameOf(msg) {
|
|
981
|
-
const match =
|
|
704
|
+
const match = firstFullText(msg !== null && typeof msg === "object" ? msg.content : void 0).match(/<skill_content\s+name="([^"]+)"/);
|
|
982
705
|
return match === null ? "" : match[1];
|
|
983
706
|
}
|
|
984
|
-
function applySurface(st, ev, type,
|
|
707
|
+
function applySurface(st, ev, type, message) {
|
|
985
708
|
const cat = categoryOf(type, message ?? void 0);
|
|
986
709
|
const node = {
|
|
987
710
|
seq: ev.seq,
|
|
@@ -1003,10 +726,7 @@ function applySurface(st, ev, type, data, message) {
|
|
|
1003
726
|
}
|
|
1004
727
|
} else if (type === "tool/result") {
|
|
1005
728
|
const srcId = source?.callId;
|
|
1006
|
-
const
|
|
1007
|
-
const srcEntry = typeof srcId === "string" ? st.callNames[srcId] : void 0;
|
|
1008
|
-
const blockEntry = srcEntry === void 0 && typeof blockId === "string" ? st.callNames[blockId] : void 0;
|
|
1009
|
-
const toolEntry = srcEntry ?? blockEntry;
|
|
729
|
+
const toolEntry = typeof srcId === "string" ? st.callNames[srcId] : void 0;
|
|
1010
730
|
if (toolEntry !== void 0) {
|
|
1011
731
|
node.tool = toolEntry.name;
|
|
1012
732
|
const timing = ensureTiming(st);
|
|
@@ -1020,12 +740,12 @@ function applySurface(st, ev, type, data, message) {
|
|
|
1020
740
|
end: ev.time
|
|
1021
741
|
}];
|
|
1022
742
|
}
|
|
1023
|
-
if (typeof srcId === "string"
|
|
743
|
+
if (typeof srcId === "string") {
|
|
1024
744
|
const kept = {};
|
|
1025
|
-
for (const k in st.callNames) if (k !== srcId
|
|
745
|
+
for (const k in st.callNames) if (k !== srcId) kept[k] = st.callNames[k];
|
|
1026
746
|
st.callNames = kept;
|
|
1027
747
|
}
|
|
1028
|
-
if (
|
|
748
|
+
if (message?.isError === true) node.err = true;
|
|
1029
749
|
} else if (source?.kind === "skill-invocation") node.skill = typeof source.name === "string" ? source.name : "?";
|
|
1030
750
|
else if (source?.kind === "plugin") {
|
|
1031
751
|
if (source.form === "notice" && typeof source.summary === "string") node.text = source.summary;
|
|
@@ -1079,21 +799,6 @@ function applySurface(st, ev, type, data, message) {
|
|
|
1079
799
|
st.sums[cat] += node.tokens;
|
|
1080
800
|
return node;
|
|
1081
801
|
}
|
|
1082
|
-
/**
|
|
1083
|
-
* One provider-reported usage bucket as a billed count, or null when the
|
|
1084
|
-
* field carries no readable number. Accepts finite numbers and numeric
|
|
1085
|
-
* strings; fractions round (some gateways report fractional counts) and
|
|
1086
|
-
* negatives clamp to 0 — a mis-accounting gateway that reports
|
|
1087
|
-
* `cached_tokens > prompt_tokens` drives the disjoint uncached-input figure
|
|
1088
|
-
* below zero, and one raw figure in the state would fail the wire and state
|
|
1089
|
-
* schemas' `.int().nonnegative()` gates on EVERY later delivery, permanently
|
|
1090
|
-
* freezing the projection feed for the session (issue #44). NaN, infinities,
|
|
1091
|
-
* and non-numeric values read as absent.
|
|
1092
|
-
*
|
|
1093
|
-
* Exported for the activity unit (host/activity.ts): the daily ledger re-proves
|
|
1094
|
-
* the same durable usage buckets with the same sanitizer, so one raw figure
|
|
1095
|
-
* can never enter either fold's state.
|
|
1096
|
-
*/
|
|
1097
802
|
function tokenCountOf(value) {
|
|
1098
803
|
if (typeof value === "number") return Number.isFinite(value) ? Math.max(0, Math.round(value)) : null;
|
|
1099
804
|
if (typeof value === "string" && value.trim() !== "") {
|
|
@@ -1102,15 +807,6 @@ function tokenCountOf(value) {
|
|
|
1102
807
|
}
|
|
1103
808
|
return null;
|
|
1104
809
|
}
|
|
1105
|
-
/**
|
|
1106
|
-
* DeepSeek's peak windows (the official list: UTC 01:00–04:00 and 06:00–10:00,
|
|
1107
|
-
* Monday through Friday — Beijing Time 09:00–12:00 and 14:00–18:00). All other
|
|
1108
|
-
* hours, plus entire weekends, bill at the half-price off-peak rate.
|
|
1109
|
-
*
|
|
1110
|
-
* Exported for the activity unit (host/activity.ts): the daily ledger prices
|
|
1111
|
-
* each day's buckets with the same split, so its per-day fees sum to the
|
|
1112
|
-
* session-cost totals the timeline fold books.
|
|
1113
|
-
*/
|
|
1114
810
|
function isPeakUtc(time) {
|
|
1115
811
|
const at = new Date(time);
|
|
1116
812
|
const day = at.getUTCDay();
|
|
@@ -1118,26 +814,9 @@ function isPeakUtc(time) {
|
|
|
1118
814
|
const h = at.getUTCHours();
|
|
1119
815
|
return h >= 1 && h < 4 || h >= 6 && h < 10;
|
|
1120
816
|
}
|
|
1121
|
-
|
|
1122
|
-
* Fold one billed request into the session-cost totals, cloning along the
|
|
1123
|
-
* mutated path only (the untouched branch stays shared with the persisted
|
|
1124
|
-
* previous state — the apply contract never mutates it in place). The
|
|
1125
|
-
* buckets arrive sanitized ({@link BilledUsage}), so the totals stay at the
|
|
1126
|
-
* schemas' non-negative safe integers no matter what the provider reported.
|
|
1127
|
-
* The key is the request envelope's (provider, model) face — the exact
|
|
1128
|
-
* lookup the Client's model-price book resolves (models.dev). A request
|
|
1129
|
-
* without a provider still accumulates (under the '' key) and the Client
|
|
1130
|
-
* prices it when the model id is unambiguous; without a model there is
|
|
1131
|
-
* nothing to price. DeepSeek's period-based list splits the buckets
|
|
1132
|
-
* (peak windows at list price, all other hours half price); every other
|
|
1133
|
-
* provider books everything under the list-price period.
|
|
1134
|
-
*/
|
|
1135
|
-
function accumulateCost(st, time, usage) {
|
|
1136
|
-
const model = st.model;
|
|
1137
|
-
if (model === void 0) return;
|
|
1138
|
-
const provider = st.provider ?? "";
|
|
817
|
+
function addBilledUsage(prev, provider, model, time, usage) {
|
|
1139
818
|
const period = isDeepSeekProvider(provider) && !isPeakUtc(time) ? "off" : "peak";
|
|
1140
|
-
const models =
|
|
819
|
+
const models = prev?.[provider] ?? {};
|
|
1141
820
|
const periods = models[model] ?? {};
|
|
1142
821
|
const b = periods[period] ?? {
|
|
1143
822
|
uncached: 0,
|
|
@@ -1156,44 +835,27 @@ function accumulateCost(st, time, usage) {
|
|
|
1156
835
|
...models,
|
|
1157
836
|
[model]: nextPeriods
|
|
1158
837
|
};
|
|
1159
|
-
|
|
1160
|
-
...
|
|
838
|
+
return {
|
|
839
|
+
...prev ?? {},
|
|
1161
840
|
[provider]: nextModels
|
|
1162
841
|
};
|
|
1163
842
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
* `bounds` come from the plugin config (config.ts) — retention only, they
|
|
1170
|
-
* never change the state shape.
|
|
1171
|
-
*/
|
|
1172
|
-
/** The timing card's per-tool ranking cap: the busiest 16 names are kept. */
|
|
843
|
+
function accumulateCost(st, time, usage) {
|
|
844
|
+
const model = st.model;
|
|
845
|
+
if (model === void 0) return;
|
|
846
|
+
st.cost = addBilledUsage(st.cost, st.provider ?? "", model, time, usage);
|
|
847
|
+
}
|
|
1173
848
|
const TOOL_TIMING_CAP = 16;
|
|
1174
|
-
/**
|
|
1175
|
-
* The interactive Q&A tool (`dsh-tool-ask-user`): its settled result IS the
|
|
1176
|
-
* user's answer, so it folds into the human-input tally alongside the user's
|
|
1177
|
-
* own messages. One result = one answer submission, however many questions
|
|
1178
|
-
* the prompt carried.
|
|
1179
|
-
*/
|
|
1180
849
|
const ASK_USER_TOOL = "ask_user_question";
|
|
1181
|
-
/** The decode buckets of the generation split, in card order (see TimingTotals). */
|
|
1182
850
|
const DECODE_KINDS = [
|
|
1183
851
|
"reasoning",
|
|
1184
852
|
"text",
|
|
1185
853
|
"toolarg"
|
|
1186
854
|
];
|
|
1187
|
-
/** Non-negative, NaN-proof duration between two instants (hostile times degrade to 0). */
|
|
1188
855
|
function durOf(from, to) {
|
|
1189
856
|
if (!Number.isFinite(from) || !Number.isFinite(to)) return 0;
|
|
1190
857
|
return Math.max(0, to - from);
|
|
1191
858
|
}
|
|
1192
|
-
/**
|
|
1193
|
-
* The fold's private timing accumulator: created on first use, and CLONED on
|
|
1194
|
-
* every later ensure() (see `applyTimeline`) — the object left in the
|
|
1195
|
-
* persisted previous state is never written into in place.
|
|
1196
|
-
*/
|
|
1197
859
|
function ensureTiming(st) {
|
|
1198
860
|
if (st.timing === void 0) st.timing = {
|
|
1199
861
|
wallMs: 0,
|
|
@@ -1206,7 +868,6 @@ function ensureTiming(st) {
|
|
|
1206
868
|
};
|
|
1207
869
|
return st.timing;
|
|
1208
870
|
}
|
|
1209
|
-
/** The TimingTotals fields each decode bucket's span and count land in. */
|
|
1210
871
|
const DECODE_FIELDS = {
|
|
1211
872
|
reasoning: {
|
|
1212
873
|
ms: "reasoningMs",
|
|
@@ -1221,24 +882,11 @@ const DECODE_FIELDS = {
|
|
|
1221
882
|
n: "toolArgBlocks"
|
|
1222
883
|
}
|
|
1223
884
|
};
|
|
1224
|
-
/**
|
|
1225
|
-
* Fold one block's decode span and count into the totals' generation split
|
|
1226
|
-
* (see TimingTotals). A zero span or count stays ABSENT — the field then
|
|
1227
|
-
* carries the "nothing was decoded/counted in this bucket" fact without adding
|
|
1228
|
-
* dead properties to every pre-split-shaped state, and the card reads absence
|
|
1229
|
-
* as 0.
|
|
1230
|
-
*/
|
|
1231
885
|
function addDecode(timing, kind, ms, blocks) {
|
|
1232
886
|
const field = DECODE_FIELDS[kind];
|
|
1233
887
|
if (ms > 0) timing[field.ms] = (timing[field.ms] ?? 0) + ms;
|
|
1234
888
|
if (blocks > 0) timing[field.n] = (timing[field.n] ?? 0) + blocks;
|
|
1235
889
|
}
|
|
1236
|
-
/**
|
|
1237
|
-
* Tally one completed tool call into the per-name ranking, bounded to
|
|
1238
|
-
* TOOL_TIMING_CAP names: repeated names update in place, a new name beyond
|
|
1239
|
-
* the cap evicts the smallest tally first (the ranking's tail), so state
|
|
1240
|
-
* stays bounded even over a hostile log of unique names.
|
|
1241
|
-
*/
|
|
1242
890
|
function bumpToolTotals(timing, name, ms) {
|
|
1243
891
|
if (!Object.hasOwn(timing.tools, name)) {
|
|
1244
892
|
if (Object.keys(timing.tools).length >= TOOL_TIMING_CAP) {
|
|
@@ -1276,16 +924,6 @@ const CLONE_ALL = [
|
|
|
1276
924
|
];
|
|
1277
925
|
function applyTimeline(state, event, bounds) {
|
|
1278
926
|
let st;
|
|
1279
|
-
/**
|
|
1280
|
-
* The event's private working state: a shallow object clone up front, with
|
|
1281
|
-
* only the named collections cloned into private copies. Everything else
|
|
1282
|
-
* stays shared with the persisted previous state — the apply contract is
|
|
1283
|
-
* "never mutate the previous state in place". Called EXACTLY ONCE per event
|
|
1284
|
-
* (each case names the collections its own path may write into; helpers
|
|
1285
|
-
* never call it), so building afresh per call is safe. The default is the
|
|
1286
|
-
* full clone — a future branch that omits its list degrades to the old
|
|
1287
|
-
* behavior instead of sharing a collection it mutates.
|
|
1288
|
-
*/
|
|
1289
927
|
const ensure = (keys = CLONE_ALL) => {
|
|
1290
928
|
st = { ...state };
|
|
1291
929
|
for (const key of keys) if (key === "surface") st.surface = [...state.surface];
|
|
@@ -1335,7 +973,7 @@ function applyTimeline(state, event, bounds) {
|
|
|
1335
973
|
const op = replaceRangeOf(event.surfaceOp);
|
|
1336
974
|
if (op !== null) {
|
|
1337
975
|
s.systems = (s.systems ?? []).filter((n) => n.seq < op.start || n.seq > op.end);
|
|
1338
|
-
const claimed =
|
|
976
|
+
const claimed = new Set();
|
|
1339
977
|
for (const n of s.surface) if (n.seq >= op.start && n.seq <= op.end) claimed.add(n.seq);
|
|
1340
978
|
if (removeSurfaceSeqs(s, claimed, event.seq).length > 0) bumpDetailRev(s);
|
|
1341
979
|
}
|
|
@@ -1508,7 +1146,7 @@ function applyTimeline(state, event, bounds) {
|
|
|
1508
1146
|
"events"
|
|
1509
1147
|
]);
|
|
1510
1148
|
bumpDetailRev(s);
|
|
1511
|
-
const node = applySurface(s, event, event.type,
|
|
1149
|
+
const node = applySurface(s, event, event.type, msg);
|
|
1512
1150
|
const source = msg?.source;
|
|
1513
1151
|
if (developer || isInjection(source)) {
|
|
1514
1152
|
const rec = {
|
|
@@ -1539,10 +1177,8 @@ function applyTimeline(state, event, bounds) {
|
|
|
1539
1177
|
case "tool/result": {
|
|
1540
1178
|
const toolMsg = deriveEventMessage(event);
|
|
1541
1179
|
const srcId = (toolMsg?.source)?.callId;
|
|
1542
|
-
const
|
|
1543
|
-
const
|
|
1544
|
-
const pendingEntry = (typeof srcId === "string" ? state.callNames[srcId] : void 0) ?? (typeof blockId === "string" ? state.callNames[blockId] : void 0);
|
|
1545
|
-
const buffered = (typeof srcId === "string" ? state.pendingCodeOps?.[srcId] : void 0) ?? (typeof blockId === "string" ? state.pendingCodeOps?.[blockId] : void 0);
|
|
1180
|
+
const pendingEntry = typeof srcId === "string" ? state.callNames[srcId] : void 0;
|
|
1181
|
+
const buffered = typeof srcId === "string" ? state.pendingCodeOps?.[srcId] : void 0;
|
|
1546
1182
|
const s = ensure([
|
|
1547
1183
|
"surface",
|
|
1548
1184
|
"sums",
|
|
@@ -1552,7 +1188,7 @@ function applyTimeline(state, event, bounds) {
|
|
|
1552
1188
|
"timing"
|
|
1553
1189
|
]);
|
|
1554
1190
|
bumpDetailRev(s);
|
|
1555
|
-
const node = applySurface(s, event, event.type,
|
|
1191
|
+
const node = applySurface(s, event, event.type, toolMsg);
|
|
1556
1192
|
if (node.tool === ASK_USER_TOOL) s.humanInputs = (s.humanInputs ?? 0) + 1;
|
|
1557
1193
|
if (pendingEntry !== void 0 && pendingEntry.name === ASK_USER_TOOL && state.stepStart !== void 0) s.stepWaits = [...state.stepWaits ?? [], {
|
|
1558
1194
|
start: pendingEntry.start,
|
|
@@ -1564,7 +1200,7 @@ function applyTimeline(state, event, bounds) {
|
|
|
1564
1200
|
tool: pendingEntry.name,
|
|
1565
1201
|
argsRaw: pendingEntry.argsRaw,
|
|
1566
1202
|
meta: data?.meta,
|
|
1567
|
-
err:
|
|
1203
|
+
err: toolMsg?.isError === true
|
|
1568
1204
|
}));
|
|
1569
1205
|
if (buffered !== void 0 && buffered.length > 0) {
|
|
1570
1206
|
const program = parseCallArgs(pendingEntry?.argsRaw)?.description;
|
|
@@ -1574,7 +1210,7 @@ function applyTimeline(state, event, bounds) {
|
|
|
1574
1210
|
...typeof program === "string" && program !== "" ? { program } : {}
|
|
1575
1211
|
})));
|
|
1576
1212
|
const kept = {};
|
|
1577
|
-
for (const k in s.pendingCodeOps) if (k !== srcId
|
|
1213
|
+
for (const k in s.pendingCodeOps) if (k !== srcId) kept[k] = s.pendingCodeOps[k];
|
|
1578
1214
|
if (Object.keys(kept).length > 0) s.pendingCodeOps = kept;
|
|
1579
1215
|
else delete s.pendingCodeOps;
|
|
1580
1216
|
}
|
|
@@ -1682,7 +1318,7 @@ function applyTimeline(state, event, bounds) {
|
|
|
1682
1318
|
}
|
|
1683
1319
|
}
|
|
1684
1320
|
const asstMsg = deriveEventMessage(event);
|
|
1685
|
-
applySurface(s, event, event.type,
|
|
1321
|
+
if (applySurface(s, event, event.type, asstMsg).text !== void 0) s.answers = (s.answers ?? 0) + 1;
|
|
1686
1322
|
break;
|
|
1687
1323
|
}
|
|
1688
1324
|
case "session/end-seed": {
|
|
@@ -1731,15 +1367,6 @@ function applyTimeline(state, event, bounds) {
|
|
|
1731
1367
|
}
|
|
1732
1368
|
return state;
|
|
1733
1369
|
}
|
|
1734
|
-
/**
|
|
1735
|
-
* The envelope scalars both wire generations share: current composition, the
|
|
1736
|
-
* live-surface counters, and the copied cost/timing totals. Served value
|
|
1737
|
-
* fields are COPIES — the served value must never alias persisted state.
|
|
1738
|
-
* Optional scalars use conditional spread: an unknown value must not
|
|
1739
|
-
* materialize an `undefined`-valued property (the lossless-JSON pipeline —
|
|
1740
|
-
* a single such property can fail the whole push, the failure mode behind
|
|
1741
|
-
* issue #29).
|
|
1742
|
-
*/
|
|
1743
1370
|
function headFieldsOf(state) {
|
|
1744
1371
|
const surfaceTotal = state.sums.user + state.sums.inject + state.sums.skill + state.sums.assistant + state.sums.tool;
|
|
1745
1372
|
const result = {
|
|
@@ -1760,6 +1387,7 @@ function headFieldsOf(state) {
|
|
|
1760
1387
|
images: state.surface.reduce((n, node) => n + (node.imgs ?? 0), 0),
|
|
1761
1388
|
toolCalls: state.surface.reduce((n, node) => node.cat === "tool" || node.cat === "skill" && node.tool !== void 0 ? n + 1 : n, 0),
|
|
1762
1389
|
humanInputs: state.humanInputs ?? 0,
|
|
1390
|
+
answers: state.answers ?? 0,
|
|
1763
1391
|
...state.lastUser !== void 0 ? { lastUser: state.lastUser } : {},
|
|
1764
1392
|
requests: [],
|
|
1765
1393
|
events: [],
|
|
@@ -1793,13 +1421,6 @@ function headFieldsOf(state) {
|
|
|
1793
1421
|
if (state.systems !== void 0 && state.systems.length > 0) result.systems = state.systems.map((n) => ({ ...n }));
|
|
1794
1422
|
return result;
|
|
1795
1423
|
}
|
|
1796
|
-
/**
|
|
1797
|
-
* The heavy collections: copies of the retained request records and context
|
|
1798
|
-
* events (each event attached to the requests around it — the chart's ✂
|
|
1799
|
-
* anchoring), the bounded served surface window, and the removed-node
|
|
1800
|
-
* archive. Shared verbatim by the inline wire view (channel-less hosts) and
|
|
1801
|
-
* the on-demand detail payload (host/detail.ts).
|
|
1802
|
-
*/
|
|
1803
1424
|
function detailCollectionsOf(state, bounds) {
|
|
1804
1425
|
const result = {
|
|
1805
1426
|
requests: state.requests.map((r) => ({ ...r })),
|
|
@@ -1841,29 +1462,27 @@ function detailCollectionsOf(state, bounds) {
|
|
|
1841
1462
|
}
|
|
1842
1463
|
return result;
|
|
1843
1464
|
}
|
|
1844
|
-
/**
|
|
1845
|
-
* The split generation's SLIM wire head: the envelope scalars plus the
|
|
1846
|
-
* precomputed count figures, the newest request's billing summary (the
|
|
1847
|
-
* headline's derived anchor), and the detail revision marker. Small enough
|
|
1848
|
-
* to ride every delivery channel whole (~1KB) — the heavy collections moved
|
|
1849
|
-
* to the on-demand detail channel (host/detail.ts).
|
|
1850
|
-
*/
|
|
1851
1465
|
function buildTimelineHead(state) {
|
|
1852
1466
|
const result = headFieldsOf(state);
|
|
1853
|
-
const turns =
|
|
1467
|
+
const turns = new Set();
|
|
1854
1468
|
for (const r of state.requests) turns.add(r.turn ?? 0);
|
|
1855
1469
|
let injects = 0;
|
|
1856
1470
|
let compactions = 0;
|
|
1857
1471
|
let prunes = 0;
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1472
|
+
const skills = new Set();
|
|
1473
|
+
for (const e of state.events) {
|
|
1474
|
+
if (e.kind === "inject") injects++;
|
|
1475
|
+
else if (e.kind === "compaction") compactions++;
|
|
1476
|
+
else if (e.kind === "prune") prunes++;
|
|
1477
|
+
if (e.sub === "skill" && typeof e.name === "string" && e.name !== "") skills.add(e.name);
|
|
1478
|
+
}
|
|
1861
1479
|
result.counts = {
|
|
1862
1480
|
turns: turns.size,
|
|
1863
1481
|
steps: state.requests.length,
|
|
1864
1482
|
injects,
|
|
1865
1483
|
compactions,
|
|
1866
|
-
prunes
|
|
1484
|
+
prunes,
|
|
1485
|
+
skills: skills.size
|
|
1867
1486
|
};
|
|
1868
1487
|
const last = state.requests.at(-1);
|
|
1869
1488
|
if (last !== void 0) result.last = {
|
|
@@ -1874,12 +1493,6 @@ function buildTimelineHead(state) {
|
|
|
1874
1493
|
result.detailRev = state.detailRev ?? 0;
|
|
1875
1494
|
return result;
|
|
1876
1495
|
}
|
|
1877
|
-
/**
|
|
1878
|
-
* The on-demand detail payload (host/detail.ts serves it off the live fold
|
|
1879
|
-
* state): the heavy collections, the slim head at the SAME cut (the Agent
|
|
1880
|
-
* network's cold-node ring fetch renders composition off it), and the
|
|
1881
|
-
* revision marker the head carries.
|
|
1882
|
-
*/
|
|
1883
1496
|
function buildTimelineDetail(state, bounds) {
|
|
1884
1497
|
return {
|
|
1885
1498
|
rev: state.detailRev ?? 0,
|
|
@@ -1887,13 +1500,6 @@ function buildTimelineDetail(state, bounds) {
|
|
|
1887
1500
|
...detailCollectionsOf(state, bounds)
|
|
1888
1501
|
};
|
|
1889
1502
|
}
|
|
1890
|
-
/**
|
|
1891
|
-
* Serve the INLINE projection wire view (channel-less hosts): the head
|
|
1892
|
-
* scalars with the detail collections in place — the shape every delivery
|
|
1893
|
-
* channel carried before the split generation. Bound the surface nodes to
|
|
1894
|
-
* the newest tail and attach each event to the request around it; stamp
|
|
1895
|
-
* COPIES — the persisted state objects are never mutated.
|
|
1896
|
-
*/
|
|
1897
1503
|
function buildTimelineView(state, bounds) {
|
|
1898
1504
|
return {
|
|
1899
1505
|
...headFieldsOf(state),
|
|
@@ -1901,94 +1507,355 @@ function buildTimelineView(state, bounds) {
|
|
|
1901
1507
|
};
|
|
1902
1508
|
}
|
|
1903
1509
|
//#endregion
|
|
1904
|
-
//#region src/
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1510
|
+
//#region src/host/config.ts
|
|
1511
|
+
const DEFAULT_BOUNDS = {
|
|
1512
|
+
maxRequestSteps: 1500,
|
|
1513
|
+
maxKeptTurns: 300,
|
|
1514
|
+
maxEvents: 400,
|
|
1515
|
+
maxNodes: 2e3,
|
|
1516
|
+
maxArchiveNodes: 400,
|
|
1517
|
+
maxFileOps: 400
|
|
1518
|
+
};
|
|
1519
|
+
function volatileField(field) {
|
|
1520
|
+
const volatile = field.volatile;
|
|
1521
|
+
return typeof volatile === "function" ? volatile.call(field) : field;
|
|
1908
1522
|
}
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1523
|
+
function count(defaultValue) {
|
|
1524
|
+
return z$1.number().min(1).step(1).default(defaultValue);
|
|
1525
|
+
}
|
|
1526
|
+
const Config = z$1.object({
|
|
1527
|
+
maxRequestSteps: count(DEFAULT_BOUNDS.maxRequestSteps),
|
|
1528
|
+
maxKeptTurns: count(DEFAULT_BOUNDS.maxKeptTurns),
|
|
1529
|
+
maxEvents: count(DEFAULT_BOUNDS.maxEvents),
|
|
1530
|
+
maxNodes: count(DEFAULT_BOUNDS.maxNodes),
|
|
1531
|
+
maxArchiveNodes: count(DEFAULT_BOUNDS.maxArchiveNodes),
|
|
1532
|
+
maxFileOps: count(DEFAULT_BOUNDS.maxFileOps),
|
|
1533
|
+
defaultPlacement: volatileField(z$1.union([
|
|
1534
|
+
"all",
|
|
1535
|
+
"tab",
|
|
1536
|
+
"sidebar"
|
|
1537
|
+
]).default("all").loose()),
|
|
1538
|
+
defaultGranularity: volatileField(z$1.union(["step", "turn"]).default("step").loose()),
|
|
1539
|
+
defaultTrendMode: volatileField(z$1.union(["total", "delta"]).default("total").loose()),
|
|
1540
|
+
defaultDeltaBase: volatileField(z$1.union(["step", "turn"]).default("step").loose()),
|
|
1541
|
+
defaultToolSort: volatileField(z$1.union([
|
|
1542
|
+
"size",
|
|
1543
|
+
"count",
|
|
1544
|
+
"name"
|
|
1545
|
+
]).default("count").loose()),
|
|
1546
|
+
defaultFileSort: volatileField(z$1.union([
|
|
1547
|
+
"count",
|
|
1548
|
+
"latest",
|
|
1549
|
+
"path"
|
|
1550
|
+
]).default("count").loose()),
|
|
1551
|
+
insightsEntry: volatileField(z$1.union(["show", "hide"]).default("show").loose()),
|
|
1552
|
+
defaultDurationCurve: volatileField(z$1.union(["show", "hide"]).default("show").loose())
|
|
1553
|
+
});
|
|
1554
|
+
function resolveBounds(config) {
|
|
1555
|
+
const resolved = Config(config ?? {});
|
|
1556
|
+
return {
|
|
1557
|
+
maxRequestSteps: resolved.maxRequestSteps,
|
|
1558
|
+
maxKeptTurns: resolved.maxKeptTurns,
|
|
1559
|
+
maxEvents: resolved.maxEvents,
|
|
1560
|
+
maxNodes: resolved.maxNodes,
|
|
1561
|
+
maxArchiveNodes: resolved.maxArchiveNodes,
|
|
1562
|
+
maxFileOps: resolved.maxFileOps
|
|
1563
|
+
};
|
|
1920
1564
|
}
|
|
1921
1565
|
//#endregion
|
|
1922
|
-
//#region src/host/
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1566
|
+
//#region src/host/timeline.ts
|
|
1567
|
+
const surfaceNodeSchema = z.object({
|
|
1568
|
+
seq: z.number().int().nonnegative(),
|
|
1569
|
+
time: z.number().optional(),
|
|
1570
|
+
cat: z.enum([
|
|
1571
|
+
"user",
|
|
1572
|
+
"inject",
|
|
1573
|
+
"skill",
|
|
1574
|
+
"assistant",
|
|
1575
|
+
"tool"
|
|
1576
|
+
]),
|
|
1577
|
+
tokens: z.number().int().nonnegative(),
|
|
1578
|
+
imgs: z.number().int().nonnegative().optional(),
|
|
1579
|
+
gone: z.number().int().nonnegative().optional(),
|
|
1580
|
+
form: z.string().optional(),
|
|
1581
|
+
name: z.string().optional(),
|
|
1582
|
+
text: z.string().optional(),
|
|
1583
|
+
tool: z.string().optional(),
|
|
1584
|
+
err: z.boolean().optional(),
|
|
1585
|
+
skill: z.string().optional(),
|
|
1586
|
+
calls: z.array(z.string()).optional()
|
|
1587
|
+
}).strict();
|
|
1588
|
+
const systemPromptNodeSchema = z.object({
|
|
1589
|
+
seq: z.number().int().nonnegative(),
|
|
1590
|
+
time: z.number(),
|
|
1591
|
+
tokens: z.number().int().nonnegative()
|
|
1592
|
+
}).strict();
|
|
1593
|
+
const requestRecordSchema = z.object({
|
|
1594
|
+
turn: z.number().optional(),
|
|
1595
|
+
step: z.number().optional(),
|
|
1596
|
+
time: z.number(),
|
|
1597
|
+
seq: z.number(),
|
|
1598
|
+
system: z.number().int().nonnegative(),
|
|
1599
|
+
tools: z.number().int().nonnegative(),
|
|
1600
|
+
user: z.number().int().nonnegative(),
|
|
1601
|
+
inject: z.number().int().nonnegative(),
|
|
1602
|
+
assistant: z.number().int().nonnegative(),
|
|
1603
|
+
tool: z.number().int().nonnegative(),
|
|
1604
|
+
total: z.number().int().nonnegative(),
|
|
1605
|
+
prompt: z.number().int().nonnegative().optional(),
|
|
1606
|
+
skill: z.number().int().nonnegative().optional(),
|
|
1607
|
+
cacheRead: z.number().int().nonnegative().optional(),
|
|
1608
|
+
output: z.number().int().nonnegative().optional(),
|
|
1609
|
+
activeMs: z.number().nonnegative().optional(),
|
|
1610
|
+
stepCount: z.number().int().positive().optional()
|
|
1611
|
+
}).strict();
|
|
1612
|
+
const contextEventSchema = z.object({
|
|
1613
|
+
seq: z.number(),
|
|
1614
|
+
time: z.number(),
|
|
1615
|
+
kind: z.enum([
|
|
1616
|
+
"compaction",
|
|
1617
|
+
"prune",
|
|
1618
|
+
"inject",
|
|
1619
|
+
"model",
|
|
1620
|
+
"mode"
|
|
1621
|
+
]),
|
|
1622
|
+
form: z.string().optional(),
|
|
1623
|
+
tokens: z.number().optional(),
|
|
1624
|
+
count: z.number().optional(),
|
|
1625
|
+
sub: z.string().optional(),
|
|
1626
|
+
name: z.string().optional(),
|
|
1627
|
+
detail: z.string().optional(),
|
|
1628
|
+
from: z.string().optional(),
|
|
1629
|
+
to: z.string().optional(),
|
|
1630
|
+
fromTurn: z.number().optional(),
|
|
1631
|
+
fromStep: z.number().optional(),
|
|
1632
|
+
turn: z.number().optional(),
|
|
1633
|
+
step: z.number().optional()
|
|
1634
|
+
}).strict();
|
|
1635
|
+
const fileOpSchema = z.object({
|
|
1636
|
+
seq: z.number().int().nonnegative(),
|
|
1637
|
+
path: z.string(),
|
|
1638
|
+
kind: z.enum([
|
|
1639
|
+
"read",
|
|
1640
|
+
"write",
|
|
1641
|
+
"search"
|
|
1642
|
+
]),
|
|
1643
|
+
tool: z.string(),
|
|
1644
|
+
time: z.number().optional(),
|
|
1645
|
+
err: z.boolean(),
|
|
1646
|
+
added: z.number().int().nonnegative(),
|
|
1647
|
+
removed: z.number().int().nonnegative(),
|
|
1648
|
+
detail: z.string().optional(),
|
|
1649
|
+
hits: z.number().int().positive().optional(),
|
|
1650
|
+
read: z.union([z.object({
|
|
1651
|
+
start: z.number().int().positive(),
|
|
1652
|
+
count: z.number().int().nonnegative()
|
|
1653
|
+
}).strict(), z.object({
|
|
1654
|
+
count: z.number().int().positive(),
|
|
1655
|
+
est: z.literal(true)
|
|
1656
|
+
}).strict()]).optional(),
|
|
1657
|
+
parent: z.number().int().nonnegative().optional(),
|
|
1658
|
+
program: z.string().optional(),
|
|
1659
|
+
pattern: z.literal(true).optional(),
|
|
1660
|
+
gone: z.number().int().nonnegative().optional()
|
|
1661
|
+
}).strict();
|
|
1662
|
+
const timingSpanSchema = z.object({
|
|
1663
|
+
kind: z.enum([
|
|
1664
|
+
"ttft",
|
|
1665
|
+
"reasoning",
|
|
1666
|
+
"text",
|
|
1667
|
+
"toolarg",
|
|
1668
|
+
"tools",
|
|
1669
|
+
"other"
|
|
1670
|
+
]),
|
|
1671
|
+
start: z.number(),
|
|
1672
|
+
end: z.number()
|
|
1673
|
+
}).strict();
|
|
1674
|
+
const currentSchema = z.object({
|
|
1675
|
+
system: z.number().int().nonnegative(),
|
|
1676
|
+
tools: z.number().int().nonnegative(),
|
|
1677
|
+
user: z.number().int().nonnegative(),
|
|
1678
|
+
inject: z.number().int().nonnegative(),
|
|
1679
|
+
skill: z.number().int().nonnegative(),
|
|
1680
|
+
assistant: z.number().int().nonnegative(),
|
|
1681
|
+
tool: z.number().int().nonnegative(),
|
|
1682
|
+
total: z.number().int().nonnegative()
|
|
1683
|
+
}).strict();
|
|
1684
|
+
const costBucketsSchema = z.object({
|
|
1961
1685
|
uncached: z.number().int().nonnegative(),
|
|
1962
1686
|
cacheRead: z.number().int().nonnegative(),
|
|
1963
1687
|
cacheWrite: z.number().int().nonnegative(),
|
|
1964
1688
|
output: z.number().int().nonnegative()
|
|
1965
1689
|
}).strict();
|
|
1966
|
-
const costModelSchema
|
|
1967
|
-
peak:
|
|
1968
|
-
off:
|
|
1690
|
+
const costModelSchema = z.object({
|
|
1691
|
+
peak: costBucketsSchema.optional(),
|
|
1692
|
+
off: costBucketsSchema.optional()
|
|
1969
1693
|
}).strict();
|
|
1970
|
-
const
|
|
1971
|
-
const
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1694
|
+
const costModelsSchema = z.record(z.string(), costModelSchema);
|
|
1695
|
+
const costUsageSchema = z.record(z.string(), costModelsSchema);
|
|
1696
|
+
const toolTimingSchema = z.object({
|
|
1697
|
+
calls: z.number().int().nonnegative(),
|
|
1698
|
+
ms: z.number().nonnegative()
|
|
1699
|
+
}).strict();
|
|
1700
|
+
const timingTotalsSchema = z.object({
|
|
1701
|
+
wallMs: z.number().nonnegative(),
|
|
1702
|
+
ttftMs: z.number().nonnegative(),
|
|
1703
|
+
genMs: z.number().nonnegative(),
|
|
1704
|
+
reasoningMs: z.number().nonnegative().optional(),
|
|
1705
|
+
reasoningBlocks: z.number().int().nonnegative().optional(),
|
|
1706
|
+
textMs: z.number().nonnegative().optional(),
|
|
1707
|
+
textBlocks: z.number().int().nonnegative().optional(),
|
|
1708
|
+
toolArgMs: z.number().nonnegative().optional(),
|
|
1709
|
+
toolArgBlocks: z.number().int().nonnegative().optional(),
|
|
1710
|
+
speedTokens: z.number().nonnegative().optional(),
|
|
1711
|
+
speedMs: z.number().nonnegative().optional(),
|
|
1712
|
+
calls: z.number().int().nonnegative(),
|
|
1713
|
+
toolsMs: z.number().nonnegative(),
|
|
1714
|
+
toolCalls: z.number().int().nonnegative(),
|
|
1715
|
+
tools: z.record(z.string(), toolTimingSchema)
|
|
1716
|
+
}).strict();
|
|
1717
|
+
const unsupportedSchema = z.object({
|
|
1718
|
+
current: z.string(),
|
|
1719
|
+
minimum: z.string()
|
|
1720
|
+
}).strict();
|
|
1721
|
+
const countsSchema = z.object({
|
|
1722
|
+
turns: z.number().int().nonnegative(),
|
|
1723
|
+
steps: z.number().int().nonnegative(),
|
|
1724
|
+
injects: z.number().int().nonnegative(),
|
|
1725
|
+
compactions: z.number().int().nonnegative(),
|
|
1726
|
+
prunes: z.number().int().nonnegative(),
|
|
1727
|
+
skills: z.number().int().nonnegative().optional()
|
|
1728
|
+
}).strict();
|
|
1729
|
+
const lastSchema = z.object({
|
|
1730
|
+
seq: z.number(),
|
|
1731
|
+
total: z.number().int().nonnegative(),
|
|
1732
|
+
prompt: z.number().int().nonnegative().optional()
|
|
1733
|
+
}).strict();
|
|
1734
|
+
const contextTimelineSchema = z.object({
|
|
1735
|
+
ok: z.literal(true),
|
|
1736
|
+
unsupported: unsupportedSchema.optional(),
|
|
1737
|
+
model: z.string().optional(),
|
|
1738
|
+
provider: z.string().optional(),
|
|
1739
|
+
contextWindow: z.number().optional(),
|
|
1740
|
+
current: currentSchema,
|
|
1741
|
+
images: z.number().int().nonnegative().optional(),
|
|
1742
|
+
toolCalls: z.number().int().nonnegative().optional(),
|
|
1743
|
+
humanInputs: z.number().int().nonnegative().optional(),
|
|
1744
|
+
answers: z.number().int().nonnegative().optional(),
|
|
1745
|
+
lastUser: z.string().optional(),
|
|
1746
|
+
counts: countsSchema.optional(),
|
|
1747
|
+
last: lastSchema.optional(),
|
|
1748
|
+
detailRev: z.number().int().nonnegative().optional(),
|
|
1749
|
+
requests: z.array(requestRecordSchema).optional(),
|
|
1750
|
+
events: z.array(contextEventSchema).optional(),
|
|
1751
|
+
cost: costUsageSchema.optional(),
|
|
1752
|
+
timing: timingTotalsSchema.optional(),
|
|
1753
|
+
systems: z.array(systemPromptNodeSchema).optional(),
|
|
1754
|
+
nodes: z.array(surfaceNodeSchema).optional(),
|
|
1755
|
+
droppedNodes: z.number().int().nonnegative().optional(),
|
|
1756
|
+
archive: z.array(surfaceNodeSchema).optional(),
|
|
1757
|
+
surfaceFloor: z.number().int().nonnegative().optional(),
|
|
1758
|
+
archiveFloor: z.number().int().nonnegative().optional(),
|
|
1759
|
+
fileOps: z.array(fileOpSchema).optional(),
|
|
1760
|
+
fileOpsFloor: z.number().int().nonnegative().optional(),
|
|
1761
|
+
spans: z.array(timingSpanSchema).optional()
|
|
1762
|
+
}).strict();
|
|
1763
|
+
const timelineStateSchema = z.object({
|
|
1764
|
+
surface: z.array(surfaceNodeSchema),
|
|
1765
|
+
sums: z.object({
|
|
1766
|
+
user: z.number().int().nonnegative(),
|
|
1767
|
+
inject: z.number().int().nonnegative(),
|
|
1768
|
+
skill: z.number().int().nonnegative(),
|
|
1769
|
+
assistant: z.number().int().nonnegative(),
|
|
1770
|
+
tool: z.number().int().nonnegative()
|
|
1771
|
+
}).strict(),
|
|
1772
|
+
systemTokens: z.number().int().nonnegative(),
|
|
1773
|
+
systems: z.array(systemPromptNodeSchema).optional(),
|
|
1774
|
+
toolsTokens: z.number().int().nonnegative(),
|
|
1775
|
+
model: z.string().optional(),
|
|
1776
|
+
provider: z.string().optional(),
|
|
1777
|
+
lastModel: z.string().optional(),
|
|
1778
|
+
contextWindow: z.number().optional(),
|
|
1779
|
+
requests: z.array(requestRecordSchema),
|
|
1780
|
+
turnRuns: z.number().int().nonnegative().optional(),
|
|
1781
|
+
events: z.array(contextEventSchema),
|
|
1782
|
+
archived: z.array(surfaceNodeSchema),
|
|
1783
|
+
cost: costUsageSchema.optional(),
|
|
1784
|
+
archiveFloor: z.number().optional(),
|
|
1785
|
+
timing: timingTotalsSchema.optional(),
|
|
1786
|
+
humanInputs: z.number().int().nonnegative().optional(),
|
|
1787
|
+
answers: z.number().int().nonnegative().optional(),
|
|
1788
|
+
lastUser: z.string().optional(),
|
|
1789
|
+
stepStart: z.object({
|
|
1790
|
+
time: z.number(),
|
|
1791
|
+
firstToken: z.number().optional()
|
|
1792
|
+
}).strict().optional(),
|
|
1793
|
+
stepWaits: z.array(z.object({
|
|
1794
|
+
start: z.number(),
|
|
1795
|
+
end: z.number()
|
|
1796
|
+
}).strict()).optional(),
|
|
1797
|
+
stepApprovals: z.record(z.string(), z.number()).optional(),
|
|
1798
|
+
stepRequestSeq: z.number().optional(),
|
|
1799
|
+
callNames: z.record(z.string(), z.object({
|
|
1800
|
+
name: z.string(),
|
|
1801
|
+
start: z.number(),
|
|
1802
|
+
argsRaw: z.string().optional()
|
|
1803
|
+
}).strict()),
|
|
1804
|
+
pendingShadowedSeqs: z.array(z.number()).optional(),
|
|
1805
|
+
pendingShadowEventSeq: z.number().optional(),
|
|
1806
|
+
detailRev: z.number().int().nonnegative().optional(),
|
|
1807
|
+
fileOps: z.array(fileOpSchema),
|
|
1808
|
+
fileOpsFloor: z.number().int().nonnegative().optional(),
|
|
1809
|
+
spans: z.array(timingSpanSchema),
|
|
1810
|
+
stepSpans: z.array(timingSpanSchema).optional(),
|
|
1811
|
+
pendingCodeOps: z.record(z.string(), z.array(fileOpSchema)).optional()
|
|
1812
|
+
});
|
|
1813
|
+
function createContextTimelineDefinition(config, slim) {
|
|
1814
|
+
const bounds = resolveBounds(config);
|
|
1815
|
+
const view = (state) => slim() ? buildTimelineHead(state) : buildTimelineView(state, bounds);
|
|
1816
|
+
return {
|
|
1817
|
+
key: "contextTimeline",
|
|
1818
|
+
stateSchema: timelineStateSchema,
|
|
1819
|
+
wire: {
|
|
1820
|
+
viewSchema: contextTimelineSchema,
|
|
1821
|
+
view
|
|
1822
|
+
},
|
|
1823
|
+
init: () => createTimelineState(),
|
|
1824
|
+
apply: (state, event) => applyTimeline(state, event, bounds),
|
|
1825
|
+
stateVersion: 27
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
//#endregion
|
|
1829
|
+
//#region src/shared/days.ts
|
|
1830
|
+
function dayKeyOf(time) {
|
|
1831
|
+
if (!Number.isFinite(time)) return null;
|
|
1832
|
+
const d = new Date(time);
|
|
1833
|
+
const y = d.getFullYear();
|
|
1834
|
+
if (!Number.isFinite(y) || y < 1970 || y > 9999) return null;
|
|
1835
|
+
return `${String(y).padStart(4, "0")}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
1836
|
+
}
|
|
1837
|
+
//#endregion
|
|
1838
|
+
//#region src/host/activity.ts
|
|
1839
|
+
const MAX_KEPT_DAYS = 400;
|
|
1840
|
+
const MAX_SKILLS_PER_DAY = 100;
|
|
1841
|
+
const activitySkillSchema = z.object({
|
|
1842
|
+
n: z.number().int().nonnegative(),
|
|
1843
|
+
last: z.number()
|
|
1844
|
+
}).strict();
|
|
1845
|
+
const activityDaySchema = z.object({
|
|
1846
|
+
tokens: z.number().int().nonnegative(),
|
|
1847
|
+
requests: z.number().int().nonnegative(),
|
|
1848
|
+
cost: costUsageSchema.optional(),
|
|
1849
|
+
skills: z.record(z.string(), activitySkillSchema).optional()
|
|
1975
1850
|
}).strict();
|
|
1976
|
-
/** Validate the wire payload before it leaves the host (strict: no drift). */
|
|
1977
1851
|
const contextActivitySchema = z.object({ days: z.record(z.string(), activityDaySchema) }).strict();
|
|
1978
|
-
/** The persisted fold-state schema — validated before a checkpoint row seeds a fold. */
|
|
1979
1852
|
const activityStateSchema = z.object({
|
|
1980
1853
|
days: z.record(z.string(), activityDaySchema),
|
|
1981
1854
|
model: z.string().optional(),
|
|
1982
1855
|
provider: z.string().optional(),
|
|
1983
|
-
stepStart: z.number().optional()
|
|
1856
|
+
stepStart: z.number().optional(),
|
|
1857
|
+
skillCalls: z.array(z.string()).optional()
|
|
1984
1858
|
});
|
|
1985
|
-
/**
|
|
1986
|
-
* One durable usage object's billed buckets, or null when NO bucket is
|
|
1987
|
-
* readable (the same rule the timeline fold bills by: a settlement that
|
|
1988
|
-
* carried no usage still counts its request without fabricating tokens).
|
|
1989
|
-
* Every bucket passes the shared per-bucket sanitizer ({@link tokenCountOf}
|
|
1990
|
-
* — fractions round, negatives clamp, garbage reads absent).
|
|
1991
|
-
*/
|
|
1992
1859
|
function billedBucketsOf(value) {
|
|
1993
1860
|
if (value === null || typeof value !== "object") return null;
|
|
1994
1861
|
const usage = value;
|
|
@@ -2004,48 +1871,59 @@ function billedBucketsOf(value) {
|
|
|
2004
1871
|
output: output ?? 0
|
|
2005
1872
|
};
|
|
2006
1873
|
}
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
* The pricing period splits off the SAME instant the day bucket does: a fee
|
|
2012
|
-
* can never land on one day at the neighbouring window's rate.
|
|
2013
|
-
*/
|
|
2014
|
-
function pricedDayOf(prev, provider, model, period, usage) {
|
|
2015
|
-
const models = prev?.[provider] ?? {};
|
|
2016
|
-
const periods = models[model] ?? {};
|
|
2017
|
-
const b = periods[period] ?? {
|
|
2018
|
-
uncached: 0,
|
|
2019
|
-
cacheRead: 0,
|
|
2020
|
-
cacheWrite: 0,
|
|
2021
|
-
output: 0
|
|
1874
|
+
function withDayEntry(state, key, entry) {
|
|
1875
|
+
let days = {
|
|
1876
|
+
...state.days,
|
|
1877
|
+
[key]: entry
|
|
2022
1878
|
};
|
|
1879
|
+
const keys = Object.keys(days);
|
|
1880
|
+
if (keys.length > MAX_KEPT_DAYS) {
|
|
1881
|
+
keys.sort();
|
|
1882
|
+
const drop = new Set(keys.slice(0, keys.length - MAX_KEPT_DAYS));
|
|
1883
|
+
days = Object.fromEntries(Object.entries(days).filter(([k]) => !drop.has(k)));
|
|
1884
|
+
}
|
|
2023
1885
|
return {
|
|
2024
|
-
...
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
1886
|
+
...state,
|
|
1887
|
+
days
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
function bookSkillLoad(state, name, time) {
|
|
1891
|
+
const key = dayKeyOf(time);
|
|
1892
|
+
if (key === null) return state;
|
|
1893
|
+
const prev = state.days[key];
|
|
1894
|
+
const existing = prev?.skills?.[name];
|
|
1895
|
+
if (existing === void 0 && prev?.skills !== void 0 && Object.keys(prev.skills).length >= MAX_SKILLS_PER_DAY) return state;
|
|
1896
|
+
const skills = {
|
|
1897
|
+
...prev?.skills,
|
|
1898
|
+
[name]: {
|
|
1899
|
+
n: (existing?.n ?? 0) + 1,
|
|
1900
|
+
last: existing === void 0 ? time : Math.max(existing.last, time)
|
|
2036
1901
|
}
|
|
2037
1902
|
};
|
|
1903
|
+
return withDayEntry(state, key, {
|
|
1904
|
+
tokens: prev?.tokens ?? 0,
|
|
1905
|
+
requests: prev?.requests ?? 0,
|
|
1906
|
+
...prev?.cost !== void 0 ? { cost: prev.cost } : {},
|
|
1907
|
+
skills
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
function invokedSkillOf(event) {
|
|
1911
|
+
const data = event.data;
|
|
1912
|
+
const carrier = event.type === "developer/message" ? data?.message : data;
|
|
1913
|
+
const source = carrier !== null && typeof carrier === "object" ? carrier.source : void 0;
|
|
1914
|
+
if (source === null || typeof source !== "object") return "";
|
|
1915
|
+
const rec = source;
|
|
1916
|
+
return rec.kind === "skill-invocation" && typeof rec.name === "string" ? rec.name : "";
|
|
1917
|
+
}
|
|
1918
|
+
function toolResultCallIdOf(message) {
|
|
1919
|
+
if (message === null || typeof message !== "object") return null;
|
|
1920
|
+
const rec = message;
|
|
1921
|
+
if (typeof rec.toolCallId === "string" && rec.toolCallId !== "") return rec.toolCallId;
|
|
1922
|
+
const source = rec.source;
|
|
1923
|
+
if (source === null || typeof source !== "object") return null;
|
|
1924
|
+
const callId = source.callId;
|
|
1925
|
+
return typeof callId === "string" && callId !== "" ? callId : null;
|
|
2038
1926
|
}
|
|
2039
|
-
/**
|
|
2040
|
-
* Fold one committed event into the ledger. Four event types advance it:
|
|
2041
|
-
* `request/header` tracks the route in force (the timeline fold's rule —
|
|
2042
|
-
* last header wins), `step/start`/`step/end` maintain the initiation stamp,
|
|
2043
|
-
* and `assistant/message` (the step settlement — both supported log
|
|
2044
|
-
* generations) books the day. Everything else returns the state reference
|
|
2045
|
-
* unchanged, as does a settlement with no attributable instant. The next
|
|
2046
|
-
* state is a copy along the mutated path only — the persisted previous
|
|
2047
|
-
* state is never mutated in place.
|
|
2048
|
-
*/
|
|
2049
1927
|
function applyActivity(state, event) {
|
|
2050
1928
|
if (event.type === "request/header") {
|
|
2051
1929
|
const config = event.data?.header?.config;
|
|
@@ -2078,6 +1956,33 @@ function applyActivity(state, event) {
|
|
|
2078
1956
|
if (event.data?.inherited !== true) return state;
|
|
2079
1957
|
return { days: {} };
|
|
2080
1958
|
}
|
|
1959
|
+
if (event.type === "user/message" || event.type === "developer/message") {
|
|
1960
|
+
const name = invokedSkillOf(event);
|
|
1961
|
+
return name === "" ? state : bookSkillLoad(state, name, event.time);
|
|
1962
|
+
}
|
|
1963
|
+
if (event.type === "tool/call") {
|
|
1964
|
+
const data = event.data;
|
|
1965
|
+
const callId = data?.callId;
|
|
1966
|
+
if (data?.name !== "skill" || typeof callId !== "string" || callId === "") return state;
|
|
1967
|
+
const pending = state.skillCalls ?? [];
|
|
1968
|
+
if (pending.includes(callId)) return state;
|
|
1969
|
+
return {
|
|
1970
|
+
...state,
|
|
1971
|
+
skillCalls: [...pending, callId].slice(-100)
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
if (event.type === "tool/result") {
|
|
1975
|
+
const message = event.data?.message;
|
|
1976
|
+
const callId = toolResultCallIdOf(message);
|
|
1977
|
+
const pending = state.skillCalls;
|
|
1978
|
+
if (callId === null || pending === void 0 || !pending.includes(callId)) return state;
|
|
1979
|
+
const skillCalls = pending.filter((id) => id !== callId);
|
|
1980
|
+
const next = { ...state };
|
|
1981
|
+
if (skillCalls.length > 0) next.skillCalls = skillCalls;
|
|
1982
|
+
else delete next.skillCalls;
|
|
1983
|
+
const name = skillNameOf(message);
|
|
1984
|
+
return name === "" ? next : bookSkillLoad(next, name, event.time);
|
|
1985
|
+
}
|
|
2081
1986
|
if (event.type !== "assistant/message") return state;
|
|
2082
1987
|
const initiated = state.stepStart !== void 0 ? state.stepStart : event.time;
|
|
2083
1988
|
const key = dayKeyOf(initiated);
|
|
@@ -2086,40 +1991,14 @@ function applyActivity(state, event) {
|
|
|
2086
1991
|
const buckets = billedBucketsOf(data?.usage);
|
|
2087
1992
|
const prev = state.days[key];
|
|
2088
1993
|
let cost = prev?.cost;
|
|
2089
|
-
if (buckets !== null && state.model !== void 0)
|
|
2090
|
-
|
|
2091
|
-
const period = isDeepSeekProvider(provider) && !isPeakUtc(initiated) ? "off" : "peak";
|
|
2092
|
-
cost = pricedDayOf(cost, provider, state.model, period, buckets);
|
|
2093
|
-
}
|
|
2094
|
-
const entry = {
|
|
1994
|
+
if (buckets !== null && state.model !== void 0) cost = addBilledUsage(cost, state.provider ?? "", state.model, initiated, buckets);
|
|
1995
|
+
return withDayEntry(state, key, {
|
|
2095
1996
|
tokens: (prev === void 0 ? 0 : prev.tokens) + (buckets === null ? 0 : buckets.input + buckets.cacheRead + buckets.cacheWrite + buckets.output),
|
|
2096
1997
|
requests: (prev === void 0 ? 0 : prev.requests) + 1,
|
|
2097
|
-
...cost !== void 0 ? { cost } : {}
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
...state.days,
|
|
2101
|
-
[key]: entry
|
|
2102
|
-
};
|
|
2103
|
-
const keys = Object.keys(days);
|
|
2104
|
-
if (keys.length > MAX_KEPT_DAYS) {
|
|
2105
|
-
keys.sort();
|
|
2106
|
-
const drop = new Set(keys.slice(0, keys.length - MAX_KEPT_DAYS));
|
|
2107
|
-
days = Object.fromEntries(Object.entries(days).filter(([k]) => !drop.has(k)));
|
|
2108
|
-
}
|
|
2109
|
-
return {
|
|
2110
|
-
...state,
|
|
2111
|
-
days
|
|
2112
|
-
};
|
|
1998
|
+
...cost !== void 0 ? { cost } : {},
|
|
1999
|
+
...prev?.skills !== void 0 ? { skills: prev.skills } : {}
|
|
2000
|
+
});
|
|
2113
2001
|
}
|
|
2114
|
-
/**
|
|
2115
|
-
* The daily-activity projection unit, registered alongside the timeline and
|
|
2116
|
-
* headers units (host/index.ts); the overview reads it through the session
|
|
2117
|
-
* list's projection column. `stateVersion` 3: the fold resets the ledger at
|
|
2118
|
-
* the tagged fork/seed boundary, so a seeded session's days count post-seed
|
|
2119
|
-
* activity only (issue #94) — cached rows for seeded forks read
|
|
2120
|
-
* version-stale on upgrade and the dashboard's warm-up cold-refolds them
|
|
2121
|
-
* from the durable log, which drops the inherited prefix they booked.
|
|
2122
|
-
*/
|
|
2123
2002
|
function createContextActivityDefinition() {
|
|
2124
2003
|
return {
|
|
2125
2004
|
key: "contextActivity",
|
|
@@ -2130,17 +2009,9 @@ function createContextActivityDefinition() {
|
|
|
2130
2009
|
viewSchema: contextActivitySchema,
|
|
2131
2010
|
view: (state) => ({ days: Object.fromEntries(Object.entries(state.days).map(([k, v]) => [k, { ...v }])) })
|
|
2132
2011
|
},
|
|
2133
|
-
stateVersion:
|
|
2012
|
+
stateVersion: 4
|
|
2134
2013
|
};
|
|
2135
2014
|
}
|
|
2136
|
-
/**
|
|
2137
|
-
* Recover the MCP server display label from a proxied tool name, or undefined
|
|
2138
|
-
* for non-MCP names. `dsh-mcp-client` names tools `mcp__<server>__<rawName>`
|
|
2139
|
-
* (normalized, and hash-appended when overlong/invalid — the label then shows
|
|
2140
|
-
* whatever of the server survived the truncation). The separating `__` is the
|
|
2141
|
-
* LAST one in the name, and it must sit AFTER the `mcp__` prefix: the prefix's
|
|
2142
|
-
* own separator (or an empty server right after it) is not a server.
|
|
2143
|
-
*/
|
|
2144
2015
|
function mcpServerOf(name) {
|
|
2145
2016
|
if (!name.startsWith("mcp__")) return void 0;
|
|
2146
2017
|
const cut = name.lastIndexOf("__");
|
|
@@ -2148,23 +2019,13 @@ function mcpServerOf(name) {
|
|
|
2148
2019
|
const server = name.slice(5, cut);
|
|
2149
2020
|
return server.length > 0 ? server : void 0;
|
|
2150
2021
|
}
|
|
2151
|
-
/** The `mcp:<server>` display label of a proxied tool name, or undefined. */
|
|
2152
2022
|
function mcpSourceOf(name) {
|
|
2153
2023
|
const server = mcpServerOf(name);
|
|
2154
2024
|
return server !== void 0 ? `mcp:${server}` : void 0;
|
|
2155
2025
|
}
|
|
2156
|
-
/** The pinned package of a first-party tool name, or undefined. */
|
|
2157
2026
|
function pinnedSourceOf(name) {
|
|
2158
2027
|
return FIRST_PARTY_SOURCES[name];
|
|
2159
2028
|
}
|
|
2160
|
-
/**
|
|
2161
|
-
* Pinned first-party tool → plugin package map (see header comment). One entry
|
|
2162
|
-
* per model-facing name of the shipped tool packages in the official
|
|
2163
|
-
* tool-schema catalog of the supported dsh baseline; names are verified
|
|
2164
|
-
* stable across the supported harness range. Packages that mount under
|
|
2165
|
-
* distinct names per composition (bash/pwsh persistent variants, the
|
|
2166
|
-
* `subagent_fork` fixed-route alias) map to their primary package.
|
|
2167
|
-
*/
|
|
2168
2029
|
const FIRST_PARTY_SOURCES = Object.freeze({
|
|
2169
2030
|
read: "@deepseek-ai/dsh-tool-fs",
|
|
2170
2031
|
write: "@deepseek-ai/dsh-tool-fs",
|
|
@@ -2203,68 +2064,8 @@ const FIRST_PARTY_SOURCES = Object.freeze({
|
|
|
2203
2064
|
});
|
|
2204
2065
|
//#endregion
|
|
2205
2066
|
//#region src/host/attribution.ts
|
|
2206
|
-
/**
|
|
2207
|
-
* Live tool→plugin attribution layered on the static recovery in
|
|
2208
|
-
* toolSources.ts.
|
|
2209
|
-
*
|
|
2210
|
-
* The session log records tools as plain `ToolSchema` entries (name /
|
|
2211
|
-
* description / parameters) — the registering plugin is not in there.
|
|
2212
|
-
* toolSources.ts derives the deterministic sources (harness-logged field,
|
|
2213
|
-
* `mcp:<server>` naming, pinned first-party map). This module additionally
|
|
2214
|
-
* watches RUNTIME registrations: cordis fires the `internal/get` waterfall on
|
|
2215
|
-
* every context read of a service property, passing the READING context as the
|
|
2216
|
-
* first argument, so `reader.fiber.name` identifies the plugin that is about
|
|
2217
|
-
* to call `register()`.
|
|
2218
|
-
*
|
|
2219
|
-
* - The `internal/get` handler records the reading fiber's NAME — a scalar,
|
|
2220
|
-
* never the reader context, which would pin the caller's whole agent — and
|
|
2221
|
-
* wraps the tools service's `register` (once per underlying instance —
|
|
2222
|
-
* cordis hands out a fresh traced proxy per read of a tracked service, so
|
|
2223
|
-
* the proxy is peeled to the stable instance first; an earlier wrapper of a
|
|
2224
|
-
* previous hook incarnation is peeled back to the original, so a plugin
|
|
2225
|
-
* reload re-wraps without stacking) to capture the reader at registration
|
|
2226
|
-
* time into a live map. Every wrapper is undone when the plugin unloads:
|
|
2227
|
-
* the original `register` goes back on the instance, unless a newer hook
|
|
2228
|
-
* incarnation re-wrapped it first (that incarnation's own cleanup then
|
|
2229
|
-
* owns the restore).
|
|
2230
|
-
* - When the reader slot is missing, root-named, or this plugin's own (e.g.
|
|
2231
|
-
* LOCAL-LINK plugins — dev installs via `dsh plugin add <path>` or
|
|
2232
|
-
* npm/pnpm link — whose anonymous entrypoints make cordis fall back to the
|
|
2233
|
-
* root name), the wrapped `register` falls back to the call stack: the first
|
|
2234
|
-
* frame outside this package is resolved to its nearest `package.json`
|
|
2235
|
-
* `name`. That covers both npm installs (`node_modules/<pkg>`) and local
|
|
2236
|
-
* links (any directory carrying a package.json), which never pass through
|
|
2237
|
-
* node_modules. Frames that resolve back to this package are skipped.
|
|
2238
|
-
* - `ownerOf(name)` prefers the name-derived `mcp:<server>` label (it names
|
|
2239
|
-
* the actual provider, where the live record would only ever name the MCP
|
|
2240
|
-
* proxy client), then the LIVE record — for a post-boot registration it is
|
|
2241
|
-
* the truth, even when the name collides with a pinned first-party tool —
|
|
2242
|
-
* then the pinned map (the boot-time guess for tools registered before the
|
|
2243
|
-
* hook), and finally tags tools that were ALREADY registered when the hook
|
|
2244
|
-
* installed (the boot snapshot — third-party bundles, e.g. local links like
|
|
2245
|
-
* dsh-file-claim, that applied before dsh-context) with the
|
|
2246
|
-
* `UNKNOWN_TOOL_SOURCE` sentinel: their registering plugin is unknowable,
|
|
2247
|
-
* and a bare gap would read as "no plugin" instead of "unknown plugin".
|
|
2248
|
-
*
|
|
2249
|
-
* Best-effort by design: a read separated from `register()` by an `await` can
|
|
2250
|
-
* be overwritten by another plugin's read (misattribution) and the stack
|
|
2251
|
-
* fallback needs a resolvable package.json — both degrade to the name/pinned
|
|
2252
|
-
* chain; registrations that predate the hook degrade to the unknown tag. The
|
|
2253
|
-
* hook costs roughly +1.4us per service-property read and is negligible on
|
|
2254
|
-
* the rare register path (the stack walk only runs when the reader slot is
|
|
2255
|
-
* unusable, and its package lookups are cached per directory).
|
|
2256
|
-
*/
|
|
2257
|
-
/** This module's own file URL — the stack walk skips its own frames. */
|
|
2258
2067
|
const selfUrl = normalize(fileURLToPath(import.meta.url));
|
|
2259
|
-
|
|
2260
|
-
const packageCache = /* @__PURE__ */ new Map();
|
|
2261
|
-
/**
|
|
2262
|
-
* Best-effort package name for a module file: walk up to the nearest
|
|
2263
|
-
* `package.json` carrying a `name`. Works for dependencies installed under
|
|
2264
|
-
* `node_modules` as well as local links whose package root is any on-disk
|
|
2265
|
-
* directory. The per-directory results are cached.
|
|
2266
|
-
* @param file - absolute path of a module file.
|
|
2267
|
-
*/
|
|
2068
|
+
const packageCache = new Map();
|
|
2268
2069
|
function packageNameFrom(file) {
|
|
2269
2070
|
let dir = dirname(file);
|
|
2270
2071
|
for (let depth = 0; depth < 12; depth++) {
|
|
@@ -2285,31 +2086,11 @@ function packageNameFrom(file) {
|
|
|
2285
2086
|
}
|
|
2286
2087
|
}
|
|
2287
2088
|
const FRAME_POSITION = /:\d+:\d+$/;
|
|
2288
|
-
/** Symbol cordis registers (`Symbol.for`) on its traced proxies: reading it
|
|
2289
|
-
* yields the wrapped target — the stable underlying service instance. */
|
|
2290
2089
|
const CORDIS_ORIGINAL = Symbol.for("cordis.original");
|
|
2291
|
-
/**
|
|
2292
|
-
* Peel cordis's per-read traced proxy of a tracked service (dsh's ToolRuntime
|
|
2293
|
-
* is one — every `ctx.tools` read returns a fresh proxy closing over the
|
|
2294
|
-
* reader's context) down to the instance it wraps, so identity-keyed
|
|
2295
|
-
* bookkeeping sees one stable object. Plain service objects pass through
|
|
2296
|
-
* unchanged.
|
|
2297
|
-
* @param tools - the value as handed out by a context read.
|
|
2298
|
-
*/
|
|
2299
2090
|
function rawInstanceOf(tools) {
|
|
2300
2091
|
return tools?.[CORDIS_ORIGINAL] ?? tools;
|
|
2301
2092
|
}
|
|
2302
|
-
/** Package name of this module's own package (self-fallbacks are filtered). */
|
|
2303
2093
|
const selfPackage = packageNameFrom(selfUrl);
|
|
2304
|
-
/**
|
|
2305
|
-
* Resolve the registering package from a stack trace: walk frames from the
|
|
2306
|
-
* innermost out, skipping this module's own frames and frames that resolve to
|
|
2307
|
-
* this package, and return the package name of the first frame that resolves
|
|
2308
|
-
* elsewhere. Works with both `file://` URLs and bare absolute paths
|
|
2309
|
-
* (transpiled modules render without a scheme), with optional `fn (...)` and
|
|
2310
|
-
* `async` wrappers.
|
|
2311
|
-
* @param stack - `Error().stack`, or undefined when no fallback is desired.
|
|
2312
|
-
*/
|
|
2313
2094
|
function callerPackageFrom(stack) {
|
|
2314
2095
|
if (!stack) return void 0;
|
|
2315
2096
|
for (const raw of stack.split("\n").slice(1)) {
|
|
@@ -2334,16 +2115,9 @@ function callerPackageFrom(stack) {
|
|
|
2334
2115
|
if (name !== void 0 && name !== selfPackage) return name;
|
|
2335
2116
|
}
|
|
2336
2117
|
}
|
|
2337
|
-
/**
|
|
2338
|
-
* Install the runtime-attribution hook on a cordis app context. The hook
|
|
2339
|
-
* rides the calling fiber's lifetime (`ctx.on`, and an effect that restores
|
|
2340
|
-
* every patched `register`), so it is disposed with the plugin.
|
|
2341
|
-
* @param ctx - the context the dsh-context plugin runs in; its fiber name is
|
|
2342
|
-
* excluded from attributions.
|
|
2343
|
-
*/
|
|
2344
2118
|
function createToolAttribution(ctx) {
|
|
2345
|
-
const live =
|
|
2346
|
-
const wrapped =
|
|
2119
|
+
const live = new Map();
|
|
2120
|
+
const wrapped = new WeakSet();
|
|
2347
2121
|
const self = ctx.fiber.name;
|
|
2348
2122
|
let lastReader;
|
|
2349
2123
|
const patched = [];
|
|
@@ -2358,7 +2132,7 @@ function createToolAttribution(ctx) {
|
|
|
2358
2132
|
const wrappedRegister = function(definition) {
|
|
2359
2133
|
const toolName = definition?.name;
|
|
2360
2134
|
let owner = lastReader;
|
|
2361
|
-
if (!owner || owner === "root" || owner === self) owner = callerPackageFrom(
|
|
2135
|
+
if (!owner || owner === "root" || owner === self) owner = callerPackageFrom(new Error().stack);
|
|
2362
2136
|
const dispose = original.call(this, definition);
|
|
2363
2137
|
if (typeof toolName === "string" && owner && owner !== "root" && owner !== self && owner !== selfPackage) {
|
|
2364
2138
|
live.set(toolName, owner);
|
|
@@ -2390,7 +2164,7 @@ function createToolAttribution(ctx) {
|
|
|
2390
2164
|
}, "tools.register attribution");
|
|
2391
2165
|
const toolsService = rawInstanceOf(ctx.get("tools", false));
|
|
2392
2166
|
wrapInstance(toolsService);
|
|
2393
|
-
const boot =
|
|
2167
|
+
const boot = new Set();
|
|
2394
2168
|
try {
|
|
2395
2169
|
const toolEntries = toolsService?.layers?.global?.tools;
|
|
2396
2170
|
if (toolEntries !== void 0 && typeof toolEntries.entries === "function") for (const [name] of toolEntries.entries()) boot.add(name);
|
|
@@ -2398,53 +2172,34 @@ function createToolAttribution(ctx) {
|
|
|
2398
2172
|
return { ownerOf: (name) => mcpSourceOf(name) ?? live.get(name) ?? pinnedSourceOf(name) ?? (boot.has(name) ? "<unknown-plugin>" : void 0) };
|
|
2399
2173
|
}
|
|
2400
2174
|
//#endregion
|
|
2175
|
+
//#region src/host/connection.ts
|
|
2176
|
+
function fetchRouteRegistrar(connection) {
|
|
2177
|
+
return typeof connection?.fetch?.register === "function" ? connection.fetch.register.bind(connection.fetch) : void 0;
|
|
2178
|
+
}
|
|
2179
|
+
//#endregion
|
|
2401
2180
|
//#region src/host/balance.ts
|
|
2402
|
-
/** The plugin's balance route, under the authenticated `/api` fence. */
|
|
2403
2181
|
const BALANCE_ROUTE = "/api/dsh-context/balance";
|
|
2404
|
-
/** The settings id the DeepSeek API-key provider has served its connection
|
|
2405
|
-
* section under: the V3 registered namespace, kept as the entry id on V4+
|
|
2406
|
-
* product profiles. */
|
|
2407
2182
|
const DEEPSEEK_SETTINGS_NS = "llm-deepseek";
|
|
2408
|
-
/** llm-deepseek's default credential ref (the env-var name its section resolves). */
|
|
2409
|
-
const DEFAULT_API_KEY_ENV = "DEEPSEEK_API_KEY";
|
|
2410
|
-
/** The platform's public API root (llm-deepseek's PUBLIC_BASE_URL default). */
|
|
2411
2183
|
const PUBLIC_BASE_URL = "https://api.deepseek.com";
|
|
2412
|
-
/** One platform read's whole budget — never worth blocking the route longer. */
|
|
2413
2184
|
const FETCH_TIMEOUT_MS = 1e4;
|
|
2414
|
-
|
|
2415
|
-
function asRecord$1(value) {
|
|
2185
|
+
function asRecord$2(value) {
|
|
2416
2186
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2417
2187
|
}
|
|
2418
|
-
/** One platform amount ('110.00', or an already-numeric producer variant), or null. */
|
|
2419
2188
|
function amountOf(value) {
|
|
2420
2189
|
const n = typeof value === "string" ? Number(value) : value;
|
|
2421
2190
|
return typeof n === "number" && Number.isFinite(n) && n >= 0 ? n : null;
|
|
2422
2191
|
}
|
|
2423
|
-
/**
|
|
2424
|
-
* The DeepSeek API-key provider's settings section, read through whichever
|
|
2425
|
-
* face the running line serves. V3's `get` reads the registered
|
|
2426
|
-
* `llm-deepseek` section directly; V4+ retired that face, so its `describe()`
|
|
2427
|
-
* projection is folded instead — the provider's row is the one whose served
|
|
2428
|
-
* value declares the top-level `apiKeyEnv` credential ref (the volatile shape
|
|
2429
|
-
* only that provider declares at the section root), preferred under the entry
|
|
2430
|
-
* ids the generations have served it as (`llm-deepseek` on product profiles,
|
|
2431
|
-
* `llm-deepseek-api-key` elsewhere). Rows fold in isolation — a hostile row
|
|
2432
|
-
* drops whole, valid siblings keep serving — and a describe that throws is no
|
|
2433
|
-
* section at all.
|
|
2434
|
-
*/
|
|
2435
2192
|
function deepseekSectionOf(ctx) {
|
|
2436
2193
|
const settings = ctx.get("settings");
|
|
2437
|
-
const section = typeof settings?.get === "function" ? asRecord$1(settings.get(DEEPSEEK_SETTINGS_NS)) : null;
|
|
2438
|
-
if (section !== null) return section;
|
|
2439
2194
|
if (typeof settings?.describe !== "function") return null;
|
|
2440
2195
|
try {
|
|
2441
2196
|
const rows = settings.describe();
|
|
2442
2197
|
if (!Array.isArray(rows)) return null;
|
|
2443
2198
|
let shaped = null;
|
|
2444
2199
|
for (const row of rows) try {
|
|
2445
|
-
const record = asRecord$
|
|
2200
|
+
const record = asRecord$2(row);
|
|
2446
2201
|
if (record === null) continue;
|
|
2447
|
-
const value = asRecord$
|
|
2202
|
+
const value = asRecord$2(record.value);
|
|
2448
2203
|
if (value === null || typeof value.apiKeyEnv !== "string" || value.apiKeyEnv === "") continue;
|
|
2449
2204
|
if (record.ns === DEEPSEEK_SETTINGS_NS || record.ns === "llm-deepseek-api-key") return value;
|
|
2450
2205
|
shaped ??= value;
|
|
@@ -2456,16 +2211,10 @@ function deepseekSectionOf(ctx) {
|
|
|
2456
2211
|
return null;
|
|
2457
2212
|
}
|
|
2458
2213
|
}
|
|
2459
|
-
/**
|
|
2460
|
-
* Resolve the DeepSeek connection facts exactly as llm-deepseek serves its
|
|
2461
|
-
* requests: its settings row for the credential ref and endpoint, the
|
|
2462
|
-
* credentials service for the key. `null` whenever any fact is missing —
|
|
2463
|
-
* the platform is simply not configured for this deployment.
|
|
2464
|
-
*/
|
|
2465
2214
|
async function resolveFacts(ctx) {
|
|
2466
2215
|
const section = deepseekSectionOf(ctx);
|
|
2467
2216
|
if (section === null) return null;
|
|
2468
|
-
const apiKeyEnv =
|
|
2217
|
+
const apiKeyEnv = section.apiKeyEnv;
|
|
2469
2218
|
const baseUrl = typeof section.baseURL === "string" && section.baseURL !== "" ? section.baseURL : PUBLIC_BASE_URL;
|
|
2470
2219
|
const credentials = ctx.get("credentials");
|
|
2471
2220
|
const resolve = typeof credentials?.resolve === "function" ? credentials.resolve.bind(credentials) : void 0;
|
|
@@ -2482,22 +2231,13 @@ async function resolveFacts(ctx) {
|
|
|
2482
2231
|
apiKey
|
|
2483
2232
|
};
|
|
2484
2233
|
}
|
|
2485
|
-
/**
|
|
2486
|
-
* Narrow the platform's payload to the wire value (the boundary rigor every
|
|
2487
|
-
* parser owes untrusted input): each entry's currency and parts are re-proved
|
|
2488
|
-
* and the total is the two summed — the platform's own `total_balance` rounds
|
|
2489
|
-
* independently of them, so it can land a cent away from what the breakdown
|
|
2490
|
-
* beside it adds up to, and the viewer's arithmetic is the authority. An entry
|
|
2491
|
-
* failing the shape drops whole, and a payload with no valid entry is no
|
|
2492
|
-
* balance at all.
|
|
2493
|
-
*/
|
|
2494
2234
|
function balanceOfPayload(value) {
|
|
2495
|
-
const data = asRecord$
|
|
2235
|
+
const data = asRecord$2(value);
|
|
2496
2236
|
if (data === null) return null;
|
|
2497
2237
|
const infos = Array.isArray(data.balance_infos) ? data.balance_infos : [];
|
|
2498
2238
|
const balances = [];
|
|
2499
2239
|
for (const info of infos) try {
|
|
2500
|
-
const entry = asRecord$
|
|
2240
|
+
const entry = asRecord$2(info);
|
|
2501
2241
|
if (entry === null) continue;
|
|
2502
2242
|
const currency = entry.currency;
|
|
2503
2243
|
const granted = amountOf(entry.granted_balance);
|
|
@@ -2517,19 +2257,9 @@ function balanceOfPayload(value) {
|
|
|
2517
2257
|
balances
|
|
2518
2258
|
} : null;
|
|
2519
2259
|
}
|
|
2520
|
-
/** The outbound platform fetch through the real global fetch. */
|
|
2521
2260
|
const defaultFetcher = (url, init) => fetch(url, init);
|
|
2522
2261
|
let fetcher = defaultFetcher;
|
|
2523
2262
|
let inFlight = null;
|
|
2524
|
-
/**
|
|
2525
|
-
* One platform read for these facts, shared by the identical reads already in
|
|
2526
|
-
* flight. Each read that finds none goes to the platform: the account's
|
|
2527
|
-
* balance moves with every request the harness bills, so a remembered figure
|
|
2528
|
-
* would disagree with the console page the capsule links to. The client's own
|
|
2529
|
-
* open-time read is the rate limit. Every failure path — transport, timeout,
|
|
2530
|
-
* non-ok status, malformed payload — resolves `null`; the route never throws
|
|
2531
|
-
* into the transport.
|
|
2532
|
-
*/
|
|
2533
2263
|
async function readBalance(facts) {
|
|
2534
2264
|
if (inFlight !== null) return inFlight;
|
|
2535
2265
|
inFlight = (async () => {
|
|
@@ -2553,16 +2283,9 @@ async function readBalance(facts) {
|
|
|
2553
2283
|
inFlight = null;
|
|
2554
2284
|
}
|
|
2555
2285
|
}
|
|
2556
|
-
/**
|
|
2557
|
-
* Serve the balance route whenever the connection service composes (see
|
|
2558
|
-
* host/detail.ts for the load-order contract). The registration rides the
|
|
2559
|
-
* injected fiber: the service unloading withdraws the route. A rejecting
|
|
2560
|
-
* registry only closes the capsule — never the plugin.
|
|
2561
|
-
*/
|
|
2562
2286
|
function watchBalanceChannel(ctx) {
|
|
2563
2287
|
ctx.inject(["connection"], (c) => {
|
|
2564
|
-
const
|
|
2565
|
-
const register = typeof connection?.fetch?.register === "function" ? connection.fetch.register.bind(connection.fetch) : void 0;
|
|
2288
|
+
const register = fetchRouteRegistrar(c.get("connection"));
|
|
2566
2289
|
if (register === void 0) return;
|
|
2567
2290
|
const handler = async () => {
|
|
2568
2291
|
const facts = await resolveFacts(ctx);
|
|
@@ -2586,39 +2309,7 @@ function watchBalanceChannel(ctx) {
|
|
|
2586
2309
|
}
|
|
2587
2310
|
//#endregion
|
|
2588
2311
|
//#region src/host/coldRead.ts
|
|
2589
|
-
/**
|
|
2590
|
-
* The host-wide cold-read governor (issue #121).
|
|
2591
|
-
*
|
|
2592
|
-
* Every cold read — the warm-up pass's per-session fold (backfill.ts) and
|
|
2593
|
-
* the detail route's cold rung (detail.ts) — materializes one session's
|
|
2594
|
-
* whole durable log: the persistence backend decodes every frame before it
|
|
2595
|
-
* can slice a single event, so a 10k-event session lands as a several-
|
|
2596
|
-
* hundred-MB object graph, and the harness pins its own floors on top (the
|
|
2597
|
-
* backend's two-entry decoded-log memo, the observation reader's five-entry
|
|
2598
|
-
* prepared-session cache). Those floors are the harness's to tune; what the
|
|
2599
|
-
* PLUGIN owns is how many of its cold reads run at once and whether a new
|
|
2600
|
-
* one starts on an already-hot heap. Before this gate the answer to both
|
|
2601
|
-
* was "unbounded": the dashboard's first open fires the corpus pass and the
|
|
2602
|
-
* session cards' / Agent network's per-session detail fan-out together, and
|
|
2603
|
-
* on a capped-heap host (an Electron main whose V8 ceiling cannot move)
|
|
2604
|
-
* that stacking was enough to push the process over its limit.
|
|
2605
|
-
*
|
|
2606
|
-
* The gate therefore admits one cold read at a time (FIFO) and only while
|
|
2607
|
-
* the heap leaves headroom for it. A skipped read never fails visibly: the
|
|
2608
|
-
* pass stops early (its folded rows persist; the rest refold on their next
|
|
2609
|
-
* live activity or a later process's pass) and the detail route answers its
|
|
2610
|
-
* typed null (the client keeps its last detail, the cards their no-data
|
|
2611
|
-
* note) — every degradation already designed, no functionality removed.
|
|
2612
|
-
*/
|
|
2613
|
-
/**
|
|
2614
|
-
* The high-water mark as a fraction of the process's own heap ceiling: a
|
|
2615
|
-
* cold read starts only below two-thirds full, leaving the top third for
|
|
2616
|
-
* the read itself plus the harness's floors. The ceiling is V8's real
|
|
2617
|
-
* limit, so every host class — a default-heap CLI, a capped Electron
|
|
2618
|
-
* main — gets its own mark from the same constant.
|
|
2619
|
-
*/
|
|
2620
2312
|
const HIGH_WATER = 2 / 3;
|
|
2621
|
-
/** The process's own V8 reading. */
|
|
2622
2313
|
function v8Reading() {
|
|
2623
2314
|
const stats = getHeapStatistics();
|
|
2624
2315
|
return {
|
|
@@ -2627,11 +2318,6 @@ function v8Reading() {
|
|
|
2627
2318
|
};
|
|
2628
2319
|
}
|
|
2629
2320
|
const noop = () => {};
|
|
2630
|
-
/**
|
|
2631
|
-
* One governor per host process, shared by both cold paths. `reading` is
|
|
2632
|
-
* injectable for tests; a throwing or garbage probe fails OPEN — the meter
|
|
2633
|
-
* must never block a read (the version probe's philosophy).
|
|
2634
|
-
*/
|
|
2635
2321
|
function makeColdReadGate(reading = v8Reading) {
|
|
2636
2322
|
const clear = () => {
|
|
2637
2323
|
try {
|
|
@@ -2651,68 +2337,32 @@ function makeColdReadGate(reading = v8Reading) {
|
|
|
2651
2337
|
}
|
|
2652
2338
|
//#endregion
|
|
2653
2339
|
//#region src/host/backfill.ts
|
|
2654
|
-
/** The plugin's warm-up trigger route, under the authenticated `/api` fence. */
|
|
2655
2340
|
const BACKFILL_ROUTE = "/api/dsh-context/backfill";
|
|
2656
|
-
/**
|
|
2657
|
-
* Inter-session pacing so a multi-hundred-session backfill never starve the
|
|
2658
|
-
* host: each cold read is a full durable-log decode, and a boot-time host
|
|
2659
|
-
* under sustained read pressure stalls its own client handshake (the
|
|
2660
|
-
* workspace sidebar rides it), so the pass breathes between sessions.
|
|
2661
|
-
*/
|
|
2662
2341
|
const YIELD_MS = 100;
|
|
2663
|
-
/**
|
|
2664
|
-
* The `name` family dsh raises for a durable log it cannot interpret. The
|
|
2665
|
-
* format edge throws `SessionFormatUnsupportedMigrationError`, but the
|
|
2666
|
-
* persistence seam translates it — together with every other "intact yet not
|
|
2667
|
-
* interpretable" refusal — into `SessionFormatUnsupportedError` before the
|
|
2668
|
-
* error escapes (issue #75): classify by the family prefix, since either
|
|
2669
|
-
* exact name may be the one that arrives.
|
|
2670
|
-
*/
|
|
2671
2342
|
const UNSUPPORTED_FORMAT = "SessionFormatUnsupported";
|
|
2672
|
-
function asRecord(value) {
|
|
2343
|
+
function asRecord$1(value) {
|
|
2673
2344
|
return value !== null && typeof value === "object" ? value : null;
|
|
2674
2345
|
}
|
|
2675
|
-
/** One listed record's header, re-proved: id and cwd are the two fields the run relies on. */
|
|
2676
2346
|
function headerOf(record) {
|
|
2677
|
-
const header = asRecord(asRecord(record)?.header);
|
|
2347
|
+
const header = asRecord$1(asRecord$1(record)?.header);
|
|
2678
2348
|
if (header === null) return null;
|
|
2679
2349
|
if (typeof header.id !== "string" || header.id === "") return null;
|
|
2680
2350
|
return header;
|
|
2681
2351
|
}
|
|
2682
|
-
/** The probe keys: the projection keys the cache must already serve. */
|
|
2683
2352
|
const PROBE_KEYS = ["contextActivity", "contextTimeline"];
|
|
2684
|
-
/**
|
|
2685
|
-
* Probe the cache through the face the running generation serves. The older
|
|
2686
|
-
* spelling is tried first and the newer one answers the fallback: on a
|
|
2687
|
-
* 0.1.7-rc.2 cache the 3-arg call binds the branded offset onto `keys`,
|
|
2688
|
-
* where `new Set(0)` throws as soon as a served record is reached — while a
|
|
2689
|
-
* record-less session returns `undefined` without throwing, which is the
|
|
2690
|
-
* correct "not served" either way. On 0.1.5-rc.1 the 3-arg face answers
|
|
2691
|
-
* directly; if it ever throws there, the 2-arg retry degrades to the same
|
|
2692
|
-
* "not served" verdict through the caller's catch.
|
|
2693
|
-
*/
|
|
2694
|
-
function cachedSnapshotOf(cache, header) {
|
|
2695
|
-
try {
|
|
2696
|
-
return cache.cachedSnapshot(header, SessionLogOffset(0), PROBE_KEYS);
|
|
2697
|
-
} catch {
|
|
2698
|
-
return cache.cachedSnapshot(header, PROBE_KEYS);
|
|
2699
|
-
}
|
|
2700
|
-
}
|
|
2701
|
-
/** Whether the cache already serves BOTH projection rows for this header (nothing to backfill). */
|
|
2702
2353
|
function servesRows(cache, header) {
|
|
2703
2354
|
try {
|
|
2704
|
-
const values = asRecord(asRecord(
|
|
2355
|
+
const values = asRecord$1(asRecord$1(cache.cachedSnapshot(header, PROBE_KEYS))?.values);
|
|
2705
2356
|
return values !== null && values.contextActivity !== void 0 && values.contextTimeline !== void 0;
|
|
2706
2357
|
} catch {
|
|
2707
2358
|
return false;
|
|
2708
2359
|
}
|
|
2709
2360
|
}
|
|
2710
|
-
/** Read one stored log through a read handle, mirroring dsh-session-query's readColdSessionLog. */
|
|
2711
2361
|
async function readColdLog(persistence, id, signal) {
|
|
2712
2362
|
const handle = await persistence.open(id, "read", { signal });
|
|
2713
2363
|
let events;
|
|
2714
2364
|
try {
|
|
2715
|
-
const result = asRecord(await handle.read(0, void 0, { signal }))?.events;
|
|
2365
|
+
const result = asRecord$1(await handle.read(0, void 0, { signal }))?.events;
|
|
2716
2366
|
events = Array.isArray(result) ? result : [];
|
|
2717
2367
|
} catch (error) {
|
|
2718
2368
|
try {
|
|
@@ -2727,12 +2377,6 @@ async function readColdLog(persistence, id, signal) {
|
|
|
2727
2377
|
events: [...events, ...interruptedTurnClosers(events)]
|
|
2728
2378
|
};
|
|
2729
2379
|
}
|
|
2730
|
-
/**
|
|
2731
|
-
* Whether one session is live (folds for itself — a cold write would only
|
|
2732
|
-
* race its own checkpoints). A throwing registry read conservatively skips
|
|
2733
|
-
* the session too: better to leave a row unfolded than to write over a
|
|
2734
|
-
* possibly-live one.
|
|
2735
|
-
*/
|
|
2736
2380
|
function isLive(sessions, id) {
|
|
2737
2381
|
if (sessions === null || typeof sessions.get !== "function") return false;
|
|
2738
2382
|
try {
|
|
@@ -2741,21 +2385,13 @@ function isLive(sessions, id) {
|
|
|
2741
2385
|
return true;
|
|
2742
2386
|
}
|
|
2743
2387
|
}
|
|
2744
|
-
/**
|
|
2745
|
-
* Whether the cold read failed because the running harness refuses this log's
|
|
2746
|
-
* format — a permanent, source-side refusal (the artifact is left unchanged,
|
|
2747
|
-
* so retrying cannot succeed). Matched by the error's documented `name`, so no
|
|
2748
|
-
* dsh symbol needs importing and every supported baseline classifies alike: an
|
|
2749
|
-
* unknown error shape just falls to the warn path.
|
|
2750
|
-
*/
|
|
2751
2388
|
function isUnsupportedFormat(error) {
|
|
2752
2389
|
try {
|
|
2753
|
-
return String(asRecord(error)?.name).startsWith(UNSUPPORTED_FORMAT);
|
|
2390
|
+
return String(asRecord$1(error)?.name).startsWith(UNSUPPORTED_FORMAT);
|
|
2754
2391
|
} catch {
|
|
2755
2392
|
return false;
|
|
2756
2393
|
}
|
|
2757
2394
|
}
|
|
2758
|
-
/** The error's printable form — a hostile error may throw on its own toString. */
|
|
2759
2395
|
function messageOf(error) {
|
|
2760
2396
|
try {
|
|
2761
2397
|
return String(error);
|
|
@@ -2763,16 +2399,6 @@ function messageOf(error) {
|
|
|
2763
2399
|
return "unprintable error";
|
|
2764
2400
|
}
|
|
2765
2401
|
}
|
|
2766
|
-
/**
|
|
2767
|
-
* Arm the warm-up behind its trigger route. The route flips `requested`
|
|
2768
|
-
* when the dashboard first opens; the cold-path faces compose `launch`.
|
|
2769
|
-
* Either half may land first; the first request after both launches the one
|
|
2770
|
-
* pass, and every later request is a no-op. Returns the deferred injects'
|
|
2771
|
-
* disposer (abort on unload). Every cold read runs through the shared
|
|
2772
|
-
* host-wide gate (coldRead.ts): one read at a time, and the pass stops
|
|
2773
|
-
* early when the gate skips under heap pressure — folded rows persist and
|
|
2774
|
-
* every remaining session keeps its on-demand refold paths.
|
|
2775
|
-
*/
|
|
2776
2402
|
function watchActivityBackfill(ctx, coldReads = makeColdReadGate()) {
|
|
2777
2403
|
let requested = false;
|
|
2778
2404
|
let started = false;
|
|
@@ -2783,8 +2409,7 @@ function watchActivityBackfill(ctx, coldReads = makeColdReadGate()) {
|
|
|
2783
2409
|
launch();
|
|
2784
2410
|
};
|
|
2785
2411
|
const routeFiber = ctx.inject(["connection"], (c) => {
|
|
2786
|
-
const
|
|
2787
|
-
const register = typeof connection?.fetch?.register === "function" ? connection.fetch.register.bind(connection.fetch) : void 0;
|
|
2412
|
+
const register = fetchRouteRegistrar(c.get("connection"));
|
|
2788
2413
|
if (register === void 0) return;
|
|
2789
2414
|
try {
|
|
2790
2415
|
c.effect(() => register({
|
|
@@ -2808,13 +2433,13 @@ function watchActivityBackfill(ctx, coldReads = makeColdReadGate()) {
|
|
|
2808
2433
|
"sessions"
|
|
2809
2434
|
], (raw) => {
|
|
2810
2435
|
const injected = raw;
|
|
2811
|
-
const query = asRecord(injected.sessionQuery);
|
|
2812
|
-
const cache = asRecord(injected.sessionProjectionCache);
|
|
2813
|
-
const persistence = asRecord(injected.sessionPersistence);
|
|
2436
|
+
const query = asRecord$1(injected.sessionQuery);
|
|
2437
|
+
const cache = asRecord$1(injected.sessionProjectionCache);
|
|
2438
|
+
const persistence = asRecord$1(injected.sessionPersistence);
|
|
2814
2439
|
if (query === null || typeof query.listSessions !== "function") return;
|
|
2815
2440
|
if (cache === null || typeof cache.cachedSnapshot !== "function" || typeof cache.coldSnapshot !== "function") return;
|
|
2816
2441
|
if (persistence === null || typeof persistence.open !== "function") return;
|
|
2817
|
-
const sessions = asRecord(injected.sessions);
|
|
2442
|
+
const sessions = asRecord$1(injected.sessions);
|
|
2818
2443
|
const abort = new AbortController();
|
|
2819
2444
|
const run = async () => {
|
|
2820
2445
|
const listed = await query.listSessions(abort.signal);
|
|
@@ -2865,117 +2490,25 @@ function watchActivityBackfill(ctx, coldReads = makeColdReadGate()) {
|
|
|
2865
2490
|
};
|
|
2866
2491
|
}
|
|
2867
2492
|
//#endregion
|
|
2868
|
-
//#region src/host/
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
*
|
|
2873
|
-
* The schema is schemastery so both consumers it must serve accept it: cordis
|
|
2874
|
-
* validates the entry config through the schema's Standard Schema face before
|
|
2875
|
-
* `apply` runs (defaults per field, unknown keys merged through), and the
|
|
2876
|
-
* harness's Config-form generation (dsh 0.1.7+) derives the Plugins page's
|
|
2877
|
-
* live form from this same schema — there the namespace is the entry id
|
|
2878
|
-
* (`dsh-context`) and only the `.volatile()` preference fields are served and
|
|
2879
|
-
* editable. Bounds edits remount the entry (the folds read them at apply);
|
|
2880
|
-
* preference edits commit volatile-only and never remount. On the older lines
|
|
2881
|
-
* the preferences are inert here — their surface is the registered settings
|
|
2882
|
-
* namespace (settings.ts). The `Config` type describes the RESOLVED shape
|
|
2883
|
-
* cordis hands to `apply`; the raw patch values are partial and the schema
|
|
2884
|
-
* fills every default.
|
|
2885
|
-
*/
|
|
2886
|
-
const DEFAULT_BOUNDS = {
|
|
2887
|
-
maxRequestSteps: 1500,
|
|
2888
|
-
maxKeptTurns: 300,
|
|
2889
|
-
maxEvents: 400,
|
|
2890
|
-
maxNodes: 2e3,
|
|
2891
|
-
maxArchiveNodes: 400,
|
|
2892
|
-
maxFileOps: 400
|
|
2893
|
-
};
|
|
2894
|
-
/**
|
|
2895
|
-
* Mark a field live-editable where the harness's schemastery ships the
|
|
2896
|
-
* `.volatile()` modifier (the Config-form generation reads the mark to serve
|
|
2897
|
-
* the field on the Plugins page); a plain field on the older lines whose
|
|
2898
|
-
* schemastery predates the modifier.
|
|
2899
|
-
*/
|
|
2900
|
-
function volatileField(field) {
|
|
2901
|
-
const volatile = field.volatile;
|
|
2902
|
-
return typeof volatile === "function" ? volatile.call(field) : field;
|
|
2493
|
+
//#region src/host/detail.ts
|
|
2494
|
+
const DETAIL_ROUTE = "/api/dsh-context/detail";
|
|
2495
|
+
function reply(value) {
|
|
2496
|
+
return Response.json(value, { headers: { "cache-control": "no-store" } });
|
|
2903
2497
|
}
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2498
|
+
function failure(code, message) {
|
|
2499
|
+
return reply({
|
|
2500
|
+
ok: false,
|
|
2501
|
+
error: {
|
|
2502
|
+
code,
|
|
2503
|
+
message
|
|
2504
|
+
}
|
|
2505
|
+
});
|
|
2907
2506
|
}
|
|
2908
|
-
/** The cordis `Config` validator and the Config-form generation's served schema. */
|
|
2909
|
-
const Config = z$1.object({
|
|
2910
|
-
maxRequestSteps: count(DEFAULT_BOUNDS.maxRequestSteps),
|
|
2911
|
-
maxKeptTurns: count(DEFAULT_BOUNDS.maxKeptTurns),
|
|
2912
|
-
maxEvents: count(DEFAULT_BOUNDS.maxEvents),
|
|
2913
|
-
maxNodes: count(DEFAULT_BOUNDS.maxNodes),
|
|
2914
|
-
maxArchiveNodes: count(DEFAULT_BOUNDS.maxArchiveNodes),
|
|
2915
|
-
maxFileOps: count(DEFAULT_BOUNDS.maxFileOps),
|
|
2916
|
-
defaultPlacement: volatileField(z$1.union([
|
|
2917
|
-
"all",
|
|
2918
|
-
"tab",
|
|
2919
|
-
"sidebar"
|
|
2920
|
-
]).default("all").loose()),
|
|
2921
|
-
defaultGranularity: volatileField(z$1.union(["step", "turn"]).default("step").loose()),
|
|
2922
|
-
defaultTrendMode: volatileField(z$1.union(["total", "delta"]).default("total").loose()),
|
|
2923
|
-
defaultDeltaBase: volatileField(z$1.union(["step", "turn"]).default("step").loose()),
|
|
2924
|
-
defaultToolSort: volatileField(z$1.union([
|
|
2925
|
-
"size",
|
|
2926
|
-
"count",
|
|
2927
|
-
"name"
|
|
2928
|
-
]).default("count").loose()),
|
|
2929
|
-
defaultFileSort: volatileField(z$1.union([
|
|
2930
|
-
"count",
|
|
2931
|
-
"latest",
|
|
2932
|
-
"path"
|
|
2933
|
-
]).default("count").loose()),
|
|
2934
|
-
insightsEntry: volatileField(z$1.union(["show", "hide"]).default("show").loose()),
|
|
2935
|
-
defaultDurationCurve: volatileField(z$1.union(["show", "hide"]).default("show").loose())
|
|
2936
|
-
});
|
|
2937
|
-
/** Resolve the fold's retention bounds (the schema fills every default). */
|
|
2938
|
-
function resolveBounds(config) {
|
|
2939
|
-
const resolved = Config(config ?? {});
|
|
2940
|
-
return {
|
|
2941
|
-
maxRequestSteps: resolved.maxRequestSteps,
|
|
2942
|
-
maxKeptTurns: resolved.maxKeptTurns,
|
|
2943
|
-
maxEvents: resolved.maxEvents,
|
|
2944
|
-
maxNodes: resolved.maxNodes,
|
|
2945
|
-
maxArchiveNodes: resolved.maxArchiveNodes,
|
|
2946
|
-
maxFileOps: resolved.maxFileOps
|
|
2947
|
-
};
|
|
2948
|
-
}
|
|
2949
|
-
//#endregion
|
|
2950
|
-
//#region src/host/detail.ts
|
|
2951
|
-
/** The plugin's detail route, under the authenticated `/api` fence. */
|
|
2952
|
-
const DETAIL_ROUTE = "/api/dsh-context/detail";
|
|
2953
|
-
/** One JSON reply of the detail route. */
|
|
2954
|
-
function reply(value) {
|
|
2955
|
-
return Response.json(value, { headers: { "cache-control": "no-store" } });
|
|
2956
|
-
}
|
|
2957
|
-
/** The typed failure envelope (the same shape the Connection RPC carried). */
|
|
2958
|
-
function failure(code, message) {
|
|
2959
|
-
return reply({
|
|
2960
|
-
ok: false,
|
|
2961
|
-
error: {
|
|
2962
|
-
code,
|
|
2963
|
-
message
|
|
2964
|
-
}
|
|
2965
|
-
});
|
|
2966
|
-
}
|
|
2967
|
-
/**
|
|
2968
|
-
* Serve the detail route whenever the connection and sessions services are
|
|
2969
|
-
* both composed (see the module header for the load-order contract). The
|
|
2970
|
-
* registration rides the injected fiber: either service unloading withdraws
|
|
2971
|
-
* the route and closes the gate.
|
|
2972
|
-
*/
|
|
2973
2507
|
function watchDetailChannel(ctx, bounds, coldReads = makeColdReadGate()) {
|
|
2974
2508
|
const gate = { live: false };
|
|
2975
2509
|
ctx.inject(["connection", "sessions"], (c) => {
|
|
2976
|
-
const connection = c.get("connection");
|
|
2977
2510
|
const sessions = c.get("sessions");
|
|
2978
|
-
const register =
|
|
2511
|
+
const register = fetchRouteRegistrar(c.get("connection"));
|
|
2979
2512
|
const getSession = typeof sessions?.get === "function" ? sessions.get.bind(sessions) : void 0;
|
|
2980
2513
|
if (register === void 0 || getSession === void 0) return;
|
|
2981
2514
|
const projections = ctx.sessionProjections;
|
|
@@ -3046,33 +2579,11 @@ function watchDetailChannel(ctx, bounds, coldReads = makeColdReadGate()) {
|
|
|
3046
2579
|
}
|
|
3047
2580
|
//#endregion
|
|
3048
2581
|
//#region src/shared/version.ts
|
|
3049
|
-
|
|
3050
|
-
* The harness-version gate's shared arithmetic — the supported dsh baseline
|
|
3051
|
-
* and the version compare behind it. Runtime code shared by BOTH halves (the
|
|
3052
|
-
* host probes and gates; the client displays what the wire record carries),
|
|
3053
|
-
* so this module must stay dependency-free.
|
|
3054
|
-
*
|
|
3055
|
-
* The baseline mirrors the support matrix (docs/compatibility.md and the
|
|
3056
|
-
* package's `dsh.compatibility.dshReleases` declaration): the oldest dsh
|
|
3057
|
-
* release this plugin works on. A harness BELOW it gets the fallback units
|
|
3058
|
-
* (host/fallback.ts) instead of the real folds.
|
|
3059
|
-
*/
|
|
3060
|
-
/** The oldest supported dsh release (see the matrix note above). */
|
|
3061
|
-
const BASELINE_DSH_VERSION = "0.1.5-rc.1";
|
|
3062
|
-
/**
|
|
3063
|
-
* Release-channel rank at an equal X.Y.Z: a final release outranks its
|
|
3064
|
-
* release candidates, which outrank betas, which outrank alphas
|
|
3065
|
-
* (正式版 > RC > Beta > Alpha).
|
|
3066
|
-
*/
|
|
2582
|
+
const BASELINE_DSH_VERSION = "0.1.7-rc.2";
|
|
3067
2583
|
function channelRank(channel) {
|
|
3068
2584
|
return channel === "rc" ? 3 : channel === "beta" ? 2 : 1;
|
|
3069
2585
|
}
|
|
3070
2586
|
const RELEASE_RANK = 4;
|
|
3071
|
-
/**
|
|
3072
|
-
* Parse `v?[major].[minor].[patch][-(alpha|beta|rc)[.N]][+build]`, or null
|
|
3073
|
-
* when the string is not that shape. Channels other than alpha/beta/rc
|
|
3074
|
-
* (nightly, dev, …) do not parse — the gate fails open on them.
|
|
3075
|
-
*/
|
|
3076
2587
|
function parseVersion(version) {
|
|
3077
2588
|
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-(alpha|beta|rc)(?:\.(\d+))?)?(?:\+[0-9a-z.-]+)?$/i.exec(version.trim());
|
|
3078
2589
|
if (match === null) return null;
|
|
@@ -3085,10 +2596,6 @@ function parseVersion(version) {
|
|
|
3085
2596
|
serial: match[5] ? Number(match[5]) : 0
|
|
3086
2597
|
};
|
|
3087
2598
|
}
|
|
3088
|
-
/**
|
|
3089
|
-
* Total order over parsed versions: X.Y.Z numerically first, then the
|
|
3090
|
-
* channel rank, then the prerelease serial.
|
|
3091
|
-
*/
|
|
3092
2599
|
function compareParsed(a, b) {
|
|
3093
2600
|
if (a.major !== b.major) return a.major - b.major;
|
|
3094
2601
|
if (a.minor !== b.minor) return a.minor - b.minor;
|
|
@@ -3096,12 +2603,6 @@ function compareParsed(a, b) {
|
|
|
3096
2603
|
if (a.rank !== b.rank) return a.rank - b.rank;
|
|
3097
2604
|
return a.serial - b.serial;
|
|
3098
2605
|
}
|
|
3099
|
-
/**
|
|
3100
|
-
* Whether `version` satisfies the supported baseline. FAIL OPEN by design: a
|
|
3101
|
-
* version that cannot be parsed (a dev/nightly harness build) must not blank
|
|
3102
|
-
* a working deployment, so it passes — the gate trips only on a proven
|
|
3103
|
-
* below-baseline release.
|
|
3104
|
-
*/
|
|
3105
2606
|
function meetsBaseline(version, baseline = BASELINE_DSH_VERSION) {
|
|
3106
2607
|
const v = parseVersion(version);
|
|
3107
2608
|
const b = parseVersion(baseline);
|
|
@@ -3110,46 +2611,7 @@ function meetsBaseline(version, baseline = BASELINE_DSH_VERSION) {
|
|
|
3110
2611
|
}
|
|
3111
2612
|
//#endregion
|
|
3112
2613
|
//#region src/host/headers.ts
|
|
3113
|
-
/**
|
|
3114
|
-
* The `contextHeaders` session projection unit — the request-header EPOCH
|
|
3115
|
-
* METADATA behind the timeline's envelope figures.
|
|
3116
|
-
*
|
|
3117
|
-
* The hot `contextTimeline` unit carries only token prices of the system
|
|
3118
|
-
* prompt and tool schemas; this companion unit keeps the per-epoch METADATA
|
|
3119
|
-
* (epoch seq/time boundaries, per-tool token prices and plugin attribution)
|
|
3120
|
-
* so the Context browser can pick the header epoch in force at any step and
|
|
3121
|
-
* size its sections immediately. The epoch CONTENT (full system prompt text,
|
|
3122
|
-
* full tool JSON schemas) deliberately does NOT ride the projection VALUE:
|
|
3123
|
-
* session projections are served whole in every `session.list` row, control
|
|
3124
|
-
* baseline, push frame, and change notification, so carrying content here
|
|
3125
|
-
* multiplied it by sessions × epochs across every channel. The client
|
|
3126
|
-
* fetches one epoch's `request/header` event on demand — a seq-anchored
|
|
3127
|
-
* history read off the epoch's `seq`, the same targeted read the browser
|
|
3128
|
-
* already uses for message content — and caches it per session (history is
|
|
3129
|
-
* immutable).
|
|
3130
|
-
*
|
|
3131
|
-
* Read-compat over the persisted state (the pinned decision behind keeping
|
|
3132
|
-
* `stateVersion` at 1): the harness serves a cold session's projections from
|
|
3133
|
-
* its CACHED checkpoint rows and has no refresh channel for an idle session
|
|
3134
|
-
* — a version bump invalidates every row and orphans the key until the
|
|
3135
|
-
* session goes live again (the #37 regression). The state therefore still
|
|
3136
|
-
* ACCEPTS the v1 content-bearing record shape, current folds append
|
|
3137
|
-
* metadata-only records alongside any seeded legacy ones, and the view
|
|
3138
|
-
* normalizes BOTH to the metadata-only wire shape (pricing the legacy system
|
|
3139
|
-
* text at read time). Cached v1 rows keep working, new checkpoint writes
|
|
3140
|
-
* shrink as legacy epochs age out of the capped list, and the wire — the
|
|
3141
|
-
* part every delivery channel carries — is metadata-only from day one.
|
|
3142
|
-
*
|
|
3143
|
-
* Same projection contract as the timeline unit: pure init/apply/view,
|
|
3144
|
-
* `Object.is` reference stability for uninteresting events, plain-JSON
|
|
3145
|
-
* bounded state (epoch list capped — see HEADERS_MAX).
|
|
3146
|
-
*/
|
|
3147
|
-
/** Retention cap on header epochs (metadata only; changes are rare; 50 is generous). */
|
|
3148
2614
|
const HEADERS_MAX = 50;
|
|
3149
|
-
/**
|
|
3150
|
-
* The persisted-state schema: the SUPERSET of both record generations, so a
|
|
3151
|
-
* cached v1 row (content-bearing) seeds the fold instead of being discarded.
|
|
3152
|
-
*/
|
|
3153
2615
|
const storedToolSchema = z.object({
|
|
3154
2616
|
name: z.string(),
|
|
3155
2617
|
tokens: z.number().int().nonnegative(),
|
|
@@ -3165,13 +2627,11 @@ const storedEpochSchema = z.object({
|
|
|
3165
2627
|
tools: z.array(storedToolSchema)
|
|
3166
2628
|
}).strict();
|
|
3167
2629
|
const contextHeadersStateSchema = z.object({ headers: z.array(storedEpochSchema) }).strict();
|
|
3168
|
-
/** The wire schema: strict metadata — the shape every delivery channel carries. */
|
|
3169
2630
|
const headerToolWireSchema = z.object({
|
|
3170
2631
|
name: z.string(),
|
|
3171
2632
|
tokens: z.number().int().nonnegative(),
|
|
3172
2633
|
plugin: z.string().optional()
|
|
3173
2634
|
}).strict();
|
|
3174
|
-
/** Exported for the fallback unit (fallback.ts): one wire contract, one schema. */
|
|
3175
2635
|
const contextHeadersSchema = z.object({ headers: z.array(z.object({
|
|
3176
2636
|
seq: z.number(),
|
|
3177
2637
|
time: z.number(),
|
|
@@ -3198,13 +2658,6 @@ function recordOf(event) {
|
|
|
3198
2658
|
})
|
|
3199
2659
|
};
|
|
3200
2660
|
}
|
|
3201
|
-
/**
|
|
3202
|
-
* The context-headers projection unit; registered alongside the timeline unit (host/index.ts); clients read it through
|
|
3203
|
-
* `useProjection('contextHeaders')` and fetch an epoch's full content on demand via the session history (historyPage.ts).
|
|
3204
|
-
* Contract mirror with a REQUIRED `wire` block (see compat.ts).
|
|
3205
|
-
* @param resolve - best-effort tool-to-plugin attribution (see toolSources.ts); fills a missing `plugin` at view time so
|
|
3206
|
-
* epochs folded without attribution still render a tag when the source is known.
|
|
3207
|
-
*/
|
|
3208
2661
|
function createContextHeadersDefinition(resolve) {
|
|
3209
2662
|
const view = (state) => ({ headers: state.headers.map((h) => {
|
|
3210
2663
|
const record = {
|
|
@@ -3244,382 +2697,8 @@ function createContextHeadersDefinition(resolve) {
|
|
|
3244
2697
|
};
|
|
3245
2698
|
}
|
|
3246
2699
|
//#endregion
|
|
3247
|
-
//#region src/host/timeline.ts
|
|
3248
|
-
/**
|
|
3249
|
-
* The `contextTimeline` session projection unit — the plugin's data plane.
|
|
3250
|
-
*
|
|
3251
|
-
* This is the whole Host half after the v0.9 data-path migration: instead of
|
|
3252
|
-
* serving snapshots over a custom `/dsh-context` RPC channel, the plugin
|
|
3253
|
-
* registers one pure projection unit on the harness's
|
|
3254
|
-
* `ctx.sessionProjections` registry. The framework then:
|
|
3255
|
-
* - drives the fold per committed `session/event` (eager, incremental),
|
|
3256
|
-
* - persists the unit state through `ctx.sessionProjectionCache`
|
|
3257
|
-
* (checkpointed rows, cold-read ladder, resume-safe),
|
|
3258
|
-
* - delivers finished values to the browser as a `session/projection` push
|
|
3259
|
-
* frame plus a tail-page baseline, where the Client reads them through
|
|
3260
|
-
* the framework-standard `useProjection('contextTimeline')` seat.
|
|
3261
|
-
*
|
|
3262
|
-
* The unit is pure mathematics (init/apply/view) — it holds no subscriptions
|
|
3263
|
-
* and never touches the client. The wire value is the same Snapshot the UI
|
|
3264
|
-
* has always rendered (shared/types.ts), so the Client renders unchanged.
|
|
3265
|
-
*/
|
|
3266
|
-
/** Validate the wire payload before it leaves the host (strict: no drift). */
|
|
3267
|
-
const surfaceNodeSchema = z.object({
|
|
3268
|
-
seq: z.number().int().nonnegative(),
|
|
3269
|
-
time: z.number().optional(),
|
|
3270
|
-
cat: z.enum([
|
|
3271
|
-
"user",
|
|
3272
|
-
"inject",
|
|
3273
|
-
"skill",
|
|
3274
|
-
"assistant",
|
|
3275
|
-
"tool"
|
|
3276
|
-
]),
|
|
3277
|
-
tokens: z.number().int().nonnegative(),
|
|
3278
|
-
imgs: z.number().int().nonnegative().optional(),
|
|
3279
|
-
gone: z.number().int().nonnegative().optional(),
|
|
3280
|
-
form: z.string().optional(),
|
|
3281
|
-
name: z.string().optional(),
|
|
3282
|
-
text: z.string().optional(),
|
|
3283
|
-
tool: z.string().optional(),
|
|
3284
|
-
err: z.boolean().optional(),
|
|
3285
|
-
skill: z.string().optional(),
|
|
3286
|
-
calls: z.array(z.string()).optional()
|
|
3287
|
-
}).strict();
|
|
3288
|
-
/** One live system-prompt node (shared/types.ts SystemPromptNode). */
|
|
3289
|
-
const systemPromptNodeSchema = z.object({
|
|
3290
|
-
seq: z.number().int().nonnegative(),
|
|
3291
|
-
time: z.number(),
|
|
3292
|
-
tokens: z.number().int().nonnegative()
|
|
3293
|
-
}).strict();
|
|
3294
|
-
const requestRecordSchema = z.object({
|
|
3295
|
-
turn: z.number().optional(),
|
|
3296
|
-
step: z.number().optional(),
|
|
3297
|
-
time: z.number(),
|
|
3298
|
-
seq: z.number(),
|
|
3299
|
-
system: z.number().int().nonnegative(),
|
|
3300
|
-
tools: z.number().int().nonnegative(),
|
|
3301
|
-
user: z.number().int().nonnegative(),
|
|
3302
|
-
inject: z.number().int().nonnegative(),
|
|
3303
|
-
assistant: z.number().int().nonnegative(),
|
|
3304
|
-
tool: z.number().int().nonnegative(),
|
|
3305
|
-
total: z.number().int().nonnegative(),
|
|
3306
|
-
prompt: z.number().int().nonnegative().optional(),
|
|
3307
|
-
/**
|
|
3308
|
-
* Skill-machinery tokens (issue #66). The fold writes it on every record;
|
|
3309
|
-
* optional so rows folded before the category existed still parse.
|
|
3310
|
-
*/
|
|
3311
|
-
skill: z.number().int().nonnegative().optional(),
|
|
3312
|
-
cacheRead: z.number().int().nonnegative().optional(),
|
|
3313
|
-
output: z.number().int().nonnegative().optional(),
|
|
3314
|
-
/**
|
|
3315
|
-
* The request's step active milliseconds (see RequestRecord.activeMs).
|
|
3316
|
-
* Optional so a record whose step never closed (the live tail) still parses.
|
|
3317
|
-
*/
|
|
3318
|
-
activeMs: z.number().nonnegative().optional(),
|
|
3319
|
-
stepCount: z.number().int().positive().optional()
|
|
3320
|
-
}).strict();
|
|
3321
|
-
const contextEventSchema = z.object({
|
|
3322
|
-
seq: z.number(),
|
|
3323
|
-
time: z.number(),
|
|
3324
|
-
kind: z.enum([
|
|
3325
|
-
"compaction",
|
|
3326
|
-
"prune",
|
|
3327
|
-
"inject",
|
|
3328
|
-
"model",
|
|
3329
|
-
"mode"
|
|
3330
|
-
]),
|
|
3331
|
-
form: z.string().optional(),
|
|
3332
|
-
tokens: z.number().optional(),
|
|
3333
|
-
count: z.number().optional(),
|
|
3334
|
-
sub: z.string().optional(),
|
|
3335
|
-
name: z.string().optional(),
|
|
3336
|
-
detail: z.string().optional(),
|
|
3337
|
-
from: z.string().optional(),
|
|
3338
|
-
to: z.string().optional(),
|
|
3339
|
-
fromTurn: z.number().optional(),
|
|
3340
|
-
fromStep: z.number().optional(),
|
|
3341
|
-
turn: z.number().optional(),
|
|
3342
|
-
step: z.number().optional()
|
|
3343
|
-
}).strict();
|
|
3344
|
-
/** The fold-derived file-operation record (shared/types.ts FileOpRecord). */
|
|
3345
|
-
const fileOpSchema = z.object({
|
|
3346
|
-
seq: z.number().int().nonnegative(),
|
|
3347
|
-
path: z.string(),
|
|
3348
|
-
kind: z.enum([
|
|
3349
|
-
"read",
|
|
3350
|
-
"write",
|
|
3351
|
-
"search"
|
|
3352
|
-
]),
|
|
3353
|
-
tool: z.string(),
|
|
3354
|
-
time: z.number().optional(),
|
|
3355
|
-
err: z.boolean(),
|
|
3356
|
-
added: z.number().int().nonnegative(),
|
|
3357
|
-
removed: z.number().int().nonnegative(),
|
|
3358
|
-
detail: z.string().optional(),
|
|
3359
|
-
hits: z.number().int().positive().optional(),
|
|
3360
|
-
read: z.union([z.object({
|
|
3361
|
-
start: z.number().int().positive(),
|
|
3362
|
-
count: z.number().int().nonnegative()
|
|
3363
|
-
}).strict(), z.object({
|
|
3364
|
-
count: z.number().int().positive(),
|
|
3365
|
-
est: z.literal(true)
|
|
3366
|
-
}).strict()]).optional(),
|
|
3367
|
-
parent: z.number().int().nonnegative().optional(),
|
|
3368
|
-
program: z.string().optional(),
|
|
3369
|
-
pattern: z.literal(true).optional(),
|
|
3370
|
-
gone: z.number().int().nonnegative().optional()
|
|
3371
|
-
}).strict();
|
|
3372
|
-
/** One painted span of the timing strip (shared/types.ts TimingSpan). */
|
|
3373
|
-
const timingSpanSchema = z.object({
|
|
3374
|
-
kind: z.enum([
|
|
3375
|
-
"ttft",
|
|
3376
|
-
"reasoning",
|
|
3377
|
-
"text",
|
|
3378
|
-
"toolarg",
|
|
3379
|
-
"tools",
|
|
3380
|
-
"other"
|
|
3381
|
-
]),
|
|
3382
|
-
start: z.number(),
|
|
3383
|
-
end: z.number()
|
|
3384
|
-
}).strict();
|
|
3385
|
-
const currentSchema = z.object({
|
|
3386
|
-
system: z.number().int().nonnegative(),
|
|
3387
|
-
tools: z.number().int().nonnegative(),
|
|
3388
|
-
user: z.number().int().nonnegative(),
|
|
3389
|
-
inject: z.number().int().nonnegative(),
|
|
3390
|
-
skill: z.number().int().nonnegative(),
|
|
3391
|
-
assistant: z.number().int().nonnegative(),
|
|
3392
|
-
tool: z.number().int().nonnegative(),
|
|
3393
|
-
total: z.number().int().nonnegative()
|
|
3394
|
-
}).strict();
|
|
3395
|
-
const costBucketsSchema = z.object({
|
|
3396
|
-
uncached: z.number().int().nonnegative(),
|
|
3397
|
-
cacheRead: z.number().int().nonnegative(),
|
|
3398
|
-
cacheWrite: z.number().int().nonnegative(),
|
|
3399
|
-
output: z.number().int().nonnegative()
|
|
3400
|
-
}).strict();
|
|
3401
|
-
const costModelSchema = z.object({
|
|
3402
|
-
peak: costBucketsSchema.optional(),
|
|
3403
|
-
off: costBucketsSchema.optional()
|
|
3404
|
-
}).strict();
|
|
3405
|
-
const costModelsSchema = z.record(z.string(), costModelSchema);
|
|
3406
|
-
const costUsageSchema = z.record(z.string(), costModelsSchema);
|
|
3407
|
-
const toolTimingSchema = z.object({
|
|
3408
|
-
calls: z.number().int().nonnegative(),
|
|
3409
|
-
ms: z.number().nonnegative()
|
|
3410
|
-
}).strict();
|
|
3411
|
-
const timingTotalsSchema = z.object({
|
|
3412
|
-
wallMs: z.number().nonnegative(),
|
|
3413
|
-
ttftMs: z.number().nonnegative(),
|
|
3414
|
-
genMs: z.number().nonnegative(),
|
|
3415
|
-
reasoningMs: z.number().nonnegative().optional(),
|
|
3416
|
-
reasoningBlocks: z.number().int().nonnegative().optional(),
|
|
3417
|
-
textMs: z.number().nonnegative().optional(),
|
|
3418
|
-
textBlocks: z.number().int().nonnegative().optional(),
|
|
3419
|
-
toolArgMs: z.number().nonnegative().optional(),
|
|
3420
|
-
toolArgBlocks: z.number().int().nonnegative().optional(),
|
|
3421
|
-
speedTokens: z.number().nonnegative().optional(),
|
|
3422
|
-
speedMs: z.number().nonnegative().optional(),
|
|
3423
|
-
calls: z.number().int().nonnegative(),
|
|
3424
|
-
toolsMs: z.number().nonnegative(),
|
|
3425
|
-
toolCalls: z.number().int().nonnegative(),
|
|
3426
|
-
tools: z.record(z.string(), toolTimingSchema)
|
|
3427
|
-
}).strict();
|
|
3428
|
-
/** The baseline-gate record the fallback unit serves (see fallback.ts). */
|
|
3429
|
-
const unsupportedSchema = z.object({
|
|
3430
|
-
current: z.string(),
|
|
3431
|
-
minimum: z.string()
|
|
3432
|
-
}).strict();
|
|
3433
|
-
/** The stats board's precomputed count figures (the split head — see Snapshot.counts). */
|
|
3434
|
-
const countsSchema = z.object({
|
|
3435
|
-
turns: z.number().int().nonnegative(),
|
|
3436
|
-
steps: z.number().int().nonnegative(),
|
|
3437
|
-
injects: z.number().int().nonnegative(),
|
|
3438
|
-
compactions: z.number().int().nonnegative(),
|
|
3439
|
-
prunes: z.number().int().nonnegative()
|
|
3440
|
-
}).strict();
|
|
3441
|
-
/** The newest retained request's billing summary (the split head's headline anchor). */
|
|
3442
|
-
const lastSchema = z.object({
|
|
3443
|
-
seq: z.number(),
|
|
3444
|
-
total: z.number().int().nonnegative(),
|
|
3445
|
-
prompt: z.number().int().nonnegative().optional()
|
|
3446
|
-
}).strict();
|
|
3447
|
-
/**
|
|
3448
|
-
* One wire contract for both generations: the SPLIT head (envelope scalars +
|
|
3449
|
-
* counts/last/detailRev; the heavy collections stay absent — they ride the
|
|
3450
|
-
* on-demand detail channel, host/detail.ts) and the INLINE value
|
|
3451
|
-
* (channel-less hosts and the fallback unit carry the collections in place).
|
|
3452
|
-
* The collections are therefore optional on the schema; the split marker is
|
|
3453
|
-
* `detailRev` (present ⟺ split).
|
|
3454
|
-
*/
|
|
3455
|
-
const contextTimelineSchema = z.object({
|
|
3456
|
-
ok: z.literal(true),
|
|
3457
|
-
unsupported: unsupportedSchema.optional(),
|
|
3458
|
-
model: z.string().optional(),
|
|
3459
|
-
provider: z.string().optional(),
|
|
3460
|
-
contextWindow: z.number().optional(),
|
|
3461
|
-
current: currentSchema,
|
|
3462
|
-
images: z.number().int().nonnegative().optional(),
|
|
3463
|
-
toolCalls: z.number().int().nonnegative().optional(),
|
|
3464
|
-
humanInputs: z.number().int().nonnegative().optional(),
|
|
3465
|
-
lastUser: z.string().optional(),
|
|
3466
|
-
counts: countsSchema.optional(),
|
|
3467
|
-
last: lastSchema.optional(),
|
|
3468
|
-
detailRev: z.number().int().nonnegative().optional(),
|
|
3469
|
-
requests: z.array(requestRecordSchema).optional(),
|
|
3470
|
-
events: z.array(contextEventSchema).optional(),
|
|
3471
|
-
cost: costUsageSchema.optional(),
|
|
3472
|
-
timing: timingTotalsSchema.optional(),
|
|
3473
|
-
systems: z.array(systemPromptNodeSchema).optional(),
|
|
3474
|
-
nodes: z.array(surfaceNodeSchema).optional(),
|
|
3475
|
-
droppedNodes: z.number().int().nonnegative().optional(),
|
|
3476
|
-
archive: z.array(surfaceNodeSchema).optional(),
|
|
3477
|
-
surfaceFloor: z.number().int().nonnegative().optional(),
|
|
3478
|
-
archiveFloor: z.number().int().nonnegative().optional(),
|
|
3479
|
-
fileOps: z.array(fileOpSchema).optional(),
|
|
3480
|
-
fileOpsFloor: z.number().int().nonnegative().optional(),
|
|
3481
|
-
spans: z.array(timingSpanSchema).optional()
|
|
3482
|
-
}).strict();
|
|
3483
|
-
/**
|
|
3484
|
-
* The persisted fold-state schema (the registry's `stateSchema`
|
|
3485
|
-
* contract). Validates the plain-JSON `TimelineState` before a checkpoint
|
|
3486
|
-
* row seeds a fold — the same shape guarantee the projection cache's
|
|
3487
|
-
* plain-JSON precondition already enforces at write time.
|
|
3488
|
-
*/
|
|
3489
|
-
const timelineStateSchema = z.object({
|
|
3490
|
-
surface: z.array(surfaceNodeSchema),
|
|
3491
|
-
sums: z.object({
|
|
3492
|
-
user: z.number().int().nonnegative(),
|
|
3493
|
-
inject: z.number().int().nonnegative(),
|
|
3494
|
-
skill: z.number().int().nonnegative(),
|
|
3495
|
-
assistant: z.number().int().nonnegative(),
|
|
3496
|
-
tool: z.number().int().nonnegative()
|
|
3497
|
-
}).strict(),
|
|
3498
|
-
systemTokens: z.number().int().nonnegative(),
|
|
3499
|
-
systems: z.array(systemPromptNodeSchema).optional(),
|
|
3500
|
-
toolsTokens: z.number().int().nonnegative(),
|
|
3501
|
-
model: z.string().optional(),
|
|
3502
|
-
provider: z.string().optional(),
|
|
3503
|
-
lastModel: z.string().optional(),
|
|
3504
|
-
contextWindow: z.number().optional(),
|
|
3505
|
-
requests: z.array(requestRecordSchema),
|
|
3506
|
-
turnRuns: z.number().int().nonnegative().optional(),
|
|
3507
|
-
events: z.array(contextEventSchema),
|
|
3508
|
-
archived: z.array(surfaceNodeSchema),
|
|
3509
|
-
cost: costUsageSchema.optional(),
|
|
3510
|
-
archiveFloor: z.number().optional(),
|
|
3511
|
-
timing: timingTotalsSchema.optional(),
|
|
3512
|
-
humanInputs: z.number().int().nonnegative().optional(),
|
|
3513
|
-
lastUser: z.string().optional(),
|
|
3514
|
-
stepStart: z.object({
|
|
3515
|
-
time: z.number(),
|
|
3516
|
-
firstToken: z.number().optional()
|
|
3517
|
-
}).strict().optional(),
|
|
3518
|
-
stepWaits: z.array(z.object({
|
|
3519
|
-
start: z.number(),
|
|
3520
|
-
end: z.number()
|
|
3521
|
-
}).strict()).optional(),
|
|
3522
|
-
stepApprovals: z.record(z.string(), z.number()).optional(),
|
|
3523
|
-
stepRequestSeq: z.number().optional(),
|
|
3524
|
-
callNames: z.record(z.string(), z.object({
|
|
3525
|
-
name: z.string(),
|
|
3526
|
-
start: z.number(),
|
|
3527
|
-
argsRaw: z.string().optional()
|
|
3528
|
-
}).strict()),
|
|
3529
|
-
pendingShadowedSeqs: z.array(z.number()).optional(),
|
|
3530
|
-
pendingShadowEventSeq: z.number().optional(),
|
|
3531
|
-
detailRev: z.number().int().nonnegative().optional(),
|
|
3532
|
-
fileOps: z.array(fileOpSchema),
|
|
3533
|
-
fileOpsFloor: z.number().int().nonnegative().optional(),
|
|
3534
|
-
spans: z.array(timingSpanSchema),
|
|
3535
|
-
stepSpans: z.array(timingSpanSchema).optional(),
|
|
3536
|
-
pendingCodeOps: z.record(z.string(), z.array(fileOpSchema)).optional()
|
|
3537
|
-
});
|
|
3538
|
-
/**
|
|
3539
|
-
* The context-timeline projection unit, created per plugin instance with its
|
|
3540
|
-
* config-resolved retention bounds (config.ts), and registered on
|
|
3541
|
-
* `ctx.sessionProjections`. Registry lifecycle notes (mirrored from the
|
|
3542
|
-
* harness contract): registration is an effect on the caller's fiber — an
|
|
3543
|
-
* unloaded Host half removes the key, and clients read it as capability
|
|
3544
|
-
* absence. `stateVersion` must be bumped whenever the persisted state shape
|
|
3545
|
-
* or fold semantics change (invalidation of cached rows); config-only
|
|
3546
|
-
* changes never require it (bounds tune retention, not state shape).
|
|
3547
|
-
*
|
|
3548
|
-
* The definition carries the session-projection contract served by every
|
|
3549
|
-
* supported harness (see compat.ts): `stateSchema` + a REQUIRED `wire` block.
|
|
3550
|
-
* (The return type is the mirrored contract, not the installed dts
|
|
3551
|
-
* `ProjectionDefinition`: the registry's wired-register overload demands
|
|
3552
|
-
* `wire` PRESENT, which the dts's optional `wire?` fails.) Without the
|
|
3553
|
-
* `wire` block the registry treats the unit as host-only and never delivers
|
|
3554
|
-
* `contextTimeline` to the browser (the Context tab would stay on its
|
|
3555
|
-
* loading screen forever).
|
|
3556
|
-
*
|
|
3557
|
-
* `slim` selects the wire generation PER SERVE (a liveness probe, not a
|
|
3558
|
-
* fixed flag): while the on-demand detail channel is live (host/detail.ts),
|
|
3559
|
-
* the wire value is the SLIM head (buildTimelineHead) — the heavy
|
|
3560
|
-
* collections no longer ride every session.list row, control baseline,
|
|
3561
|
-
* follow snapshot, and push frame. Before the channel arms (the connection
|
|
3562
|
-
* service may activate after this plugin) or on a deployment whose
|
|
3563
|
-
* connection/sessions services never compose, the unit serves the INLINE
|
|
3564
|
-
* value so the tab keeps working end to end. Both generations validate
|
|
3565
|
-
* against the same schema (the collections are optional on it), and both
|
|
3566
|
-
* fold the SAME state — the split is view-only, so no `stateVersion` bump
|
|
3567
|
-
* and no cached-row invalidation comes with it (the `detailRev` state field
|
|
3568
|
-
* is additive-optional: older rows restore without it and read as revision
|
|
3569
|
-
* 0).
|
|
3570
|
-
*/
|
|
3571
|
-
function createContextTimelineDefinition(config, slim) {
|
|
3572
|
-
const bounds = resolveBounds(config);
|
|
3573
|
-
const view = (state) => slim() ? buildTimelineHead(state) : buildTimelineView(state, bounds);
|
|
3574
|
-
return {
|
|
3575
|
-
key: "contextTimeline",
|
|
3576
|
-
stateSchema: timelineStateSchema,
|
|
3577
|
-
wire: {
|
|
3578
|
-
viewSchema: contextTimelineSchema,
|
|
3579
|
-
view
|
|
3580
|
-
},
|
|
3581
|
-
init: () => createTimelineState(),
|
|
3582
|
-
apply: (state, event) => applyTimeline(state, event, bounds),
|
|
3583
|
-
stateVersion: 26
|
|
3584
|
-
};
|
|
3585
|
-
}
|
|
3586
|
-
//#endregion
|
|
3587
2700
|
//#region src/host/fallback.ts
|
|
3588
|
-
/**
|
|
3589
|
-
* The baseline gate's fallback projection units (see host/index.ts).
|
|
3590
|
-
*
|
|
3591
|
-
* On a harness below the supported baseline (shared/version.ts) the plugin
|
|
3592
|
-
* registers these INSTEAD of the real folds: the log's event shapes on such
|
|
3593
|
-
* a harness are outside the compat matrix, so nothing is parsed at all —
|
|
3594
|
-
* `apply` is the identity over an opaque empty state, and `view` serves a
|
|
3595
|
-
* fixed value whose timeline snapshot carries the gate record (`unsupported`:
|
|
3596
|
-
* the detected harness version and the baseline). The client's cards render
|
|
3597
|
-
* the blank data and its gate modal urges the upgrade; the registry pipeline
|
|
3598
|
-
* (fold, cache, push feed) keeps working end to end, so nothing hangs on a
|
|
3599
|
-
* loading screen.
|
|
3600
|
-
*
|
|
3601
|
-
* The definition carries BOTH registry contract generations: the modern
|
|
3602
|
-
* `stateSchema` + `wire` block (dsh 0.1.1-rc.1+, the realistic below-baseline
|
|
3603
|
-
* case) and the pre-0.1.1 top-level `schema` + `view` aliases that line's
|
|
3604
|
-
* registry reads instead. Each registry ignores the other generation's
|
|
3605
|
-
* fields, so one definition serves the gate on every harness that can
|
|
3606
|
-
* deliver projections to clients at all.
|
|
3607
|
-
*
|
|
3608
|
-
* `stateVersion` is pinned at 1. Downgrade/upgrade cache choreography: a
|
|
3609
|
-
* downgrade to a gated harness refolds the timeline key from scratch (no
|
|
3610
|
-
* ver-1 rows exist for it) and seeds the headers key from the real unit's
|
|
3611
|
-
* ver-1 rows — stripped to the empty state, which the gate never reads. An
|
|
3612
|
-
* upgrade back discards the fallback's ver-1 timeline rows and REJECTS its
|
|
3613
|
-
* ver-1 headers rows at the real unit's stricter state schema, refolding
|
|
3614
|
-
* both from the log. Nothing stale survives in either direction.
|
|
3615
|
-
*/
|
|
3616
|
-
/**
|
|
3617
|
-
* The opaque fold state: the gate folds nothing, so any cached row seeds it
|
|
3618
|
-
* (strip-mode object — never a discard, never a throw), and the plain-JSON
|
|
3619
|
-
* cache-write gate trivially holds.
|
|
3620
|
-
*/
|
|
3621
2701
|
const fallbackStateSchema = z.object({});
|
|
3622
|
-
/** One gate unit: identity fold over the opaque state, constant view, both contract generations. */
|
|
3623
2702
|
function fallbackDefinition(key, wireSchema, value) {
|
|
3624
2703
|
const view = () => value;
|
|
3625
2704
|
return {
|
|
@@ -3636,10 +2715,6 @@ function fallbackDefinition(key, wireSchema, value) {
|
|
|
3636
2715
|
stateVersion: 1
|
|
3637
2716
|
};
|
|
3638
2717
|
}
|
|
3639
|
-
/**
|
|
3640
|
-
* The fallback `contextTimeline` unit: serves the fixed zeroed snapshot
|
|
3641
|
-
* naming the detected harness `current` version against the baseline.
|
|
3642
|
-
*/
|
|
3643
2718
|
function createFallbackTimelineDefinition(current) {
|
|
3644
2719
|
return fallbackDefinition("contextTimeline", contextTimelineSchema, {
|
|
3645
2720
|
ok: true,
|
|
@@ -3664,103 +2739,20 @@ function createFallbackTimelineDefinition(current) {
|
|
|
3664
2739
|
archive: []
|
|
3665
2740
|
});
|
|
3666
2741
|
}
|
|
3667
|
-
/**
|
|
3668
|
-
* The fallback `contextHeaders` unit: an empty epoch list — the browser's
|
|
3669
|
-
* header sections degrade to their metadata-free rendering.
|
|
3670
|
-
*/
|
|
3671
2742
|
function createFallbackHeadersDefinition() {
|
|
3672
2743
|
return fallbackDefinition("contextHeaders", contextHeadersSchema, { headers: [] });
|
|
3673
2744
|
}
|
|
3674
|
-
/**
|
|
3675
|
-
* The fallback `contextActivity` unit: an empty ledger — the overview's
|
|
3676
|
-
* heatmap renders its empty note instead of waiting on a key that the gate
|
|
3677
|
-
* would never deliver.
|
|
3678
|
-
*/
|
|
3679
2745
|
function createFallbackActivityDefinition() {
|
|
3680
2746
|
return fallbackDefinition("contextActivity", contextActivitySchema, { days: {} });
|
|
3681
2747
|
}
|
|
3682
2748
|
//#endregion
|
|
3683
|
-
//#region src/host/settings.ts
|
|
3684
|
-
/** The namespace is the join key between the Host registration and the browser card. */
|
|
3685
|
-
const SETTINGS_NAMESPACE = "dsh-context";
|
|
3686
|
-
/** Section schema: also the wire envelope the browser scope validates against. */
|
|
3687
|
-
const SettingsSchema = z$1.object({
|
|
3688
|
-
defaultPlacement: z$1.union([
|
|
3689
|
-
"all",
|
|
3690
|
-
"tab",
|
|
3691
|
-
"sidebar"
|
|
3692
|
-
]).default("all").loose(),
|
|
3693
|
-
defaultGranularity: z$1.union(["step", "turn"]).default("step"),
|
|
3694
|
-
defaultTrendMode: z$1.union(["total", "delta"]).default("total").loose(),
|
|
3695
|
-
defaultDeltaBase: z$1.union(["step", "turn"]).default("step").loose(),
|
|
3696
|
-
defaultToolSort: z$1.union([
|
|
3697
|
-
"size",
|
|
3698
|
-
"count",
|
|
3699
|
-
"name"
|
|
3700
|
-
]).default("count").loose(),
|
|
3701
|
-
defaultFileSort: z$1.union([
|
|
3702
|
-
"count",
|
|
3703
|
-
"latest",
|
|
3704
|
-
"path"
|
|
3705
|
-
]).default("count").loose(),
|
|
3706
|
-
insightsEntry: z$1.union(["show", "hide"]).default("show").loose(),
|
|
3707
|
-
defaultDurationCurve: z$1.union(["show", "hide"]).default("show").loose()
|
|
3708
|
-
});
|
|
3709
|
-
/** Serve the namespace while a settings provider with the register face is composed; inert otherwise. */
|
|
3710
|
-
function installSettings(ctx) {
|
|
3711
|
-
ctx.inject(["settings"], (sctx) => {
|
|
3712
|
-
const service = sctx.settings;
|
|
3713
|
-
if (typeof service.register !== "function") return;
|
|
3714
|
-
service.register(SETTINGS_NAMESPACE, SettingsSchema);
|
|
3715
|
-
});
|
|
3716
|
-
}
|
|
3717
|
-
//#endregion
|
|
3718
2749
|
//#region src/host/stepIdentity.ts
|
|
3719
|
-
/**
|
|
3720
|
-
* Step-boundary message identity guard (issue #51 compatibility).
|
|
3721
|
-
*
|
|
3722
|
-
* The harness refuses to LOAD any session whose durable log carries a
|
|
3723
|
-
* `user/message` event whose message lacks a non-empty string `id`
|
|
3724
|
-
* (`assertMessageEventShape`) — while the runtime append path runs no such
|
|
3725
|
-
* check. One unidentified message therefore persists silently and permanently
|
|
3726
|
-
* bricks the session at its next load (`... failed validation: session event
|
|
3727
|
-
* at seq N lacks an identified message`). The observed archive held a wrap-up
|
|
3728
|
-
* notice whose `id` (and `summary`) had been stripped upstream of the append —
|
|
3729
|
-
* a shape no shipped harness producer emits, i.e. a delivery-boundary rebuild
|
|
3730
|
-
* outside the audited paths of both this plugin and the harness.
|
|
3731
|
-
*
|
|
3732
|
-
* This guard hardens the durability boundary from the plugin side. The agent
|
|
3733
|
-
* loop appends every step-boundary `user/message` from the `agent/pre-step`
|
|
3734
|
-
* decision's `messages` — the one seam all claimed inbox input (prompts,
|
|
3735
|
-
* steering, tool-deferred context) flows through before it persists. A
|
|
3736
|
-
* prepended listener sits OUTERMOST in that waterfall, so after `next()`
|
|
3737
|
-
* resolves it sees the final message list: any message that would persist
|
|
3738
|
-
* unidentified gets a fresh id; everything else passes by reference, and the
|
|
3739
|
-
* decision object is copied only when a mint happened.
|
|
3740
|
-
*
|
|
3741
|
-
* Fail-open by contract: a hostile entry (one that throws on property access),
|
|
3742
|
-
* a missing list, or any unexpected shape leaves the decision verbatim — a
|
|
3743
|
-
* guard must never break the turn it protects. Minted ids carry the `dshctx-`
|
|
3744
|
-
* prefix so a backfilled message stays identifiable in the wild.
|
|
3745
|
-
*/
|
|
3746
|
-
/** Prefix marking an id this guard minted (forensically distinguishable). */
|
|
3747
2750
|
const MINTED_ID_PREFIX = "dshctx-";
|
|
3748
|
-
/**
|
|
3749
|
-
* Whether the message fails the harness's restore-time identity check (a
|
|
3750
|
-
* non-empty string id). Non-object entries are unfixable (an id needs a
|
|
3751
|
-
* container) and stay verbatim.
|
|
3752
|
-
*/
|
|
3753
2751
|
function lacksId(message) {
|
|
3754
2752
|
if (typeof message !== "object" || message === null) return false;
|
|
3755
2753
|
const id = message.id;
|
|
3756
2754
|
return typeof id !== "string" || id === "";
|
|
3757
2755
|
}
|
|
3758
|
-
/**
|
|
3759
|
-
* Mint ids for the messages that would persist unidentified.
|
|
3760
|
-
* @param messages - the decision's message list, in append order.
|
|
3761
|
-
* @returns the rewritten list (untouched entries by reference), or undefined
|
|
3762
|
-
* when every entry already carries an identity.
|
|
3763
|
-
*/
|
|
3764
2756
|
function identifiedMessages(messages) {
|
|
3765
2757
|
let copy;
|
|
3766
2758
|
for (const [index, message] of messages.entries()) {
|
|
@@ -3776,7 +2768,6 @@ function identifiedMessages(messages) {
|
|
|
3776
2768
|
}
|
|
3777
2769
|
return copy;
|
|
3778
2770
|
}
|
|
3779
|
-
/** Arm the prepended `agent/pre-step` guard on the plugin's context. */
|
|
3780
2771
|
function watchStepIdentity(ctx) {
|
|
3781
2772
|
ctx.on("agent/pre-step", async (_input, next) => {
|
|
3782
2773
|
const decision = await next();
|
|
@@ -3793,80 +2784,155 @@ function watchStepIdentity(ctx) {
|
|
|
3793
2784
|
}, { prepend: true });
|
|
3794
2785
|
}
|
|
3795
2786
|
//#endregion
|
|
2787
|
+
//#region src/host/skills.ts
|
|
2788
|
+
const SKILLS_ROUTE = "/api/dsh-context/skills";
|
|
2789
|
+
const MAX_SKILLS = 500;
|
|
2790
|
+
function asRecord(value) {
|
|
2791
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2792
|
+
}
|
|
2793
|
+
function stringOf(value) {
|
|
2794
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
2795
|
+
}
|
|
2796
|
+
async function disposeOf(resource) {
|
|
2797
|
+
if (resource === null || typeof resource !== "object" && typeof resource !== "function") return;
|
|
2798
|
+
try {
|
|
2799
|
+
const asyncDispose = resource[Symbol.asyncDispose];
|
|
2800
|
+
if (typeof asyncDispose === "function") {
|
|
2801
|
+
await asyncDispose.call(resource);
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
const syncDispose = resource[Symbol.dispose];
|
|
2805
|
+
if (typeof syncDispose === "function") syncDispose.call(resource);
|
|
2806
|
+
} catch {}
|
|
2807
|
+
}
|
|
2808
|
+
function skillInfoOf(value) {
|
|
2809
|
+
const record = asRecord(value);
|
|
2810
|
+
if (record === null || typeof record.name !== "string" || record.name === "") return null;
|
|
2811
|
+
return {
|
|
2812
|
+
name: record.name,
|
|
2813
|
+
description: typeof record.description === "string" ? record.description : "",
|
|
2814
|
+
...typeof record.path === "string" && record.path !== "" ? { path: record.path } : {},
|
|
2815
|
+
...typeof record.source === "string" && record.source !== "" ? { source: record.source } : {}
|
|
2816
|
+
};
|
|
2817
|
+
}
|
|
2818
|
+
async function sessionViewOf(ctx, sessionId) {
|
|
2819
|
+
if (sessionId === void 0) return {};
|
|
2820
|
+
const query = asRecord(ctx.get("sessionQuery"));
|
|
2821
|
+
if (typeof query?.observeSession !== "function") return {};
|
|
2822
|
+
let observation;
|
|
2823
|
+
try {
|
|
2824
|
+
observation = await query.observeSession.call(query, sessionId);
|
|
2825
|
+
} catch {
|
|
2826
|
+
return {};
|
|
2827
|
+
}
|
|
2828
|
+
try {
|
|
2829
|
+
const obs = asRecord(observation);
|
|
2830
|
+
const cwd = stringOf(asRecord(obs?.header)?.cwd);
|
|
2831
|
+
const agentPreset = stringOf(asRecord(asRecord(obs?.projections)?.values)?.agentPreset);
|
|
2832
|
+
return {
|
|
2833
|
+
...cwd === void 0 ? {} : { cwd },
|
|
2834
|
+
...agentPreset === void 0 ? {} : { agentPreset }
|
|
2835
|
+
};
|
|
2836
|
+
} finally {
|
|
2837
|
+
await disposeOf(observation);
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
async function resolveRegistry(ctx, sessionId, agentPreset) {
|
|
2841
|
+
const presets = asRecord(ctx.get("agentPresets"));
|
|
2842
|
+
let live;
|
|
2843
|
+
if (sessionId !== void 0) {
|
|
2844
|
+
const agents = asRecord(ctx.get("agents"));
|
|
2845
|
+
if (typeof agents?.get === "function") try {
|
|
2846
|
+
live = agents.get.call(agents, sessionId);
|
|
2847
|
+
} catch {
|
|
2848
|
+
live = void 0;
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
if (live !== void 0 && live !== null && typeof presets?.serviceFor === "function") {
|
|
2852
|
+
const scoped = asRecord(presets.serviceFor.call(presets, live, "skills"));
|
|
2853
|
+
if (scoped !== null && typeof scoped.list === "function") return {
|
|
2854
|
+
registry: scoped,
|
|
2855
|
+
scope: live,
|
|
2856
|
+
lease: void 0
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
const registry = asRecord(ctx.get("skills"));
|
|
2860
|
+
if (registry === null || typeof registry.list !== "function") return null;
|
|
2861
|
+
let scope;
|
|
2862
|
+
let lease;
|
|
2863
|
+
if (typeof presets?.acquireScope === "function") try {
|
|
2864
|
+
lease = await presets.acquireScope.call(presets, agentPreset);
|
|
2865
|
+
scope = asRecord(lease)?.key;
|
|
2866
|
+
} catch {
|
|
2867
|
+
lease = void 0;
|
|
2868
|
+
}
|
|
2869
|
+
return {
|
|
2870
|
+
registry,
|
|
2871
|
+
scope,
|
|
2872
|
+
lease
|
|
2873
|
+
};
|
|
2874
|
+
}
|
|
2875
|
+
function watchSkillCatalog(ctx) {
|
|
2876
|
+
ctx.inject(["connection"], (c) => {
|
|
2877
|
+
const register = fetchRouteRegistrar(c.get("connection"));
|
|
2878
|
+
if (register === void 0) return;
|
|
2879
|
+
const handler = async (request) => {
|
|
2880
|
+
let value = null;
|
|
2881
|
+
let lease;
|
|
2882
|
+
try {
|
|
2883
|
+
const body = asRecord(await request.json());
|
|
2884
|
+
const sessionId = stringOf(body?.sessionId);
|
|
2885
|
+
const view = await sessionViewOf(ctx, sessionId);
|
|
2886
|
+
const cwd = view.cwd ?? stringOf(body?.cwd);
|
|
2887
|
+
const resolved = await resolveRegistry(ctx, sessionId, view.agentPreset);
|
|
2888
|
+
if (resolved !== null) {
|
|
2889
|
+
lease = resolved.lease;
|
|
2890
|
+
const listed = await resolved.registry.list({
|
|
2891
|
+
...cwd === void 0 ? {} : { cwd },
|
|
2892
|
+
...resolved.scope === void 0 ? {} : { scope: resolved.scope }
|
|
2893
|
+
});
|
|
2894
|
+
const rows = Array.isArray(listed) ? listed : [];
|
|
2895
|
+
const skills = [];
|
|
2896
|
+
for (const row of rows) {
|
|
2897
|
+
const info = skillInfoOf(row);
|
|
2898
|
+
if (info !== null) skills.push(info);
|
|
2899
|
+
if (skills.length >= MAX_SKILLS) break;
|
|
2900
|
+
}
|
|
2901
|
+
value = { skills };
|
|
2902
|
+
}
|
|
2903
|
+
} catch {} finally {
|
|
2904
|
+
await disposeOf(lease);
|
|
2905
|
+
}
|
|
2906
|
+
return Response.json({
|
|
2907
|
+
ok: true,
|
|
2908
|
+
value
|
|
2909
|
+
}, { headers: { "cache-control": "no-store" } });
|
|
2910
|
+
};
|
|
2911
|
+
try {
|
|
2912
|
+
c.effect(() => register({
|
|
2913
|
+
path: SKILLS_ROUTE,
|
|
2914
|
+
methods: ["POST"],
|
|
2915
|
+
requestBody: "buffered",
|
|
2916
|
+
fetch: handler
|
|
2917
|
+
}), "dsh-context: skills route");
|
|
2918
|
+
} catch {
|
|
2919
|
+
return;
|
|
2920
|
+
}
|
|
2921
|
+
});
|
|
2922
|
+
}
|
|
2923
|
+
//#endregion
|
|
3796
2924
|
//#region src/host/version.ts
|
|
3797
|
-
/**
|
|
3798
|
-
* Runtime harness-version probe behind the baseline gate (host/index.ts).
|
|
3799
|
-
*
|
|
3800
|
-
* The probe never imports a harness package for its version; it reads
|
|
3801
|
-
* manifests through Node resolution from two anchors, in order:
|
|
3802
|
-
*
|
|
3803
|
-
* 1. This module's own ESM resolution of the library packages the Host half
|
|
3804
|
-
* imports at runtime. That is the very pipeline the process resolves those
|
|
3805
|
-
* imports through, so an embedding shell that redirects plugin imports —
|
|
3806
|
-
* the packaged Desktop's ASAR resolver bridge does exactly this — redirects
|
|
3807
|
-
* the probe identically, and the answer names the module instances the
|
|
3808
|
-
* harness is actually running. A witness that lands inside this package's
|
|
3809
|
-
* own tree is the plugin's dependency closure (a `link:`-installed dev
|
|
3810
|
-
* checkout's pinned devDependencies), never the harness, so it is discarded
|
|
3811
|
-
* for the home anchor.
|
|
3812
|
-
* 2. The harness home's healed `profiles/node_modules` mirror (located via
|
|
3813
|
-
* the app-boot `dshHomePath` service). It names the installation that last
|
|
3814
|
-
* healed it: authoritative for a dev checkout, whose own tree holds pinned
|
|
3815
|
-
* devDependencies, but only as fresh as the last CLI profile boot. A
|
|
3816
|
-
* packaged Desktop never heals it, so a stale global-CLI mirror can outlive
|
|
3817
|
-
* the CLI and misname the running harness — that is why it answers only
|
|
3818
|
-
* when the running anchor has nothing it can trust.
|
|
3819
|
-
*
|
|
3820
|
-
* The `@deepseek-ai/dsh` CLI package is probed ONLY through the home anchor:
|
|
3821
|
-
* the plugin never imports it, so a hit from the running anchor can only be an
|
|
3822
|
-
* ambient install above the plugin's tree (e.g. a global copy under
|
|
3823
|
-
* ~/node_modules) — not necessarily the RUNNING harness.
|
|
3824
|
-
*
|
|
3825
|
-
* Every step is guarded: any failure (absent service, unresolvable package,
|
|
3826
|
-
* non-file URL, unreadable/invalid manifest, non-string version) degrades to
|
|
3827
|
-
* `undefined`, and the gate treats an unknown version as SATISFIED — a probe
|
|
3828
|
-
* misfire must never blank a working deployment.
|
|
3829
|
-
*/
|
|
3830
|
-
/**
|
|
3831
|
-
* Library packages whose manifest version IS the harness release version (the
|
|
3832
|
-
* dsh monorepo versions every package in lockstep). The home anchor probes
|
|
3833
|
-
* them after the CLI package; the running anchor probes the two first- and
|
|
3834
|
-
* second-ordered.
|
|
3835
|
-
*/
|
|
3836
2925
|
const LIBRARY_PROBE_PACKAGES = ["@deepseek-ai/dsh-session-projection", "@deepseek-ai/dsh-session"];
|
|
3837
|
-
/**
|
|
3838
|
-
* Running-anchor probe order: the package the Host half genuinely imports at
|
|
3839
|
-
* runtime first (host/fold.ts imports it, so it MUST resolve whenever this
|
|
3840
|
-
* plugin runs), then its co-versioned sibling.
|
|
3841
|
-
*/
|
|
3842
2926
|
const RUNNING_PROBE_PACKAGES = ["@deepseek-ai/dsh-session", "@deepseek-ai/dsh-session-projection"];
|
|
3843
|
-
/** Home-anchor probe order: the user-facing CLI version first, then the libraries. */
|
|
3844
2927
|
const HOME_PROBE_PACKAGES = ["@deepseek-ai/dsh", ...LIBRARY_PROBE_PACKAGES];
|
|
3845
|
-
/**
|
|
3846
|
-
* This package's root. A running-anchor witness under it belongs to the
|
|
3847
|
-
* plugin's own dependency closure, not to the harness.
|
|
3848
|
-
*
|
|
3849
|
-
* Located through the package's own `./package.json` self-reference: Node
|
|
3850
|
-
* resolves that against the nearest package.json, so the answer is the package
|
|
3851
|
-
* root in the source tree and in the bundled profile install alike — a fixed
|
|
3852
|
-
* number of `..` steps cannot be, since `src/host/` and `lib/` differ by a
|
|
3853
|
-
* level. A loader that cannot self-resolve (an exotic loader, or a fork renamed
|
|
3854
|
-
* without a matching `exports`) falls back to this module's own directory: the
|
|
3855
|
-
* guard then covers less of the package, but it still can never mistake the
|
|
3856
|
-
* plugin's own `node_modules` for the harness.
|
|
3857
|
-
*/
|
|
3858
2928
|
function ownPackageRoot() {
|
|
3859
2929
|
try {
|
|
3860
2930
|
return dirname(fileURLToPath(import.meta.resolve("dsh-context/package.json")));
|
|
3861
2931
|
} catch {
|
|
3862
|
-
/* v8 ignore next -- a loader that cannot resolve one of its own package's
|
|
3863
|
-
published subpaths is out of the suite's reach; the fallback keeps one
|
|
3864
|
-
probe path (an unanswered probe fails open either way). */
|
|
3865
2932
|
return dirname(fileURLToPath(import.meta.url));
|
|
3866
2933
|
}
|
|
3867
2934
|
}
|
|
3868
2935
|
const PLUGIN_ROOT = ownPackageRoot();
|
|
3869
|
-
/** One manifest's `version`, or undefined on any read/shape failure. */
|
|
3870
2936
|
function versionOfManifest(manifestPath, expectedName) {
|
|
3871
2937
|
try {
|
|
3872
2938
|
const parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
@@ -3878,7 +2944,6 @@ function versionOfManifest(manifestPath, expectedName) {
|
|
|
3878
2944
|
return;
|
|
3879
2945
|
}
|
|
3880
2946
|
}
|
|
3881
|
-
/** Probe the package's published `./package.json` subpath. */
|
|
3882
2947
|
function versionViaManifest(resolve, packageName) {
|
|
3883
2948
|
try {
|
|
3884
2949
|
return versionOfManifest(resolve(packageName + "/package.json"));
|
|
@@ -3886,12 +2951,6 @@ function versionViaManifest(resolve, packageName) {
|
|
|
3886
2951
|
return;
|
|
3887
2952
|
}
|
|
3888
2953
|
}
|
|
3889
|
-
/**
|
|
3890
|
-
* Probe via the package's entry point, ascending to its owning manifest.
|
|
3891
|
-
* Covers the packaged-executable module proxies: their generated manifests
|
|
3892
|
-
* carry the real version but export only entry stubs, and Node's exports
|
|
3893
|
-
* gate refuses the direct `./package.json` subpath.
|
|
3894
|
-
*/
|
|
3895
2954
|
function versionViaEntry(resolve, packageName) {
|
|
3896
2955
|
let dir;
|
|
3897
2956
|
try {
|
|
@@ -3907,33 +2966,16 @@ function versionViaEntry(resolve, packageName) {
|
|
|
3907
2966
|
dir = parent;
|
|
3908
2967
|
}
|
|
3909
2968
|
}
|
|
3910
|
-
/** One anchor's answer: the first probe package that yields a version. */
|
|
3911
2969
|
function probeAnchor(resolve, packageNames) {
|
|
3912
2970
|
for (const packageName of packageNames) {
|
|
3913
2971
|
const version = versionViaManifest(resolve, packageName) ?? versionViaEntry(resolve, packageName);
|
|
3914
2972
|
if (version !== void 0) return version;
|
|
3915
2973
|
}
|
|
3916
2974
|
}
|
|
3917
|
-
/**
|
|
3918
|
-
* Whether `candidate` lies inside `root`'s tree. A `..`-prefixed relative path
|
|
3919
|
-
* escapes it; a sibling such as `dsh-context-extra` is reached through `..` too,
|
|
3920
|
-
* so it never counts as a child. On Windows, a cross-drive `relative()` returns
|
|
3921
|
-
* the candidate's ABSOLUTE path (no `..` prefix) — a different drive is never
|
|
3922
|
-
* inside this tree, so the absolute answer is rejected the same way. A
|
|
3923
|
-
* pathological child literally named `..foo` would degrade to the home anchor,
|
|
3924
|
-
* which is the safe direction.
|
|
3925
|
-
*/
|
|
3926
2975
|
function isInside(root, candidate) {
|
|
3927
2976
|
const rel = relative(root, candidate);
|
|
3928
2977
|
return !rel.startsWith("..") && !isAbsolute(rel);
|
|
3929
2978
|
}
|
|
3930
|
-
/**
|
|
3931
|
-
* The running module tree's answer, or undefined when it cannot be trusted.
|
|
3932
|
-
* Each probe package is checked on its own: a witness inside the plugin's own
|
|
3933
|
-
* closure is the plugin's dependency, not the harness, and a trusted witness
|
|
3934
|
-
* whose manifest cannot be read is no answer either — the next package may
|
|
3935
|
-
* still resolve to one.
|
|
3936
|
-
*/
|
|
3937
2979
|
function probeRunningTree(resolve, pluginRoot) {
|
|
3938
2980
|
for (const packageName of RUNNING_PROBE_PACKAGES) {
|
|
3939
2981
|
let witness;
|
|
@@ -3947,16 +2989,9 @@ function probeRunningTree(resolve, pluginRoot) {
|
|
|
3947
2989
|
if (version !== void 0) return version;
|
|
3948
2990
|
}
|
|
3949
2991
|
}
|
|
3950
|
-
/** The default running anchor: this module's own ESM resolution. */
|
|
3951
2992
|
function resolveRunningModule(specifier) {
|
|
3952
2993
|
return import.meta.resolve(specifier);
|
|
3953
2994
|
}
|
|
3954
|
-
/**
|
|
3955
|
-
* The running harness's version string, or undefined when nothing answers
|
|
3956
|
-
* (the gate fails open on it — see the header note).
|
|
3957
|
-
* @param resolveUrl - the running anchor's URL resolver (injectable for hermetic tests).
|
|
3958
|
-
* @param pluginRoot - this package's root for the own-closure check (injectable for hermetic tests).
|
|
3959
|
-
*/
|
|
3960
2995
|
function detectHarnessVersion(ctx, resolveUrl = resolveRunningModule, pluginRoot = PLUGIN_ROOT) {
|
|
3961
2996
|
const running = probeRunningTree((specifier) => {
|
|
3962
2997
|
const url = resolveUrl(specifier);
|
|
@@ -3979,6 +3014,7 @@ const name = "dsh-context";
|
|
|
3979
3014
|
const inject = ["sessionProjections"];
|
|
3980
3015
|
function apply(ctx, config) {
|
|
3981
3016
|
watchBalanceChannel(ctx);
|
|
3017
|
+
watchSkillCatalog(ctx);
|
|
3982
3018
|
const harnessVersion = detectHarnessVersion(ctx);
|
|
3983
3019
|
if (harnessVersion !== void 0 && !meetsBaseline(harnessVersion)) {
|
|
3984
3020
|
ctx.sessionProjections.register(createFallbackTimelineDefinition(harnessVersion));
|
|
@@ -3994,7 +3030,6 @@ function apply(ctx, config) {
|
|
|
3994
3030
|
ctx.sessionProjections.register(createContextHeadersDefinition((name) => attribution.ownerOf(name)));
|
|
3995
3031
|
ctx.sessionProjections.register(createContextActivityDefinition());
|
|
3996
3032
|
watchActivityBackfill(ctx, coldReads);
|
|
3997
|
-
installSettings(ctx);
|
|
3998
3033
|
}
|
|
3999
3034
|
//#endregion
|
|
4000
3035
|
export { Config, apply, inject, name };
|