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