dsh-context 0.43.0 → 0.44.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/lib/client.js +1044 -562
- package/lib/index.d.ts +171 -24
- package/lib/index.js +830 -251
- package/package.json +6 -4
package/lib/index.js
CHANGED
|
@@ -273,7 +273,8 @@ const DEFAULT_BOUNDS = {
|
|
|
273
273
|
maxKeptTurns: 300,
|
|
274
274
|
maxEvents: 400,
|
|
275
275
|
maxNodes: 2e3,
|
|
276
|
-
maxArchiveNodes: 400
|
|
276
|
+
maxArchiveNodes: 400,
|
|
277
|
+
maxFileOps: 400
|
|
277
278
|
};
|
|
278
279
|
/**
|
|
279
280
|
* The cordis `Config` validator: strict on keys, defaults on the schema fields; tolerates `undefined` (a patch row without a `config:`
|
|
@@ -284,74 +285,27 @@ const Config = z.preprocess((v) => v ?? {}, z.object({
|
|
|
284
285
|
maxKeptTurns: z.number().int().min(1).default(DEFAULT_BOUNDS.maxKeptTurns),
|
|
285
286
|
maxEvents: z.number().int().min(1).default(DEFAULT_BOUNDS.maxEvents),
|
|
286
287
|
maxNodes: z.number().int().min(1).default(DEFAULT_BOUNDS.maxNodes),
|
|
287
|
-
maxArchiveNodes: z.number().int().min(1).default(DEFAULT_BOUNDS.maxArchiveNodes)
|
|
288
|
+
maxArchiveNodes: z.number().int().min(1).default(DEFAULT_BOUNDS.maxArchiveNodes),
|
|
289
|
+
maxFileOps: z.number().int().min(1).default(DEFAULT_BOUNDS.maxFileOps)
|
|
288
290
|
}).strict());
|
|
289
291
|
function resolveBounds(config) {
|
|
290
292
|
return Config.parse(config ?? {});
|
|
291
293
|
}
|
|
292
294
|
//#endregion
|
|
293
|
-
//#region src/shared/
|
|
294
|
-
/**
|
|
295
|
-
* The harness-version gate's shared arithmetic — the supported dsh baseline
|
|
296
|
-
* and the version compare behind it. Runtime code shared by BOTH halves (the
|
|
297
|
-
* host probes and gates; the client displays what the wire record carries),
|
|
298
|
-
* so this module must stay dependency-free.
|
|
299
|
-
*
|
|
300
|
-
* The baseline mirrors the support matrix (AGENTS.md "Compatibility" and the
|
|
301
|
-
* package's `dsh.compatibility.dshReleases` declaration): the oldest dsh
|
|
302
|
-
* release this plugin works on. A harness BELOW it gets the fallback units
|
|
303
|
-
* (host/fallback.ts) instead of the real folds.
|
|
304
|
-
*/
|
|
305
|
-
/** The oldest supported dsh release (see the matrix note above). */
|
|
306
|
-
const BASELINE_DSH_VERSION = "0.1.2-rc.1";
|
|
307
|
-
/**
|
|
308
|
-
* Release-channel rank at an equal X.Y.Z: a final release outranks its
|
|
309
|
-
* release candidates, which outrank betas, which outrank alphas
|
|
310
|
-
* (正式版 > RC > Beta > Alpha).
|
|
311
|
-
*/
|
|
312
|
-
function channelRank(channel) {
|
|
313
|
-
return channel === "rc" ? 3 : channel === "beta" ? 2 : 1;
|
|
314
|
-
}
|
|
315
|
-
const RELEASE_RANK = 4;
|
|
316
|
-
/**
|
|
317
|
-
* Parse `v?[major].[minor].[patch][-(alpha|beta|rc)[.N]][+build]`, or null
|
|
318
|
-
* when the string is not that shape. Channels other than alpha/beta/rc
|
|
319
|
-
* (nightly, dev, …) do not parse — the gate fails open on them.
|
|
320
|
-
*/
|
|
321
|
-
function parseVersion(version) {
|
|
322
|
-
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-(alpha|beta|rc)(?:\.(\d+))?)?(?:\+[0-9a-z.-]+)?$/i.exec(version.trim());
|
|
323
|
-
if (match === null) return null;
|
|
324
|
-
const channel = match[4]?.toLowerCase();
|
|
325
|
-
return {
|
|
326
|
-
major: Number(match[1]),
|
|
327
|
-
minor: Number(match[2]),
|
|
328
|
-
patch: Number(match[3]),
|
|
329
|
-
rank: channel === void 0 ? RELEASE_RANK : channelRank(channel),
|
|
330
|
-
serial: match[5] ? Number(match[5]) : 0
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
/**
|
|
334
|
-
* Total order over parsed versions: X.Y.Z numerically first, then the
|
|
335
|
-
* channel rank, then the prerelease serial.
|
|
336
|
-
*/
|
|
337
|
-
function compareParsed(a, b) {
|
|
338
|
-
if (a.major !== b.major) return a.major - b.major;
|
|
339
|
-
if (a.minor !== b.minor) return a.minor - b.minor;
|
|
340
|
-
if (a.patch !== b.patch) return a.patch - b.patch;
|
|
341
|
-
if (a.rank !== b.rank) return a.rank - b.rank;
|
|
342
|
-
return a.serial - b.serial;
|
|
343
|
-
}
|
|
295
|
+
//#region src/shared/estimate.ts
|
|
344
296
|
/**
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
297
|
+
* Token heuristics shared by the host fold and the client boundary — the
|
|
298
|
+
* harness token-meter's own fixed-density figure (dsh-token-meter/estimate.ts:
|
|
299
|
+
* ~4 chars ≈ 1 token, +4 role framing). Priced identically on both sides so a
|
|
300
|
+
* legacy value normalized at the client boundary matches what the host view
|
|
301
|
+
* would have served.
|
|
349
302
|
*/
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
303
|
+
const CHARS_PER_TOKEN$1 = 4;
|
|
304
|
+
const ROLE_OVERHEAD$1 = 4;
|
|
305
|
+
/** Price rendered system-prompt text; 0 for absent/empty/non-string input. */
|
|
306
|
+
function estimateSystemTokens(text) {
|
|
307
|
+
if (typeof text !== "string" || text.length === 0) return 0;
|
|
308
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN$1) + ROLE_OVERHEAD$1;
|
|
355
309
|
}
|
|
356
310
|
//#endregion
|
|
357
311
|
//#region src/shared/imageTokens.ts
|
|
@@ -499,11 +453,11 @@ function estimateImageTokens(width, height) {
|
|
|
499
453
|
* (shared/imageTokens.ts), falling back to the meter's JSON price when the
|
|
500
454
|
* attachment's dimensions are unknown.
|
|
501
455
|
*/
|
|
502
|
-
const CHARS_PER_TOKEN
|
|
456
|
+
const CHARS_PER_TOKEN = 4;
|
|
503
457
|
const BLOCK_OVERHEAD = 4;
|
|
504
|
-
const ROLE_OVERHEAD
|
|
458
|
+
const ROLE_OVERHEAD = 4;
|
|
505
459
|
function estimateToolsTotal(tools) {
|
|
506
|
-
return tools.length > 0 ? Math.ceil(JSON.stringify(tools).length / CHARS_PER_TOKEN
|
|
460
|
+
return tools.length > 0 ? Math.ceil(JSON.stringify(tools).length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD : 0;
|
|
507
461
|
}
|
|
508
462
|
/** The `ContentBlock` walkers take `unknown`: block arrays ride the untrusted
|
|
509
463
|
* log, so their element shapes (null and primitives included) are re-proved
|
|
@@ -520,10 +474,10 @@ function estimateBlocks(blocks) {
|
|
|
520
474
|
switch (block.type) {
|
|
521
475
|
case "text":
|
|
522
476
|
case "reasoning":
|
|
523
|
-
tokens += Math.ceil((block.text || "").length / CHARS_PER_TOKEN
|
|
477
|
+
tokens += Math.ceil((block.text || "").length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD;
|
|
524
478
|
break;
|
|
525
479
|
case "tool-call":
|
|
526
|
-
tokens += Math.ceil((block.name || "").length / CHARS_PER_TOKEN
|
|
480
|
+
tokens += Math.ceil((block.name || "").length / CHARS_PER_TOKEN) + Math.ceil((block.arguments || "").length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD;
|
|
527
481
|
break;
|
|
528
482
|
case "tool-result":
|
|
529
483
|
tokens += estimateBlocks(block.content) + BLOCK_OVERHEAD;
|
|
@@ -531,10 +485,10 @@ function estimateBlocks(blocks) {
|
|
|
531
485
|
case "image": {
|
|
532
486
|
const ref = block.attachment;
|
|
533
487
|
const priced = ref !== null && typeof ref === "object" && typeof ref.width === "number" && typeof ref.height === "number" ? estimateImageTokens(ref.width, ref.height) : null;
|
|
534
|
-
tokens += (priced ?? Math.ceil(JSON.stringify(block).length / CHARS_PER_TOKEN
|
|
488
|
+
tokens += (priced ?? Math.ceil(JSON.stringify(block).length / CHARS_PER_TOKEN)) + BLOCK_OVERHEAD;
|
|
535
489
|
break;
|
|
536
490
|
}
|
|
537
|
-
default: tokens += BLOCK_OVERHEAD + Math.ceil(JSON.stringify(block).length / CHARS_PER_TOKEN
|
|
491
|
+
default: tokens += BLOCK_OVERHEAD + Math.ceil(JSON.stringify(block).length / CHARS_PER_TOKEN);
|
|
538
492
|
}
|
|
539
493
|
}
|
|
540
494
|
return tokens;
|
|
@@ -546,12 +500,12 @@ function estimateBlocks(blocks) {
|
|
|
546
500
|
*/
|
|
547
501
|
function estimateMessage(message, emptyIsZero = false) {
|
|
548
502
|
if (emptyIsZero && (message === null || message === void 0 || !Array.isArray(message.content) || message.content.length === 0)) return 0;
|
|
549
|
-
return estimateBlocks(message?.content) + ROLE_OVERHEAD
|
|
503
|
+
return estimateBlocks(message?.content) + ROLE_OVERHEAD;
|
|
550
504
|
}
|
|
551
505
|
/** The shared meter heuristic over rendered system-prompt text (shared/estimate.ts). */
|
|
552
506
|
/** Per-tool price for the top-tools display (the total uses dsh's whole-array price). */
|
|
553
507
|
function estimateToolSchema(tool) {
|
|
554
|
-
return Math.ceil(JSON.stringify(tool).length / CHARS_PER_TOKEN
|
|
508
|
+
return Math.ceil(JSON.stringify(tool).length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD;
|
|
555
509
|
}
|
|
556
510
|
/**
|
|
557
511
|
* Count image blocks in a message payload, recursing into nested content (tool-result blocks carry their inner blocks) — seeds each node's
|
|
@@ -610,157 +564,231 @@ function isInjection(source) {
|
|
|
610
564
|
return source !== null && source !== void 0 && (typeof source.kind === "string" && source.kind !== "" && source.kind !== "user" || typeof source.form === "string");
|
|
611
565
|
}
|
|
612
566
|
//#endregion
|
|
613
|
-
//#region src/shared/
|
|
567
|
+
//#region src/shared/fileOps.ts
|
|
568
|
+
/** Parse a call's raw JSON arguments; non-string/malformed/non-record inputs yield null. */
|
|
569
|
+
function parseCallArgs(raw) {
|
|
570
|
+
if (typeof raw !== "string" || raw === "") return null;
|
|
571
|
+
try {
|
|
572
|
+
const parsed = JSON.parse(raw);
|
|
573
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
574
|
+
} catch {
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
const KIND_BY_TOOL = {
|
|
579
|
+
read: "read",
|
|
580
|
+
read_image: "read",
|
|
581
|
+
write: "write",
|
|
582
|
+
edit: "write",
|
|
583
|
+
grep: "search",
|
|
584
|
+
glob: "search"
|
|
585
|
+
};
|
|
586
|
+
/** The file purpose of a tool, or null for non-file tools (bash, web_search…). */
|
|
587
|
+
function kindOfTool(tool) {
|
|
588
|
+
if (tool === void 0) return null;
|
|
589
|
+
return KIND_BY_TOOL[tool] ?? null;
|
|
590
|
+
}
|
|
614
591
|
/**
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
592
|
+
* The file purpose of one executed call. Like {@link kindOfTool} except for
|
|
593
|
+
* the one file tool whose purpose follows its arguments: `str_replace_editor`
|
|
594
|
+
* reads on `view` and writes on every other command (create / str_replace /
|
|
595
|
+
* insert — an unknown command writes too; the call failed and the row keeps
|
|
596
|
+
* its error flag).
|
|
620
597
|
*/
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
function estimateSystemTokens(text) {
|
|
625
|
-
if (typeof text !== "string" || text.length === 0) return 0;
|
|
626
|
-
return Math.ceil(text.length / CHARS_PER_TOKEN) + ROLE_OVERHEAD;
|
|
598
|
+
function kindOfCall(tool, args) {
|
|
599
|
+
if (tool === "str_replace_editor") return args !== null && args.command === "view" ? "read" : "write";
|
|
600
|
+
return kindOfTool(tool);
|
|
627
601
|
}
|
|
628
|
-
//#endregion
|
|
629
|
-
//#region src/host/headers.ts
|
|
630
602
|
/**
|
|
631
|
-
* The
|
|
632
|
-
*
|
|
633
|
-
*
|
|
634
|
-
* The hot `contextTimeline` unit carries only token prices of the system
|
|
635
|
-
* prompt and tool schemas; this companion unit keeps the per-epoch METADATA
|
|
636
|
-
* (epoch seq/time boundaries, per-tool token prices and plugin attribution)
|
|
637
|
-
* so the Context browser can pick the header epoch in force at any step and
|
|
638
|
-
* size its sections immediately. The epoch CONTENT (full system prompt text,
|
|
639
|
-
* full tool JSON schemas) deliberately does NOT ride the projection VALUE:
|
|
640
|
-
* session projections are served whole in every `session.list` row, control
|
|
641
|
-
* baseline, push frame, and change notification, so carrying content here
|
|
642
|
-
* multiplied it by sessions × epochs across every channel. The client
|
|
643
|
-
* fetches one epoch's `request/header` event on demand — a seq-anchored
|
|
644
|
-
* history read off the epoch's `seq`, the same targeted read the browser
|
|
645
|
-
* already uses for message content — and caches it per session (history is
|
|
646
|
-
* immutable).
|
|
647
|
-
*
|
|
648
|
-
* Read-compat over the persisted state (the pinned decision behind keeping
|
|
649
|
-
* `stateVersion` at 1): the harness serves a cold session's projections from
|
|
650
|
-
* its CACHED checkpoint rows and has no refresh channel for an idle session
|
|
651
|
-
* — a version bump invalidates every row and orphans the key until the
|
|
652
|
-
* session goes live again (the #37 regression). The state therefore still
|
|
653
|
-
* ACCEPTS the v1 content-bearing record shape, current folds append
|
|
654
|
-
* metadata-only records alongside any seeded legacy ones, and the view
|
|
655
|
-
* normalizes BOTH to the metadata-only wire shape (pricing the legacy system
|
|
656
|
-
* text at read time). Cached v1 rows keep working, new checkpoint writes
|
|
657
|
-
* shrink as legacy epochs age out of the capped list, and the wire — the
|
|
658
|
-
* part every delivery channel carries — is metadata-only from day one.
|
|
659
|
-
*
|
|
660
|
-
* Same projection contract as the timeline unit: pure init/apply/view,
|
|
661
|
-
* `Object.is` reference stability for uninteresting events, plain-JSON
|
|
662
|
-
* bounded state (epoch list capped — see HEADERS_MAX).
|
|
603
|
+
* The operation's target path: the path-ish argument of read/write tools;
|
|
604
|
+
* for searches the narrowing `path`, else the pattern itself (a pathless
|
|
605
|
+
* grep/glob's target IS the pattern — the workspace-wide search text).
|
|
663
606
|
*/
|
|
664
|
-
|
|
665
|
-
|
|
607
|
+
function pathOfArgs(tool, args) {
|
|
608
|
+
if (args === null) return null;
|
|
609
|
+
if (tool === "grep" || tool === "glob") {
|
|
610
|
+
const p = args.path;
|
|
611
|
+
if (typeof p === "string" && p !== "") return p;
|
|
612
|
+
const pattern = args.pattern;
|
|
613
|
+
return typeof pattern === "string" && pattern !== "" ? pattern : null;
|
|
614
|
+
}
|
|
615
|
+
for (const k of [
|
|
616
|
+
"file_path",
|
|
617
|
+
"filePath",
|
|
618
|
+
"path"
|
|
619
|
+
]) {
|
|
620
|
+
const v = args[k];
|
|
621
|
+
if (typeof v === "string" && v !== "") return v;
|
|
622
|
+
}
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
/** Rendered line count: '' is 0, a trailing newline closes its own line. */
|
|
626
|
+
function linesOf(s) {
|
|
627
|
+
if (s === "") return 0;
|
|
628
|
+
let n = 0;
|
|
629
|
+
for (let i = 0; i < s.length; i++) if (s[i] === "\n") n++;
|
|
630
|
+
return s.endsWith("\n") ? n : n + 1;
|
|
631
|
+
}
|
|
632
|
+
/** The added/removed pair of one content-bearing argument set, or zeros. */
|
|
633
|
+
function pairOf(added, removed) {
|
|
634
|
+
return {
|
|
635
|
+
added: typeof added === "string" ? linesOf(added) : 0,
|
|
636
|
+
removed: typeof removed === "string" ? linesOf(removed) : 0
|
|
637
|
+
};
|
|
638
|
+
}
|
|
666
639
|
/**
|
|
667
|
-
* The
|
|
668
|
-
*
|
|
640
|
+
* The signed line footprint of one call: an edit removes its old string and
|
|
641
|
+
* adds its new one; a write adds its content (the pre-existing body, if any,
|
|
642
|
+
* is unknowable from the arguments — the estimate stays honest about that);
|
|
643
|
+
* `str_replace_editor` splits the same shapes across its commands. Callers
|
|
644
|
+
* reach here only with parsed args (a null parse yields no path).
|
|
669
645
|
*/
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
systemTokens: z.number().int().nonnegative().optional(),
|
|
682
|
-
tools: z.array(storedToolSchema)
|
|
683
|
-
}).strict();
|
|
684
|
-
const contextHeadersStateSchema = z.object({ headers: z.array(storedEpochSchema) }).strict();
|
|
685
|
-
/** The wire schema: strict metadata — the shape every delivery channel carries. */
|
|
686
|
-
const headerToolWireSchema = z.object({
|
|
687
|
-
name: z.string(),
|
|
688
|
-
tokens: z.number().int().nonnegative(),
|
|
689
|
-
plugin: z.string().optional()
|
|
690
|
-
}).strict();
|
|
691
|
-
/** Exported for the fallback unit (fallback.ts): one wire contract, one schema. */
|
|
692
|
-
const contextHeadersSchema = z.object({ headers: z.array(z.object({
|
|
693
|
-
seq: z.number(),
|
|
694
|
-
time: z.number(),
|
|
695
|
-
systemTokens: z.number().int().nonnegative().optional(),
|
|
696
|
-
tools: z.array(headerToolWireSchema)
|
|
697
|
-
}).strict()) }).strict();
|
|
698
|
-
function recordOf(event) {
|
|
699
|
-
if (event.type !== "request/header") return null;
|
|
700
|
-
const rawHeader = event.data.header;
|
|
701
|
-
if (rawHeader === null || rawHeader === void 0 || typeof rawHeader !== "object") return null;
|
|
702
|
-
const header = rawHeader;
|
|
703
|
-
const tools = Array.isArray(header.tools) ? header.tools : [];
|
|
704
|
-
const record = {
|
|
705
|
-
seq: event.seq,
|
|
706
|
-
time: event.time,
|
|
707
|
-
tools: tools.map((t) => {
|
|
708
|
-
const tool = t !== null && typeof t === "object" ? t : {};
|
|
709
|
-
const entry = {
|
|
710
|
-
name: typeof tool.name === "string" ? tool.name : "?",
|
|
711
|
-
tokens: estimateToolSchema(t)
|
|
712
|
-
};
|
|
713
|
-
if (typeof tool.plugin === "string" && tool.plugin !== "") entry.plugin = tool.plugin;
|
|
714
|
-
return entry;
|
|
715
|
-
})
|
|
646
|
+
function deltaOf(tool, args) {
|
|
647
|
+
if (tool === "edit") return pairOf(args.new_string, args.old_string);
|
|
648
|
+
if (tool === "write") return pairOf(args.content, void 0);
|
|
649
|
+
if (tool === "str_replace_editor") {
|
|
650
|
+
if (args.command === "str_replace") return pairOf(args.new_str, args.old_str);
|
|
651
|
+
if (args.command === "insert") return pairOf(args.new_str, void 0);
|
|
652
|
+
if (args.command === "create") return pairOf(args.file_text, void 0);
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
added: 0,
|
|
656
|
+
removed: 0
|
|
716
657
|
};
|
|
717
|
-
|
|
718
|
-
|
|
658
|
+
}
|
|
659
|
+
/** The exact window a read's result meta reports: `offset` plus the retained
|
|
660
|
+
* `lines` array (the same bounded payload the read card renders from). Null
|
|
661
|
+
* for a foreign or malformed meta. */
|
|
662
|
+
function readWindowOf(meta) {
|
|
663
|
+
if (meta === null || typeof meta !== "object") return null;
|
|
664
|
+
const m = meta;
|
|
665
|
+
if (typeof m.path !== "string" || m.path === "") return null;
|
|
666
|
+
if (typeof m.offset !== "number" || !Number.isFinite(m.offset) || m.offset < 1) return null;
|
|
667
|
+
if (!Array.isArray(m.lines) || m.lines.length === 0) return null;
|
|
668
|
+
return {
|
|
669
|
+
start: m.offset,
|
|
670
|
+
count: m.lines.length
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
/** The limit estimate: the tool reads up to `limit` lines from `offset`;
|
|
674
|
+
* absent when the call reads unbounded. */
|
|
675
|
+
function readEstimateOf(args) {
|
|
676
|
+
const limit = args.limit;
|
|
677
|
+
return typeof limit === "number" && Number.isFinite(limit) && limit > 0 ? {
|
|
678
|
+
count: Math.floor(limit),
|
|
679
|
+
est: true
|
|
680
|
+
} : void 0;
|
|
681
|
+
}
|
|
682
|
+
/** What a read op shows: the exact window off the result meta, else the
|
|
683
|
+
* limit estimate, else nothing (an unbounded read names no footprint). */
|
|
684
|
+
function readOf(meta, args) {
|
|
685
|
+
const win = readWindowOf(meta);
|
|
686
|
+
if (win !== null) return {
|
|
687
|
+
start: win.start,
|
|
688
|
+
count: win.count
|
|
689
|
+
};
|
|
690
|
+
return readEstimateOf(args);
|
|
719
691
|
}
|
|
720
692
|
/**
|
|
721
|
-
*
|
|
722
|
-
*
|
|
723
|
-
* Contract mirror with a REQUIRED `wire` block (see compat.ts).
|
|
724
|
-
* @param resolve - best-effort tool-to-plugin attribution (see toolSources.ts); fills a missing `plugin` at view time so
|
|
725
|
-
* epochs folded without attribution still render a tag when the source is known.
|
|
693
|
+
* A search op's detail: the pattern, with the include filter appended when
|
|
694
|
+
* one narrowed the call. A patternless (malformed) search has no detail.
|
|
726
695
|
*/
|
|
727
|
-
function
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
696
|
+
function searchDetailOf(args) {
|
|
697
|
+
const pattern = args?.pattern;
|
|
698
|
+
if (typeof pattern !== "string" || pattern === "") return void 0;
|
|
699
|
+
const include = args?.include;
|
|
700
|
+
return typeof include === "string" && include !== "" ? `${pattern} (${include})` : pattern;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* The files a search demonstrably reached, read off the result's bounded
|
|
704
|
+
* presentation meta (grep groups matched lines by file; glob lists paths).
|
|
705
|
+
* Only the COMPLETE list attributes: a capped search (`truncated`) names a
|
|
706
|
+
* partial file set, and a malformed meta names none — both fall back to the
|
|
707
|
+
* call's own target. Each entry carries the reported match count.
|
|
708
|
+
*/
|
|
709
|
+
function searchFilesOf(meta) {
|
|
710
|
+
if (meta === null || typeof meta !== "object") return null;
|
|
711
|
+
const m = meta;
|
|
712
|
+
if (m.truncated !== false) return null;
|
|
713
|
+
const files = [];
|
|
714
|
+
if (m.shape === "matches" && Array.isArray(m.files)) for (const f of m.files) {
|
|
715
|
+
if (f === null || typeof f !== "object") continue;
|
|
716
|
+
const group = f;
|
|
717
|
+
if (typeof group.path === "string" && group.path !== "" && Array.isArray(group.matches)) files.push({
|
|
718
|
+
path: group.path,
|
|
719
|
+
hits: group.matches.length
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
else if (m.shape === "paths" && Array.isArray(m.paths)) {
|
|
723
|
+
for (const p of m.paths) if (typeof p === "string" && p !== "") files.push({
|
|
724
|
+
path: p,
|
|
725
|
+
hits: 0
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
return files.length > 0 ? files : null;
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* The one-shot per-call op assembly, uniform across every producer: the
|
|
732
|
+
* host's call/result pairing (args off the armed call, meta off the
|
|
733
|
+
* result), the nested Code-Mode settle (no meta exists on a dispatch — the
|
|
734
|
+
* read window and per-file search attribution degrade to the argument-only
|
|
735
|
+
* forms), and the client's inline-generation join fallback. Returns zero to
|
|
736
|
+
* N records: a search with the complete matched-file meta rows per file;
|
|
737
|
+
* any other file call rows once; a non-file tool (or a call whose arguments
|
|
738
|
+
* resolve no target) rows nothing.
|
|
739
|
+
*/
|
|
740
|
+
function opsOfCall(input) {
|
|
741
|
+
const args = parseCallArgs(input.argsRaw);
|
|
742
|
+
const kind = kindOfCall(input.tool, args);
|
|
743
|
+
if (kind === null) return [];
|
|
744
|
+
const stamp = {
|
|
745
|
+
seq: input.seq,
|
|
746
|
+
kind,
|
|
747
|
+
tool: input.tool,
|
|
748
|
+
err: input.err === true,
|
|
749
|
+
added: 0,
|
|
750
|
+
removed: 0,
|
|
751
|
+
path: "",
|
|
752
|
+
...input.time !== void 0 ? { time: input.time } : {},
|
|
753
|
+
...input.gone !== void 0 ? { gone: input.gone } : {},
|
|
754
|
+
...input.parent !== void 0 ? { parent: input.parent } : {},
|
|
755
|
+
...input.program !== void 0 ? { program: input.program } : {}
|
|
763
756
|
};
|
|
757
|
+
if (kind === "search") {
|
|
758
|
+
const files = searchFilesOf(input.meta);
|
|
759
|
+
if (files !== null) {
|
|
760
|
+
const detail = searchDetailOf(args);
|
|
761
|
+
const target = args !== null ? pathOfArgs(input.tool, args) : null;
|
|
762
|
+
const narrowed = args !== null && typeof args.path === "string" && args.path !== "";
|
|
763
|
+
return [...target !== null && !files.some((f) => f.path === target) ? [{
|
|
764
|
+
...stamp,
|
|
765
|
+
path: target,
|
|
766
|
+
...narrowed && detail !== void 0 ? { detail } : {},
|
|
767
|
+
...narrowed ? {} : { pattern: true }
|
|
768
|
+
}] : [], ...files.map((f) => ({
|
|
769
|
+
...stamp,
|
|
770
|
+
path: f.path,
|
|
771
|
+
...detail !== void 0 ? { detail } : {},
|
|
772
|
+
...f.hits > 0 ? { hits: f.hits } : {}
|
|
773
|
+
}))];
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (args === null) return [];
|
|
777
|
+
const path = pathOfArgs(input.tool, args);
|
|
778
|
+
if (path === null) return [];
|
|
779
|
+
const { added, removed } = deltaOf(input.tool, args);
|
|
780
|
+
const narrowed = typeof args.path === "string" && args.path !== "";
|
|
781
|
+
const detail = kind === "search" && narrowed ? searchDetailOf(args) : void 0;
|
|
782
|
+
const read = kind === "read" ? readOf(input.meta, args) : void 0;
|
|
783
|
+
return [{
|
|
784
|
+
...stamp,
|
|
785
|
+
path,
|
|
786
|
+
added,
|
|
787
|
+
removed,
|
|
788
|
+
...detail !== void 0 ? { detail } : {},
|
|
789
|
+
...read !== void 0 ? { read } : {},
|
|
790
|
+
...kind === "search" && !narrowed ? { pattern: true } : {}
|
|
791
|
+
}];
|
|
764
792
|
}
|
|
765
793
|
//#endregion
|
|
766
794
|
//#region src/host/fold.ts
|
|
@@ -792,6 +820,11 @@ function trimState(st, bounds) {
|
|
|
792
820
|
if (countTurnRuns(st.requests) > bounds.maxKeptTurns) st.requests = trimToLastTurns(st.requests, bounds.maxKeptTurns);
|
|
793
821
|
if (st.requests.length > bounds.maxRequestSteps) st.requests = st.requests.slice(-bounds.maxRequestSteps);
|
|
794
822
|
if (st.events.length > bounds.maxEvents) st.events = st.events.slice(-bounds.maxEvents);
|
|
823
|
+
if (st.fileOps.length > bounds.maxFileOps) {
|
|
824
|
+
const drop = st.fileOps.length - bounds.maxFileOps;
|
|
825
|
+
st.fileOpsFloor = Math.max(st.fileOpsFloor ?? 0, st.fileOps[drop - 1].seq);
|
|
826
|
+
st.fileOps = st.fileOps.slice(drop);
|
|
827
|
+
}
|
|
795
828
|
if (st.archived.length > 0) {
|
|
796
829
|
let drop = 0;
|
|
797
830
|
const oldestReq = st.requests.length > 0 ? st.requests[0].seq : void 0;
|
|
@@ -818,7 +851,8 @@ function createTimelineState() {
|
|
|
818
851
|
requests: [],
|
|
819
852
|
events: [],
|
|
820
853
|
archived: [],
|
|
821
|
-
callNames: {}
|
|
854
|
+
callNames: {},
|
|
855
|
+
fileOps: []
|
|
822
856
|
};
|
|
823
857
|
}
|
|
824
858
|
function categoryOf(type, message) {
|
|
@@ -828,13 +862,58 @@ function categoryOf(type, message) {
|
|
|
828
862
|
return "user";
|
|
829
863
|
}
|
|
830
864
|
/**
|
|
831
|
-
*
|
|
832
|
-
*
|
|
833
|
-
*
|
|
865
|
+
* Mark the detail collections dirty (TimelineState.detailRev). Every caller
|
|
866
|
+
* is a fold branch that just mutated the requests/events/surface/archive;
|
|
867
|
+
* branches that touch only the working slots (stepStart, callNames, the
|
|
868
|
+
* shadow claim) or the envelope scalars do NOT bump — the served detail is
|
|
869
|
+
* unchanged, and an open tab has nothing to refetch.
|
|
834
870
|
*/
|
|
835
|
-
function
|
|
836
|
-
|
|
837
|
-
|
|
871
|
+
function bumpDetailRev(st) {
|
|
872
|
+
st.detailRev = (st.detailRev ?? 0) + 1;
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Bound on the buffered nested Code-Mode ops (TimelineState.pendingCodeOps)
|
|
876
|
+
* — a hostile log that dispatches without settling the parent run_code
|
|
877
|
+
* cannot grow the persisted state past this.
|
|
878
|
+
*/
|
|
879
|
+
const PENDING_CODE_OPS_MAX = 200;
|
|
880
|
+
/** JSON-stringify an unknown argument payload; a hostile (cyclic) value yields no args. */
|
|
881
|
+
function argsRawOf(value) {
|
|
882
|
+
if (typeof value === "string") return value;
|
|
883
|
+
if (value === void 0 || value === null) return void 0;
|
|
884
|
+
try {
|
|
885
|
+
return JSON.stringify(value);
|
|
886
|
+
} catch {
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
/** Append op records to the fold-derived log (the trim lives in trimState, with the other collections). */
|
|
891
|
+
function pushFileOps(st, ops) {
|
|
892
|
+
for (const op of ops) st.fileOps.push(op);
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Buffer nested Code-Mode ops under their top run_code call id (they flush
|
|
896
|
+
* when the parent's result folds — the ops' locate target). A full buffer
|
|
897
|
+
* drops new arrivals wholesale (defensive logs only).
|
|
898
|
+
*/
|
|
899
|
+
function bufferCodeOps(st, rootCallId, ops) {
|
|
900
|
+
const pending = st.pendingCodeOps ?? {};
|
|
901
|
+
let total = 0;
|
|
902
|
+
for (const k in pending) total += pending[k].length;
|
|
903
|
+
if (total + ops.length > PENDING_CODE_OPS_MAX) return;
|
|
904
|
+
st.pendingCodeOps = {
|
|
905
|
+
...pending,
|
|
906
|
+
[rootCallId]: [...pending[rootCallId] ?? [], ...ops]
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Archive removed surface nodes as stamped COPIES — the objects leaving
|
|
911
|
+
* `st.surface` are shared with the persisted previous state, so `gone` must
|
|
912
|
+
* never be written onto them directly.
|
|
913
|
+
*/
|
|
914
|
+
function archiveRemoved(st, removed, goneSeq) {
|
|
915
|
+
for (const n of removed) st.archived.push({
|
|
916
|
+
...n,
|
|
838
917
|
gone: goneSeq
|
|
839
918
|
});
|
|
840
919
|
}
|
|
@@ -1130,6 +1209,8 @@ function applyTimeline(state, event, bounds) {
|
|
|
1130
1209
|
events: [...state.events],
|
|
1131
1210
|
archived: [...state.archived],
|
|
1132
1211
|
callNames: { ...state.callNames },
|
|
1212
|
+
fileOps: [...state.fileOps],
|
|
1213
|
+
...state.pendingCodeOps !== void 0 ? { pendingCodeOps: { ...state.pendingCodeOps } } : {},
|
|
1133
1214
|
...state.timing !== void 0 ? { timing: {
|
|
1134
1215
|
...state.timing,
|
|
1135
1216
|
tools: { ...state.timing.tools }
|
|
@@ -1146,13 +1227,16 @@ function applyTimeline(state, event, bounds) {
|
|
|
1146
1227
|
s.systemTokens = estimateSystemTokens(header.system);
|
|
1147
1228
|
if (header.config && typeof header.config.model === "string") s.model = header.config.model;
|
|
1148
1229
|
if (header.config && typeof header.config.provider === "string") s.provider = header.config.provider;
|
|
1149
|
-
if ((data?.reason === "change" || data?.reason === "resume") && s.model && s.lastModel && s.model !== s.lastModel)
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1230
|
+
if ((data?.reason === "change" || data?.reason === "resume") && s.model && s.lastModel && s.model !== s.lastModel) {
|
|
1231
|
+
s.events.push({
|
|
1232
|
+
seq: event.seq,
|
|
1233
|
+
time: event.time,
|
|
1234
|
+
kind: "model",
|
|
1235
|
+
from: s.lastModel,
|
|
1236
|
+
to: s.model
|
|
1237
|
+
});
|
|
1238
|
+
bumpDetailRev(s);
|
|
1239
|
+
}
|
|
1156
1240
|
if (s.model) s.lastModel = s.model;
|
|
1157
1241
|
break;
|
|
1158
1242
|
}
|
|
@@ -1166,12 +1250,29 @@ function applyTimeline(state, event, bounds) {
|
|
|
1166
1250
|
case "tool/call":
|
|
1167
1251
|
if (data && typeof data.callId === "string" && typeof data.name === "string") {
|
|
1168
1252
|
const s = ensure();
|
|
1253
|
+
const argsRaw = argsRawOf(data.arguments);
|
|
1169
1254
|
s.callNames[data.callId] = {
|
|
1170
1255
|
name: data.name,
|
|
1171
|
-
start: event.time
|
|
1256
|
+
start: event.time,
|
|
1257
|
+
...argsRaw !== void 0 ? { argsRaw } : {}
|
|
1172
1258
|
};
|
|
1173
1259
|
}
|
|
1174
1260
|
break;
|
|
1261
|
+
case "tool/code-dispatch": {
|
|
1262
|
+
const rootCallId = data?.rootCallId;
|
|
1263
|
+
const name = data?.name;
|
|
1264
|
+
if (typeof rootCallId === "string" && typeof name === "string") {
|
|
1265
|
+
const ops = opsOfCall({
|
|
1266
|
+
seq: event.seq,
|
|
1267
|
+
time: event.time,
|
|
1268
|
+
tool: name,
|
|
1269
|
+
argsRaw: argsRawOf(data?.arguments),
|
|
1270
|
+
err: data?.isError === true
|
|
1271
|
+
});
|
|
1272
|
+
if (ops.length > 0) bufferCodeOps(ensure(), rootCallId, ops);
|
|
1273
|
+
}
|
|
1274
|
+
break;
|
|
1275
|
+
}
|
|
1175
1276
|
case "assistant/chunk": {
|
|
1176
1277
|
const start = state.stepStart;
|
|
1177
1278
|
if (start === void 0 || start.firstToken !== void 0) return state;
|
|
@@ -1199,6 +1300,7 @@ function applyTimeline(state, event, bounds) {
|
|
|
1199
1300
|
case "user/message": {
|
|
1200
1301
|
const msg = deriveEventMessage(event);
|
|
1201
1302
|
const s = ensure();
|
|
1303
|
+
bumpDetailRev(s);
|
|
1202
1304
|
const node = applySurface(s, event, event.type, data, msg);
|
|
1203
1305
|
const source = msg?.source;
|
|
1204
1306
|
if (isInjection(source)) {
|
|
@@ -1223,8 +1325,34 @@ function applyTimeline(state, event, bounds) {
|
|
|
1223
1325
|
}
|
|
1224
1326
|
case "tool/result": {
|
|
1225
1327
|
const toolMsg = deriveEventMessage(event);
|
|
1328
|
+
const srcId = (toolMsg?.source)?.callId;
|
|
1329
|
+
const firstBlock = toolMsg?.content?.[0];
|
|
1330
|
+
const blockId = firstBlock?.toolCallId;
|
|
1331
|
+
const pendingEntry = (typeof srcId === "string" ? state.callNames[srcId] : void 0) ?? (typeof blockId === "string" ? state.callNames[blockId] : void 0);
|
|
1332
|
+
const buffered = (typeof srcId === "string" ? state.pendingCodeOps?.[srcId] : void 0) ?? (typeof blockId === "string" ? state.pendingCodeOps?.[blockId] : void 0);
|
|
1226
1333
|
const s = ensure();
|
|
1334
|
+
bumpDetailRev(s);
|
|
1227
1335
|
const node = applySurface(s, event, event.type, data, toolMsg);
|
|
1336
|
+
if (pendingEntry !== void 0) pushFileOps(s, opsOfCall({
|
|
1337
|
+
seq: event.seq,
|
|
1338
|
+
time: event.time,
|
|
1339
|
+
tool: pendingEntry.name,
|
|
1340
|
+
argsRaw: pendingEntry.argsRaw,
|
|
1341
|
+
meta: data?.meta,
|
|
1342
|
+
err: Boolean(data?.error) || firstBlock?.isError === true
|
|
1343
|
+
}));
|
|
1344
|
+
if (buffered !== void 0 && buffered.length > 0) {
|
|
1345
|
+
const program = parseCallArgs(pendingEntry?.argsRaw)?.description;
|
|
1346
|
+
pushFileOps(s, buffered.map((op) => ({
|
|
1347
|
+
...op,
|
|
1348
|
+
parent: event.seq,
|
|
1349
|
+
...typeof program === "string" && program !== "" ? { program } : {}
|
|
1350
|
+
})));
|
|
1351
|
+
const kept = {};
|
|
1352
|
+
for (const k in s.pendingCodeOps) if (k !== srcId && k !== blockId) kept[k] = s.pendingCodeOps[k];
|
|
1353
|
+
if (Object.keys(kept).length > 0) s.pendingCodeOps = kept;
|
|
1354
|
+
else delete s.pendingCodeOps;
|
|
1355
|
+
}
|
|
1228
1356
|
if (node.tool === "skill" || node.tool === void 0) {
|
|
1229
1357
|
const name = skillNameOf(toolMsg);
|
|
1230
1358
|
if (name !== "") {
|
|
@@ -1245,6 +1373,7 @@ function applyTimeline(state, event, bounds) {
|
|
|
1245
1373
|
case "assistant/message": {
|
|
1246
1374
|
const usage = data?.usage;
|
|
1247
1375
|
const s = ensure();
|
|
1376
|
+
bumpDetailRev(s);
|
|
1248
1377
|
const total = s.systemTokens + s.toolsTokens + s.sums.user + s.sums.inject + s.sums.assistant + s.sums.tool;
|
|
1249
1378
|
const record = {
|
|
1250
1379
|
time: event.time,
|
|
@@ -1289,16 +1418,21 @@ function applyTimeline(state, event, bounds) {
|
|
|
1289
1418
|
break;
|
|
1290
1419
|
}
|
|
1291
1420
|
case "plan/mode":
|
|
1292
|
-
if (data && typeof data.active === "boolean")
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1421
|
+
if (data && typeof data.active === "boolean") {
|
|
1422
|
+
const s = ensure();
|
|
1423
|
+
s.events.push({
|
|
1424
|
+
seq: event.seq,
|
|
1425
|
+
time: event.time,
|
|
1426
|
+
kind: "mode",
|
|
1427
|
+
name: data.active ? "plan.on" : "plan.off"
|
|
1428
|
+
});
|
|
1429
|
+
bumpDetailRev(s);
|
|
1430
|
+
}
|
|
1298
1431
|
break;
|
|
1299
1432
|
case "compaction/summary":
|
|
1300
1433
|
case "compaction/prune": {
|
|
1301
1434
|
const s = ensure();
|
|
1435
|
+
bumpDetailRev(s);
|
|
1302
1436
|
if (data && Array.isArray(data.shadowedSeqs)) {
|
|
1303
1437
|
s.pendingShadowedSeqs = data.shadowedSeqs.filter((x) => typeof x === "number");
|
|
1304
1438
|
s.pendingShadowEventSeq = event.seq;
|
|
@@ -1324,11 +1458,15 @@ function applyTimeline(state, event, bounds) {
|
|
|
1324
1458
|
return state;
|
|
1325
1459
|
}
|
|
1326
1460
|
/**
|
|
1327
|
-
*
|
|
1328
|
-
*
|
|
1329
|
-
* — the
|
|
1461
|
+
* The envelope scalars both wire generations share: current composition, the
|
|
1462
|
+
* live-surface counters, and the copied cost/timing totals. Served value
|
|
1463
|
+
* fields are COPIES — the served value must never alias persisted state.
|
|
1464
|
+
* Optional scalars use conditional spread: an unknown value must not
|
|
1465
|
+
* materialize an `undefined`-valued property (the lossless-JSON pipeline —
|
|
1466
|
+
* a single such property can fail the whole push, the failure mode behind
|
|
1467
|
+
* issue #29).
|
|
1330
1468
|
*/
|
|
1331
|
-
function
|
|
1469
|
+
function headFieldsOf(state) {
|
|
1332
1470
|
const surfaceTotal = state.sums.user + state.sums.inject + state.sums.assistant + state.sums.tool;
|
|
1333
1471
|
const result = {
|
|
1334
1472
|
ok: true,
|
|
@@ -1346,11 +1484,11 @@ function buildTimelineView(state, bounds) {
|
|
|
1346
1484
|
},
|
|
1347
1485
|
images: state.surface.reduce((n, node) => n + (node.imgs ?? 0), 0),
|
|
1348
1486
|
toolCalls: state.surface.reduce((n, node) => node.cat === "tool" ? n + 1 : n, 0),
|
|
1349
|
-
requests:
|
|
1350
|
-
events:
|
|
1487
|
+
requests: [],
|
|
1488
|
+
events: [],
|
|
1351
1489
|
nodes: [],
|
|
1352
1490
|
droppedNodes: 0,
|
|
1353
|
-
archive:
|
|
1491
|
+
archive: []
|
|
1354
1492
|
};
|
|
1355
1493
|
if (state.cost !== void 0) {
|
|
1356
1494
|
const copyFam = (f) => {
|
|
@@ -1375,6 +1513,25 @@ function buildTimelineView(state, bounds) {
|
|
|
1375
1513
|
tools
|
|
1376
1514
|
};
|
|
1377
1515
|
}
|
|
1516
|
+
return result;
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* The heavy collections: copies of the retained request records and context
|
|
1520
|
+
* events (each event attached to the requests around it — the chart's ✂
|
|
1521
|
+
* anchoring), the bounded served surface window, and the removed-node
|
|
1522
|
+
* archive. Shared verbatim by the inline wire view (channel-less hosts) and
|
|
1523
|
+
* the on-demand detail payload (host/detail.ts).
|
|
1524
|
+
*/
|
|
1525
|
+
function detailCollectionsOf(state, bounds) {
|
|
1526
|
+
const result = {
|
|
1527
|
+
requests: state.requests.map((r) => ({ ...r })),
|
|
1528
|
+
events: state.events.map((e) => ({ ...e })),
|
|
1529
|
+
nodes: [],
|
|
1530
|
+
droppedNodes: 0,
|
|
1531
|
+
archive: state.archived.map((n) => ({ ...n })),
|
|
1532
|
+
fileOps: state.fileOps.map((o) => ({ ...o })),
|
|
1533
|
+
...state.fileOpsFloor !== void 0 ? { fileOpsFloor: state.fileOpsFloor } : {}
|
|
1534
|
+
};
|
|
1378
1535
|
const overflowCount = Math.max(0, state.surface.length - bounds.maxNodes);
|
|
1379
1536
|
const overflow = state.surface.slice(0, overflowCount);
|
|
1380
1537
|
const tail = state.surface.slice(overflowCount);
|
|
@@ -1405,6 +1562,354 @@ function buildTimelineView(state, bounds) {
|
|
|
1405
1562
|
}
|
|
1406
1563
|
return result;
|
|
1407
1564
|
}
|
|
1565
|
+
/**
|
|
1566
|
+
* The split generation's SLIM wire head: the envelope scalars plus the
|
|
1567
|
+
* precomputed count figures, the newest request's billing summary (the
|
|
1568
|
+
* headline's derived anchor), and the detail revision marker. Small enough
|
|
1569
|
+
* to ride every delivery channel whole (~1KB) — the heavy collections moved
|
|
1570
|
+
* to the on-demand detail channel (host/detail.ts).
|
|
1571
|
+
*/
|
|
1572
|
+
function buildTimelineHead(state) {
|
|
1573
|
+
const result = headFieldsOf(state);
|
|
1574
|
+
const turns = /* @__PURE__ */ new Set();
|
|
1575
|
+
for (const r of state.requests) turns.add(r.turn ?? 0);
|
|
1576
|
+
let injects = 0;
|
|
1577
|
+
let compactions = 0;
|
|
1578
|
+
let prunes = 0;
|
|
1579
|
+
for (const e of state.events) if (e.kind === "inject") injects++;
|
|
1580
|
+
else if (e.kind === "compaction") compactions++;
|
|
1581
|
+
else if (e.kind === "prune") prunes++;
|
|
1582
|
+
result.counts = {
|
|
1583
|
+
turns: turns.size,
|
|
1584
|
+
steps: state.requests.length,
|
|
1585
|
+
injects,
|
|
1586
|
+
compactions,
|
|
1587
|
+
prunes
|
|
1588
|
+
};
|
|
1589
|
+
const last = state.requests.at(-1);
|
|
1590
|
+
if (last !== void 0) result.last = {
|
|
1591
|
+
seq: last.seq,
|
|
1592
|
+
total: last.total,
|
|
1593
|
+
...typeof last.prompt === "number" ? { prompt: last.prompt } : {}
|
|
1594
|
+
};
|
|
1595
|
+
result.detailRev = state.detailRev ?? 0;
|
|
1596
|
+
return result;
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* The on-demand detail payload (host/detail.ts serves it off the live fold
|
|
1600
|
+
* state): the heavy collections plus the revision marker the head carries.
|
|
1601
|
+
*/
|
|
1602
|
+
function buildTimelineDetail(state, bounds) {
|
|
1603
|
+
return {
|
|
1604
|
+
rev: state.detailRev ?? 0,
|
|
1605
|
+
...detailCollectionsOf(state, bounds)
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
/**
|
|
1609
|
+
* Serve the INLINE projection wire view (channel-less hosts): the head
|
|
1610
|
+
* scalars with the detail collections in place — the shape every delivery
|
|
1611
|
+
* channel carried before the split generation. Bound the surface nodes to
|
|
1612
|
+
* the newest tail and attach each event to the request around it; stamp
|
|
1613
|
+
* COPIES — the persisted state objects are never mutated.
|
|
1614
|
+
*/
|
|
1615
|
+
function buildTimelineView(state, bounds) {
|
|
1616
|
+
return {
|
|
1617
|
+
...headFieldsOf(state),
|
|
1618
|
+
...detailCollectionsOf(state, bounds)
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
//#endregion
|
|
1622
|
+
//#region src/host/detail.ts
|
|
1623
|
+
/** The plugin's generic Connection RPC channel (the pre-v0.9 name, kept). */
|
|
1624
|
+
const DETAIL_CHANNEL = "/dsh-context";
|
|
1625
|
+
/** The RPC failure envelope the transport expects (ConnectionRpcFailure). */
|
|
1626
|
+
function failure(code, message) {
|
|
1627
|
+
return {
|
|
1628
|
+
ok: false,
|
|
1629
|
+
error: {
|
|
1630
|
+
code,
|
|
1631
|
+
message,
|
|
1632
|
+
details: {}
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Arm the detail endpoint whenever the connection and sessions services are
|
|
1638
|
+
* both composed (see the module header for the load-order contract). The
|
|
1639
|
+
* registration rides the injected fiber: either service unloading withdraws
|
|
1640
|
+
* the channel and closes the gate.
|
|
1641
|
+
*/
|
|
1642
|
+
function watchDetailChannel(ctx, bounds) {
|
|
1643
|
+
const gate = { live: false };
|
|
1644
|
+
ctx.inject(["connection", "sessions"], (c) => {
|
|
1645
|
+
const connection = c.get("connection");
|
|
1646
|
+
const sessions = c.get("sessions");
|
|
1647
|
+
const handle = typeof connection?.rpc?.handle === "function" ? connection.rpc.handle.bind(connection.rpc) : void 0;
|
|
1648
|
+
const getSession = typeof sessions?.get === "function" ? sessions.get.bind(sessions) : void 0;
|
|
1649
|
+
if (handle === void 0 || getSession === void 0) return;
|
|
1650
|
+
const projections = ctx.sessionProjections;
|
|
1651
|
+
const handler = async (endpoint, payload) => {
|
|
1652
|
+
if (endpoint !== "detail") return failure("dsh-context/unknown-endpoint", `unknown endpoint: ${endpoint}`);
|
|
1653
|
+
const sessionId = payload !== null && typeof payload === "object" ? payload.sessionId : void 0;
|
|
1654
|
+
if (typeof sessionId !== "string" || sessionId === "") return failure("dsh-context/bad-request", "missing sessionId");
|
|
1655
|
+
try {
|
|
1656
|
+
const session = getSession(sessionId);
|
|
1657
|
+
if (session !== void 0 && session !== null) {
|
|
1658
|
+
const state = projections.stateOf(session, "contextTimeline");
|
|
1659
|
+
if (state === void 0) return {
|
|
1660
|
+
ok: true,
|
|
1661
|
+
value: null
|
|
1662
|
+
};
|
|
1663
|
+
return {
|
|
1664
|
+
ok: true,
|
|
1665
|
+
value: buildTimelineDetail(state, bounds)
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
const query = ctx.get("sessionQuery");
|
|
1669
|
+
const observe = typeof query?.observeSession === "function" ? query.observeSession.bind(query) : void 0;
|
|
1670
|
+
if (observe === void 0) return {
|
|
1671
|
+
ok: true,
|
|
1672
|
+
value: null
|
|
1673
|
+
};
|
|
1674
|
+
const observation = await observe(sessionId, { projectionMode: "none" });
|
|
1675
|
+
const events = observation?.events;
|
|
1676
|
+
if (!Array.isArray(events)) return {
|
|
1677
|
+
ok: true,
|
|
1678
|
+
value: null
|
|
1679
|
+
};
|
|
1680
|
+
let state = createTimelineState();
|
|
1681
|
+
try {
|
|
1682
|
+
for (const ev of events) state = applyTimeline(state, ev, bounds);
|
|
1683
|
+
} finally {
|
|
1684
|
+
const dispose = observation?.[Symbol.dispose];
|
|
1685
|
+
if (typeof dispose === "function") dispose.call(observation);
|
|
1686
|
+
}
|
|
1687
|
+
return {
|
|
1688
|
+
ok: true,
|
|
1689
|
+
value: buildTimelineDetail(state, bounds)
|
|
1690
|
+
};
|
|
1691
|
+
} catch (err) {
|
|
1692
|
+
return failure("gateway/internal", err instanceof Error ? err.message : String(err));
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
try {
|
|
1696
|
+
c.effect(() => {
|
|
1697
|
+
const unregister = handle(DETAIL_CHANNEL, handler);
|
|
1698
|
+
return () => {
|
|
1699
|
+
unregister();
|
|
1700
|
+
};
|
|
1701
|
+
}, "dsh-context: detail channel");
|
|
1702
|
+
} catch {
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
gate.live = true;
|
|
1706
|
+
return () => {
|
|
1707
|
+
gate.live = false;
|
|
1708
|
+
};
|
|
1709
|
+
});
|
|
1710
|
+
return gate;
|
|
1711
|
+
}
|
|
1712
|
+
//#endregion
|
|
1713
|
+
//#region src/shared/version.ts
|
|
1714
|
+
/**
|
|
1715
|
+
* The harness-version gate's shared arithmetic — the supported dsh baseline
|
|
1716
|
+
* and the version compare behind it. Runtime code shared by BOTH halves (the
|
|
1717
|
+
* host probes and gates; the client displays what the wire record carries),
|
|
1718
|
+
* so this module must stay dependency-free.
|
|
1719
|
+
*
|
|
1720
|
+
* The baseline mirrors the support matrix (AGENTS.md "Compatibility" and the
|
|
1721
|
+
* package's `dsh.compatibility.dshReleases` declaration): the oldest dsh
|
|
1722
|
+
* release this plugin works on. A harness BELOW it gets the fallback units
|
|
1723
|
+
* (host/fallback.ts) instead of the real folds.
|
|
1724
|
+
*/
|
|
1725
|
+
/** The oldest supported dsh release (see the matrix note above). */
|
|
1726
|
+
const BASELINE_DSH_VERSION = "0.1.2-rc.1";
|
|
1727
|
+
/**
|
|
1728
|
+
* Release-channel rank at an equal X.Y.Z: a final release outranks its
|
|
1729
|
+
* release candidates, which outrank betas, which outrank alphas
|
|
1730
|
+
* (正式版 > RC > Beta > Alpha).
|
|
1731
|
+
*/
|
|
1732
|
+
function channelRank(channel) {
|
|
1733
|
+
return channel === "rc" ? 3 : channel === "beta" ? 2 : 1;
|
|
1734
|
+
}
|
|
1735
|
+
const RELEASE_RANK = 4;
|
|
1736
|
+
/**
|
|
1737
|
+
* Parse `v?[major].[minor].[patch][-(alpha|beta|rc)[.N]][+build]`, or null
|
|
1738
|
+
* when the string is not that shape. Channels other than alpha/beta/rc
|
|
1739
|
+
* (nightly, dev, …) do not parse — the gate fails open on them.
|
|
1740
|
+
*/
|
|
1741
|
+
function parseVersion(version) {
|
|
1742
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-(alpha|beta|rc)(?:\.(\d+))?)?(?:\+[0-9a-z.-]+)?$/i.exec(version.trim());
|
|
1743
|
+
if (match === null) return null;
|
|
1744
|
+
const channel = match[4]?.toLowerCase();
|
|
1745
|
+
return {
|
|
1746
|
+
major: Number(match[1]),
|
|
1747
|
+
minor: Number(match[2]),
|
|
1748
|
+
patch: Number(match[3]),
|
|
1749
|
+
rank: channel === void 0 ? RELEASE_RANK : channelRank(channel),
|
|
1750
|
+
serial: match[5] ? Number(match[5]) : 0
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Total order over parsed versions: X.Y.Z numerically first, then the
|
|
1755
|
+
* channel rank, then the prerelease serial.
|
|
1756
|
+
*/
|
|
1757
|
+
function compareParsed(a, b) {
|
|
1758
|
+
if (a.major !== b.major) return a.major - b.major;
|
|
1759
|
+
if (a.minor !== b.minor) return a.minor - b.minor;
|
|
1760
|
+
if (a.patch !== b.patch) return a.patch - b.patch;
|
|
1761
|
+
if (a.rank !== b.rank) return a.rank - b.rank;
|
|
1762
|
+
return a.serial - b.serial;
|
|
1763
|
+
}
|
|
1764
|
+
/**
|
|
1765
|
+
* Whether `version` satisfies the supported baseline. FAIL OPEN by design: a
|
|
1766
|
+
* version that cannot be parsed (a dev/nightly harness build) must not blank
|
|
1767
|
+
* a working deployment, so it passes — the gate trips only on a proven
|
|
1768
|
+
* below-baseline release.
|
|
1769
|
+
*/
|
|
1770
|
+
function meetsBaseline(version, baseline = BASELINE_DSH_VERSION) {
|
|
1771
|
+
const v = parseVersion(version);
|
|
1772
|
+
const b = parseVersion(baseline);
|
|
1773
|
+
if (v === null || b === null) return true;
|
|
1774
|
+
return compareParsed(v, b) >= 0;
|
|
1775
|
+
}
|
|
1776
|
+
//#endregion
|
|
1777
|
+
//#region src/host/headers.ts
|
|
1778
|
+
/**
|
|
1779
|
+
* The `contextHeaders` session projection unit — the request-header EPOCH
|
|
1780
|
+
* METADATA behind the timeline's envelope figures.
|
|
1781
|
+
*
|
|
1782
|
+
* The hot `contextTimeline` unit carries only token prices of the system
|
|
1783
|
+
* prompt and tool schemas; this companion unit keeps the per-epoch METADATA
|
|
1784
|
+
* (epoch seq/time boundaries, per-tool token prices and plugin attribution)
|
|
1785
|
+
* so the Context browser can pick the header epoch in force at any step and
|
|
1786
|
+
* size its sections immediately. The epoch CONTENT (full system prompt text,
|
|
1787
|
+
* full tool JSON schemas) deliberately does NOT ride the projection VALUE:
|
|
1788
|
+
* session projections are served whole in every `session.list` row, control
|
|
1789
|
+
* baseline, push frame, and change notification, so carrying content here
|
|
1790
|
+
* multiplied it by sessions × epochs across every channel. The client
|
|
1791
|
+
* fetches one epoch's `request/header` event on demand — a seq-anchored
|
|
1792
|
+
* history read off the epoch's `seq`, the same targeted read the browser
|
|
1793
|
+
* already uses for message content — and caches it per session (history is
|
|
1794
|
+
* immutable).
|
|
1795
|
+
*
|
|
1796
|
+
* Read-compat over the persisted state (the pinned decision behind keeping
|
|
1797
|
+
* `stateVersion` at 1): the harness serves a cold session's projections from
|
|
1798
|
+
* its CACHED checkpoint rows and has no refresh channel for an idle session
|
|
1799
|
+
* — a version bump invalidates every row and orphans the key until the
|
|
1800
|
+
* session goes live again (the #37 regression). The state therefore still
|
|
1801
|
+
* ACCEPTS the v1 content-bearing record shape, current folds append
|
|
1802
|
+
* metadata-only records alongside any seeded legacy ones, and the view
|
|
1803
|
+
* normalizes BOTH to the metadata-only wire shape (pricing the legacy system
|
|
1804
|
+
* text at read time). Cached v1 rows keep working, new checkpoint writes
|
|
1805
|
+
* shrink as legacy epochs age out of the capped list, and the wire — the
|
|
1806
|
+
* part every delivery channel carries — is metadata-only from day one.
|
|
1807
|
+
*
|
|
1808
|
+
* Same projection contract as the timeline unit: pure init/apply/view,
|
|
1809
|
+
* `Object.is` reference stability for uninteresting events, plain-JSON
|
|
1810
|
+
* bounded state (epoch list capped — see HEADERS_MAX).
|
|
1811
|
+
*/
|
|
1812
|
+
/** Retention cap on header epochs (metadata only; changes are rare; 50 is generous). */
|
|
1813
|
+
const HEADERS_MAX = 50;
|
|
1814
|
+
/**
|
|
1815
|
+
* The persisted-state schema: the SUPERSET of both record generations, so a
|
|
1816
|
+
* cached v1 row (content-bearing) seeds the fold instead of being discarded.
|
|
1817
|
+
*/
|
|
1818
|
+
const storedToolSchema = z.object({
|
|
1819
|
+
name: z.string(),
|
|
1820
|
+
tokens: z.number().int().nonnegative(),
|
|
1821
|
+
description: z.string().optional(),
|
|
1822
|
+
plugin: z.string().optional(),
|
|
1823
|
+
schema: z.unknown().optional()
|
|
1824
|
+
}).strict();
|
|
1825
|
+
const storedEpochSchema = z.object({
|
|
1826
|
+
seq: z.number(),
|
|
1827
|
+
time: z.number(),
|
|
1828
|
+
system: z.string().optional(),
|
|
1829
|
+
systemTokens: z.number().int().nonnegative().optional(),
|
|
1830
|
+
tools: z.array(storedToolSchema)
|
|
1831
|
+
}).strict();
|
|
1832
|
+
const contextHeadersStateSchema = z.object({ headers: z.array(storedEpochSchema) }).strict();
|
|
1833
|
+
/** The wire schema: strict metadata — the shape every delivery channel carries. */
|
|
1834
|
+
const headerToolWireSchema = z.object({
|
|
1835
|
+
name: z.string(),
|
|
1836
|
+
tokens: z.number().int().nonnegative(),
|
|
1837
|
+
plugin: z.string().optional()
|
|
1838
|
+
}).strict();
|
|
1839
|
+
/** Exported for the fallback unit (fallback.ts): one wire contract, one schema. */
|
|
1840
|
+
const contextHeadersSchema = z.object({ headers: z.array(z.object({
|
|
1841
|
+
seq: z.number(),
|
|
1842
|
+
time: z.number(),
|
|
1843
|
+
systemTokens: z.number().int().nonnegative().optional(),
|
|
1844
|
+
tools: z.array(headerToolWireSchema)
|
|
1845
|
+
}).strict()) }).strict();
|
|
1846
|
+
function recordOf(event) {
|
|
1847
|
+
if (event.type !== "request/header") return null;
|
|
1848
|
+
const rawHeader = event.data.header;
|
|
1849
|
+
if (rawHeader === null || rawHeader === void 0 || typeof rawHeader !== "object") return null;
|
|
1850
|
+
const header = rawHeader;
|
|
1851
|
+
const tools = Array.isArray(header.tools) ? header.tools : [];
|
|
1852
|
+
const record = {
|
|
1853
|
+
seq: event.seq,
|
|
1854
|
+
time: event.time,
|
|
1855
|
+
tools: tools.map((t) => {
|
|
1856
|
+
const tool = t !== null && typeof t === "object" ? t : {};
|
|
1857
|
+
const entry = {
|
|
1858
|
+
name: typeof tool.name === "string" ? tool.name : "?",
|
|
1859
|
+
tokens: estimateToolSchema(t)
|
|
1860
|
+
};
|
|
1861
|
+
if (typeof tool.plugin === "string" && tool.plugin !== "") entry.plugin = tool.plugin;
|
|
1862
|
+
return entry;
|
|
1863
|
+
})
|
|
1864
|
+
};
|
|
1865
|
+
if (typeof header.system === "string" && header.system.length > 0) record.systemTokens = estimateSystemTokens(header.system);
|
|
1866
|
+
return record;
|
|
1867
|
+
}
|
|
1868
|
+
/**
|
|
1869
|
+
* The context-headers projection unit; registered alongside the timeline unit (host/index.ts); clients read it through
|
|
1870
|
+
* `useProjection('contextHeaders')` and fetch an epoch's full content on demand via the session history (historyPage.ts).
|
|
1871
|
+
* Contract mirror with a REQUIRED `wire` block (see compat.ts).
|
|
1872
|
+
* @param resolve - best-effort tool-to-plugin attribution (see toolSources.ts); fills a missing `plugin` at view time so
|
|
1873
|
+
* epochs folded without attribution still render a tag when the source is known.
|
|
1874
|
+
*/
|
|
1875
|
+
function createContextHeadersDefinition(resolve) {
|
|
1876
|
+
const view = (state) => ({ headers: state.headers.map((h) => {
|
|
1877
|
+
const record = {
|
|
1878
|
+
seq: h.seq,
|
|
1879
|
+
time: h.time,
|
|
1880
|
+
tools: h.tools.map((t) => {
|
|
1881
|
+
const entry = {
|
|
1882
|
+
name: t.name,
|
|
1883
|
+
tokens: t.tokens
|
|
1884
|
+
};
|
|
1885
|
+
const plugin = t.plugin ?? (resolve !== void 0 ? resolve(t.name) : void 0);
|
|
1886
|
+
if (plugin !== void 0) entry.plugin = plugin;
|
|
1887
|
+
return entry;
|
|
1888
|
+
})
|
|
1889
|
+
};
|
|
1890
|
+
const systemTokens = h.systemTokens ?? (typeof h.system === "string" && h.system !== "" ? estimateSystemTokens(h.system) : void 0);
|
|
1891
|
+
if (systemTokens !== void 0) record.systemTokens = systemTokens;
|
|
1892
|
+
return record;
|
|
1893
|
+
}) });
|
|
1894
|
+
return {
|
|
1895
|
+
key: "contextHeaders",
|
|
1896
|
+
stateSchema: contextHeadersStateSchema,
|
|
1897
|
+
wire: {
|
|
1898
|
+
viewSchema: contextHeadersSchema,
|
|
1899
|
+
view
|
|
1900
|
+
},
|
|
1901
|
+
init: () => ({ headers: [] }),
|
|
1902
|
+
apply: (state, event) => {
|
|
1903
|
+
const record = recordOf(event);
|
|
1904
|
+
if (record === null) return state;
|
|
1905
|
+
const last = state.headers.at(-1);
|
|
1906
|
+
if (last !== void 0 && last.seq === record.seq) return state;
|
|
1907
|
+
const headers = [...state.headers, record];
|
|
1908
|
+
return { headers: headers.length > HEADERS_MAX ? headers.slice(-50) : headers };
|
|
1909
|
+
},
|
|
1910
|
+
stateVersion: 1
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1408
1913
|
//#endregion
|
|
1409
1914
|
//#region src/host/timeline.ts
|
|
1410
1915
|
/**
|
|
@@ -1485,6 +1990,34 @@ const contextEventSchema = z.object({
|
|
|
1485
1990
|
turn: z.number().optional(),
|
|
1486
1991
|
step: z.number().optional()
|
|
1487
1992
|
}).strict();
|
|
1993
|
+
/** The fold-derived file-operation record (shared/types.ts FileOpRecord). */
|
|
1994
|
+
const fileOpSchema = z.object({
|
|
1995
|
+
seq: z.number().int().nonnegative(),
|
|
1996
|
+
path: z.string(),
|
|
1997
|
+
kind: z.enum([
|
|
1998
|
+
"read",
|
|
1999
|
+
"write",
|
|
2000
|
+
"search"
|
|
2001
|
+
]),
|
|
2002
|
+
tool: z.string(),
|
|
2003
|
+
time: z.number().optional(),
|
|
2004
|
+
err: z.boolean(),
|
|
2005
|
+
added: z.number().int().nonnegative(),
|
|
2006
|
+
removed: z.number().int().nonnegative(),
|
|
2007
|
+
detail: z.string().optional(),
|
|
2008
|
+
hits: z.number().int().positive().optional(),
|
|
2009
|
+
read: z.union([z.object({
|
|
2010
|
+
start: z.number().int().positive(),
|
|
2011
|
+
count: z.number().int().nonnegative()
|
|
2012
|
+
}).strict(), z.object({
|
|
2013
|
+
count: z.number().int().positive(),
|
|
2014
|
+
est: z.literal(true)
|
|
2015
|
+
}).strict()]).optional(),
|
|
2016
|
+
parent: z.number().int().nonnegative().optional(),
|
|
2017
|
+
program: z.string().optional(),
|
|
2018
|
+
pattern: z.literal(true).optional(),
|
|
2019
|
+
gone: z.number().int().nonnegative().optional()
|
|
2020
|
+
}).strict();
|
|
1488
2021
|
const currentSchema = z.object({
|
|
1489
2022
|
system: z.number().int().nonnegative(),
|
|
1490
2023
|
tools: z.number().int().nonnegative(),
|
|
@@ -1522,7 +2055,28 @@ const unsupportedSchema = z.object({
|
|
|
1522
2055
|
current: z.string(),
|
|
1523
2056
|
minimum: z.string()
|
|
1524
2057
|
}).strict();
|
|
1525
|
-
/**
|
|
2058
|
+
/** The stats board's precomputed count figures (the split head — see Snapshot.counts). */
|
|
2059
|
+
const countsSchema = z.object({
|
|
2060
|
+
turns: z.number().int().nonnegative(),
|
|
2061
|
+
steps: z.number().int().nonnegative(),
|
|
2062
|
+
injects: z.number().int().nonnegative(),
|
|
2063
|
+
compactions: z.number().int().nonnegative(),
|
|
2064
|
+
prunes: z.number().int().nonnegative()
|
|
2065
|
+
}).strict();
|
|
2066
|
+
/** The newest retained request's billing summary (the split head's headline anchor). */
|
|
2067
|
+
const lastSchema = z.object({
|
|
2068
|
+
seq: z.number(),
|
|
2069
|
+
total: z.number().int().nonnegative(),
|
|
2070
|
+
prompt: z.number().int().nonnegative().optional()
|
|
2071
|
+
}).strict();
|
|
2072
|
+
/**
|
|
2073
|
+
* One wire contract for both generations: the SPLIT head (envelope scalars +
|
|
2074
|
+
* counts/last/detailRev; the heavy collections stay absent — they ride the
|
|
2075
|
+
* on-demand detail channel, host/detail.ts) and the INLINE value
|
|
2076
|
+
* (channel-less hosts and the fallback unit carry the collections in place).
|
|
2077
|
+
* The collections are therefore optional on the schema; the split marker is
|
|
2078
|
+
* `detailRev` (present ⟺ split).
|
|
2079
|
+
*/
|
|
1526
2080
|
const contextTimelineSchema = z.object({
|
|
1527
2081
|
ok: z.literal(true),
|
|
1528
2082
|
unsupported: unsupportedSchema.optional(),
|
|
@@ -1532,18 +2086,23 @@ const contextTimelineSchema = z.object({
|
|
|
1532
2086
|
current: currentSchema,
|
|
1533
2087
|
images: z.number().int().nonnegative().optional(),
|
|
1534
2088
|
toolCalls: z.number().int().nonnegative().optional(),
|
|
1535
|
-
|
|
1536
|
-
|
|
2089
|
+
counts: countsSchema.optional(),
|
|
2090
|
+
last: lastSchema.optional(),
|
|
2091
|
+
detailRev: z.number().int().nonnegative().optional(),
|
|
2092
|
+
requests: z.array(requestRecordSchema).optional(),
|
|
2093
|
+
events: z.array(contextEventSchema).optional(),
|
|
1537
2094
|
cost: z.object({
|
|
1538
2095
|
flash: costFamilySchema.optional(),
|
|
1539
2096
|
pro: costFamilySchema.optional()
|
|
1540
2097
|
}).strict().optional(),
|
|
1541
2098
|
timing: timingTotalsSchema.optional(),
|
|
1542
|
-
nodes: z.array(surfaceNodeSchema),
|
|
1543
|
-
droppedNodes: z.number().int().nonnegative(),
|
|
1544
|
-
archive: z.array(surfaceNodeSchema),
|
|
2099
|
+
nodes: z.array(surfaceNodeSchema).optional(),
|
|
2100
|
+
droppedNodes: z.number().int().nonnegative().optional(),
|
|
2101
|
+
archive: z.array(surfaceNodeSchema).optional(),
|
|
1545
2102
|
surfaceFloor: z.number().int().nonnegative().optional(),
|
|
1546
|
-
archiveFloor: z.number().int().nonnegative().optional()
|
|
2103
|
+
archiveFloor: z.number().int().nonnegative().optional(),
|
|
2104
|
+
fileOps: z.array(fileOpSchema).optional(),
|
|
2105
|
+
fileOpsFloor: z.number().int().nonnegative().optional()
|
|
1547
2106
|
}).strict();
|
|
1548
2107
|
/**
|
|
1549
2108
|
* The persisted fold-state schema (the registry's `stateSchema`
|
|
@@ -1580,10 +2139,15 @@ const timelineStateSchema = z.object({
|
|
|
1580
2139
|
}).strict().optional(),
|
|
1581
2140
|
callNames: z.record(z.string(), z.object({
|
|
1582
2141
|
name: z.string(),
|
|
1583
|
-
start: z.number()
|
|
2142
|
+
start: z.number(),
|
|
2143
|
+
argsRaw: z.string().optional()
|
|
1584
2144
|
}).strict()),
|
|
1585
2145
|
pendingShadowedSeqs: z.array(z.number()).optional(),
|
|
1586
|
-
pendingShadowEventSeq: z.number().optional()
|
|
2146
|
+
pendingShadowEventSeq: z.number().optional(),
|
|
2147
|
+
detailRev: z.number().int().nonnegative().optional(),
|
|
2148
|
+
fileOps: z.array(fileOpSchema),
|
|
2149
|
+
fileOpsFloor: z.number().int().nonnegative().optional(),
|
|
2150
|
+
pendingCodeOps: z.record(z.string(), z.array(fileOpSchema)).optional()
|
|
1587
2151
|
});
|
|
1588
2152
|
/**
|
|
1589
2153
|
* The context-timeline projection unit, created per plugin instance with its
|
|
@@ -1603,10 +2167,24 @@ const timelineStateSchema = z.object({
|
|
|
1603
2167
|
* `wire` block the registry treats the unit as host-only and never delivers
|
|
1604
2168
|
* `contextTimeline` to the browser (the Context tab would stay on its
|
|
1605
2169
|
* loading screen forever).
|
|
2170
|
+
*
|
|
2171
|
+
* `slim` selects the wire generation PER SERVE (a liveness probe, not a
|
|
2172
|
+
* fixed flag): while the on-demand detail channel is live (host/detail.ts),
|
|
2173
|
+
* the wire value is the SLIM head (buildTimelineHead) — the heavy
|
|
2174
|
+
* collections no longer ride every session.list row, control baseline,
|
|
2175
|
+
* follow snapshot, and push frame. Before the channel arms (the connection
|
|
2176
|
+
* service may activate after this plugin) or on a deployment whose
|
|
2177
|
+
* connection/sessions services never compose, the unit serves the INLINE
|
|
2178
|
+
* value so the tab keeps working end to end. Both generations validate
|
|
2179
|
+
* against the same schema (the collections are optional on it), and both
|
|
2180
|
+
* fold the SAME state — the split is view-only, so no `stateVersion` bump
|
|
2181
|
+
* and no cached-row invalidation comes with it (the `detailRev` state field
|
|
2182
|
+
* is additive-optional: older rows restore without it and read as revision
|
|
2183
|
+
* 0).
|
|
1606
2184
|
*/
|
|
1607
|
-
function createContextTimelineDefinition(config) {
|
|
2185
|
+
function createContextTimelineDefinition(config, slim) {
|
|
1608
2186
|
const bounds = resolveBounds(config);
|
|
1609
|
-
const view = (state) => buildTimelineView(state, bounds);
|
|
2187
|
+
const view = (state) => slim() ? buildTimelineHead(state) : buildTimelineView(state, bounds);
|
|
1610
2188
|
return {
|
|
1611
2189
|
key: "contextTimeline",
|
|
1612
2190
|
stateSchema: timelineStateSchema,
|
|
@@ -1616,7 +2194,7 @@ function createContextTimelineDefinition(config) {
|
|
|
1616
2194
|
},
|
|
1617
2195
|
init: () => createTimelineState(),
|
|
1618
2196
|
apply: (state, event) => applyTimeline(state, event, bounds),
|
|
1619
|
-
stateVersion:
|
|
2197
|
+
stateVersion: 15
|
|
1620
2198
|
};
|
|
1621
2199
|
}
|
|
1622
2200
|
//#endregion
|
|
@@ -1844,7 +2422,8 @@ function apply(ctx, config) {
|
|
|
1844
2422
|
return;
|
|
1845
2423
|
}
|
|
1846
2424
|
const attribution = createToolAttribution(ctx);
|
|
1847
|
-
ctx
|
|
2425
|
+
const gate = watchDetailChannel(ctx, resolveBounds(config));
|
|
2426
|
+
ctx.sessionProjections.register(createContextTimelineDefinition(config, () => gate.live));
|
|
1848
2427
|
ctx.sessionProjections.register(createContextHeadersDefinition((name) => attribution.ownerOf(name)));
|
|
1849
2428
|
installSettings(ctx);
|
|
1850
2429
|
}
|