dsh-context 0.54.4 → 0.56.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 +17 -3
- package/icon.svg +12 -0
- package/lib/client.js +686 -468
- package/lib/index.d.ts +115 -81
- package/lib/index.js +280 -192
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +33 -15
package/lib/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { SessionLogOffset, deriveEventMessage, interruptedTurnClosers } from "@deepseek-ai/dsh-session";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
-
import { dirname, join, normalize, relative } from "node:path";
|
|
5
|
+
import { dirname, isAbsolute, join, normalize, relative } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import z$1 from "@deepseek-ai/schemastery";
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
@@ -330,30 +330,28 @@ function isInjection(source) {
|
|
|
330
330
|
//#region src/host/logShapes.ts
|
|
331
331
|
/**
|
|
332
332
|
* Shape-driven readers over the durable session-event vocabulary — the ONE
|
|
333
|
-
* place the plugin
|
|
333
|
+
* place the plugin's log spellings meet. The supported range (dsh
|
|
334
|
+
* 0.1.5-rc.1+, session formats V3/V4) speaks ONE dialect:
|
|
335
|
+
* `system/message` surface nodes, `assistant/message.data.stream` /
|
|
336
|
+
* `assistant/attempt.data.stream` (packed delta runs plus raw chunk
|
|
337
|
+
* records), `SurfaceOp { startSeq, endSeq }`, `tool/ptc-dispatch`.
|
|
334
338
|
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
* exclusive within it, and a deployment's version probe can be wrong (a
|
|
344
|
-
* healed profile mirror may name a different release than the running
|
|
345
|
-
* harness). Every reader is total over untrusted input — a malformed record
|
|
346
|
-
* yields "nothing here", never a throw (the projection registry drives the
|
|
347
|
-
* fold without an error boundary; one throw stalls the unit's push feed and
|
|
348
|
-
* the browser waits on "loading" forever).
|
|
339
|
+
* The fold still reads SHAPES, never a detected harness version: the raw log
|
|
340
|
+
* is untrusted input at every layer, and the supported harness refuses —
|
|
341
|
+
* never rewrites — a legacy-format log, so anything that reaches these
|
|
342
|
+
* readers is current-dialect or hostile. Every reader is total over
|
|
343
|
+
* untrusted input — a malformed record yields "nothing here", never a throw
|
|
344
|
+
* (the projection registry drives the fold without an error boundary; one
|
|
345
|
+
* throw stalls the unit's push feed and the browser waits on "loading"
|
|
346
|
+
* forever).
|
|
349
347
|
*
|
|
350
348
|
* @module dsh-context/host/log-shapes
|
|
351
349
|
*/
|
|
352
350
|
/**
|
|
353
|
-
* Whether one raw stream chunk carries a token delta — the first-token
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
351
|
+
* Whether one raw stream chunk carries a token delta — the first-token
|
|
352
|
+
* marker, mirroring dsh-llm's `isTokenDelta` (a non-empty text or reasoning
|
|
353
|
+
* fragment, or any Tool-call delta carrying arguments or a name); a
|
|
354
|
+
* malformed chunk is simply not a token.
|
|
357
355
|
*/
|
|
358
356
|
function isTokenChunk(chunk) {
|
|
359
357
|
if (chunk === null || typeof chunk !== "object") return false;
|
|
@@ -372,21 +370,28 @@ function decodeKindOfBlock(blockType) {
|
|
|
372
370
|
if (blockType === "tool-call") return "toolarg";
|
|
373
371
|
}
|
|
374
372
|
/**
|
|
375
|
-
* Per-kind decode spans inside one embedded
|
|
376
|
-
* [first block-start, endTime]: each `block-start`
|
|
377
|
-
* to the next one, the last one up to `endTime`.
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*/
|
|
383
|
-
function
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
373
|
+
* Per-kind decode spans AND counted `block-start` markers inside one embedded
|
|
374
|
+
* assistant stream, tiling [first block-start, endTime]: each `block-start`
|
|
375
|
+
* record owns the interval up to the next one, the last one up to `endTime`.
|
|
376
|
+
* Total over untrusted input — a malformed record is skipped whole (a
|
|
377
|
+
* marker with an unusable time anchors no span and counts nothing), a
|
|
378
|
+
* non-finite boundary yields zero, and a stream with no marker (or not an
|
|
379
|
+
* array) yields all zeros.
|
|
380
|
+
*/
|
|
381
|
+
function decodeTallyOfStream(stream, endTime) {
|
|
382
|
+
const tally = {
|
|
383
|
+
spans: {
|
|
384
|
+
reasoning: 0,
|
|
385
|
+
text: 0,
|
|
386
|
+
toolarg: 0
|
|
387
|
+
},
|
|
388
|
+
blocks: {
|
|
389
|
+
reasoning: 0,
|
|
390
|
+
text: 0,
|
|
391
|
+
toolarg: 0
|
|
392
|
+
}
|
|
388
393
|
};
|
|
389
|
-
if (!Array.isArray(stream) || !Number.isFinite(endTime)) return
|
|
394
|
+
if (!Array.isArray(stream) || !Number.isFinite(endTime)) return tally;
|
|
390
395
|
let kind;
|
|
391
396
|
let since = 0;
|
|
392
397
|
for (const record of stream) {
|
|
@@ -397,12 +402,13 @@ function decodeSpansOfStream(stream, endTime) {
|
|
|
397
402
|
if (chunk.type !== "block-start") continue;
|
|
398
403
|
const time = r.time;
|
|
399
404
|
if (typeof time !== "number" || !Number.isFinite(time)) continue;
|
|
400
|
-
if (kind !== void 0) spans[kind] += Math.max(0, time - since);
|
|
405
|
+
if (kind !== void 0) tally.spans[kind] += Math.max(0, time - since);
|
|
401
406
|
kind = decodeKindOfBlock(chunk.blockType);
|
|
407
|
+
if (kind !== void 0) tally.blocks[kind] += 1;
|
|
402
408
|
since = time;
|
|
403
409
|
}
|
|
404
|
-
if (kind !== void 0) spans[kind] += Math.max(0, endTime - since);
|
|
405
|
-
return
|
|
410
|
+
if (kind !== void 0) tally.spans[kind] += Math.max(0, endTime - since);
|
|
411
|
+
return tally;
|
|
406
412
|
}
|
|
407
413
|
/**
|
|
408
414
|
* The first token's instant inside one PACKED delta run (`text-chunks` /
|
|
@@ -431,9 +437,8 @@ function runFirstTokenTime(record) {
|
|
|
431
437
|
}
|
|
432
438
|
/**
|
|
433
439
|
* The first token's instant inside an embedded assistant stream
|
|
434
|
-
* (`assistant/message.data.stream`, `assistant/attempt.data.stream`
|
|
435
|
-
*
|
|
436
|
-
* when the stream carries no token. Mirrors dsh-llm's
|
|
440
|
+
* (`assistant/message.data.stream`, `assistant/attempt.data.stream`), or
|
|
441
|
+
* undefined when the stream carries no token. Mirrors dsh-llm's
|
|
437
442
|
* `assistantStreamFirstTokenTime` over the compact record union.
|
|
438
443
|
*/
|
|
439
444
|
function firstTokenTimeOfStream(stream) {
|
|
@@ -453,16 +458,14 @@ function firstTokenTimeOfStream(stream) {
|
|
|
453
458
|
/**
|
|
454
459
|
* The inclusive surface range a replacement op covers, or null for `append`
|
|
455
460
|
* and for any unrecognized/hostile op (which the fold treats as an append).
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
* with one good and one malformed endpoint degrades to append.
|
|
461
|
+
* The `startSeq`/`endSeq` endpoints are each accepted only as a finite
|
|
462
|
+
* number, so a hostile op with one malformed endpoint degrades to append.
|
|
459
463
|
*/
|
|
460
464
|
function replaceRangeOf(surfaceOp) {
|
|
461
465
|
if (surfaceOp === null || typeof surfaceOp !== "object") return null;
|
|
462
466
|
const op = surfaceOp;
|
|
463
467
|
if (op.op !== "replace") return null;
|
|
464
|
-
const
|
|
465
|
-
const end = typeof op.endSeq === "number" ? op.endSeq : op.end;
|
|
468
|
+
const { startSeq: start, endSeq: end } = op;
|
|
466
469
|
if (typeof start !== "number" || !Number.isFinite(start)) return null;
|
|
467
470
|
if (typeof end !== "number" || !Number.isFinite(end)) return null;
|
|
468
471
|
return {
|
|
@@ -507,6 +510,24 @@ function kindOfCall(tool, args) {
|
|
|
507
510
|
return kindOfTool(tool);
|
|
508
511
|
}
|
|
509
512
|
/**
|
|
513
|
+
* Whether a settled call's arguments can yield an op at all: the file tools
|
|
514
|
+
* plus `str_replace_editor`, whose purpose follows its `command`. The public
|
|
515
|
+
* face of the gate {@link opsOfCall} short-circuits on — callers that decide
|
|
516
|
+
* whether to parse or to KEEP a call's raw arguments ask here instead of
|
|
517
|
+
* restating the tool list.
|
|
518
|
+
*/
|
|
519
|
+
function opBearingTool(tool) {
|
|
520
|
+
return tool === "str_replace_editor" || kindOfTool(tool) !== null;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Whether a call's raw arguments must ride the fold state
|
|
524
|
+
* (TimelineState.callNames): every op-bearing tool, plus `run_code`, whose
|
|
525
|
+
* `description` labels the ops its nested dispatches book at flush time.
|
|
526
|
+
*/
|
|
527
|
+
function rawArgsNeeded(tool) {
|
|
528
|
+
return opBearingTool(tool) || tool === "run_code";
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
510
531
|
* The operation's target path: the path-ish argument of read/write tools;
|
|
511
532
|
* for searches the narrowing `path`, else the pattern itself (a pathless
|
|
512
533
|
* grep/glob's target IS the pattern — the workspace-wide search text).
|
|
@@ -645,7 +666,7 @@ function searchFilesOf(meta) {
|
|
|
645
666
|
* resolve no target) rows nothing.
|
|
646
667
|
*/
|
|
647
668
|
function opsOfCall(input) {
|
|
648
|
-
const args = parseCallArgs(input.argsRaw);
|
|
669
|
+
const args = opBearingTool(input.tool) ? parseCallArgs(input.argsRaw) : null;
|
|
649
670
|
const kind = kindOfCall(input.tool, args);
|
|
650
671
|
if (kind === null) return [];
|
|
651
672
|
const stamp = {
|
|
@@ -724,8 +745,15 @@ function countTurnRuns(requests) {
|
|
|
724
745
|
return runs;
|
|
725
746
|
}
|
|
726
747
|
function trimState(st, bounds) {
|
|
727
|
-
|
|
728
|
-
if (st.
|
|
748
|
+
st.turnRuns = st.turnRuns ?? countTurnRuns(st.requests);
|
|
749
|
+
if (st.turnRuns > bounds.maxKeptTurns) {
|
|
750
|
+
st.requests = trimToLastTurns(st.requests, bounds.maxKeptTurns);
|
|
751
|
+
st.turnRuns = countTurnRuns(st.requests);
|
|
752
|
+
}
|
|
753
|
+
if (st.requests.length > bounds.maxRequestSteps) {
|
|
754
|
+
st.requests = st.requests.slice(-bounds.maxRequestSteps);
|
|
755
|
+
st.turnRuns = countTurnRuns(st.requests);
|
|
756
|
+
}
|
|
729
757
|
if (st.events.length > bounds.maxEvents) st.events = st.events.slice(-bounds.maxEvents);
|
|
730
758
|
if (st.fileOps.length > bounds.maxFileOps) {
|
|
731
759
|
const drop = st.fileOps.length - bounds.maxFileOps;
|
|
@@ -757,6 +785,7 @@ function createTimelineState() {
|
|
|
757
785
|
systemTokens: 0,
|
|
758
786
|
toolsTokens: 0,
|
|
759
787
|
requests: [],
|
|
788
|
+
turnRuns: 0,
|
|
760
789
|
events: [],
|
|
761
790
|
archived: [],
|
|
762
791
|
callNames: {},
|
|
@@ -867,8 +896,10 @@ function removeSurfaceSeqs(st, claimed, goneSeq) {
|
|
|
867
896
|
/**
|
|
868
897
|
* The message nested under an event payload's `message` field
|
|
869
898
|
* (`system/message`, `assistant/message`, `tool/result`) — read structurally
|
|
870
|
-
*
|
|
871
|
-
*
|
|
899
|
+
* because the fold needs a TOTAL read over untrusted payloads: a malformed
|
|
900
|
+
* message reads null here, never throws or guesses a shape. Purely a type
|
|
901
|
+
* cast: `deriveEventMessage` returns `event.data` for a user/message (no
|
|
902
|
+
* `data.message` indirection), so its result feeds every other case.
|
|
872
903
|
*/
|
|
873
904
|
function messageOf$1(data) {
|
|
874
905
|
const message = data?.message;
|
|
@@ -1118,17 +1149,32 @@ function ensureTiming(st) {
|
|
|
1118
1149
|
};
|
|
1119
1150
|
return st.timing;
|
|
1120
1151
|
}
|
|
1152
|
+
/** The TimingTotals fields each decode bucket's span and count land in. */
|
|
1153
|
+
const DECODE_FIELDS = {
|
|
1154
|
+
reasoning: {
|
|
1155
|
+
ms: "reasoningMs",
|
|
1156
|
+
n: "reasoningBlocks"
|
|
1157
|
+
},
|
|
1158
|
+
text: {
|
|
1159
|
+
ms: "textMs",
|
|
1160
|
+
n: "textBlocks"
|
|
1161
|
+
},
|
|
1162
|
+
toolarg: {
|
|
1163
|
+
ms: "toolArgMs",
|
|
1164
|
+
n: "toolArgBlocks"
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1121
1167
|
/**
|
|
1122
|
-
* Fold one block's decode span into the totals' generation split
|
|
1123
|
-
* TimingTotals). A zero span stays ABSENT — the field then
|
|
1124
|
-
* "
|
|
1125
|
-
* every pre-split-shaped state, and the card reads absence
|
|
1168
|
+
* Fold one block's decode span and count into the totals' generation split
|
|
1169
|
+
* (see TimingTotals). A zero span or count stays ABSENT — the field then
|
|
1170
|
+
* carries the "nothing was decoded/counted in this bucket" fact without adding
|
|
1171
|
+
* dead properties to every pre-split-shaped state, and the card reads absence
|
|
1172
|
+
* as 0.
|
|
1126
1173
|
*/
|
|
1127
|
-
function addDecode(timing, kind, ms) {
|
|
1128
|
-
|
|
1129
|
-
if (
|
|
1130
|
-
|
|
1131
|
-
else timing.toolArgMs = (timing.toolArgMs ?? 0) + ms;
|
|
1174
|
+
function addDecode(timing, kind, ms, blocks) {
|
|
1175
|
+
const field = DECODE_FIELDS[kind];
|
|
1176
|
+
if (ms > 0) timing[field.ms] = (timing[field.ms] ?? 0) + ms;
|
|
1177
|
+
if (blocks > 0) timing[field.n] = (timing[field.n] ?? 0) + blocks;
|
|
1132
1178
|
}
|
|
1133
1179
|
/**
|
|
1134
1180
|
* Tally one completed tool call into the per-name ranking, bounded to
|
|
@@ -1161,22 +1207,42 @@ function bumpToolTotals(timing, name, ms) {
|
|
|
1161
1207
|
ms: cur.ms + ms
|
|
1162
1208
|
};
|
|
1163
1209
|
}
|
|
1210
|
+
const CLONE_ALL = [
|
|
1211
|
+
"surface",
|
|
1212
|
+
"sums",
|
|
1213
|
+
"requests",
|
|
1214
|
+
"events",
|
|
1215
|
+
"archived",
|
|
1216
|
+
"callNames",
|
|
1217
|
+
"fileOps",
|
|
1218
|
+
"timing"
|
|
1219
|
+
];
|
|
1164
1220
|
function applyTimeline(state, event, bounds) {
|
|
1165
1221
|
let st;
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1222
|
+
/**
|
|
1223
|
+
* The event's private working state: a shallow object clone up front, with
|
|
1224
|
+
* only the named collections cloned into private copies. Everything else
|
|
1225
|
+
* stays shared with the persisted previous state — the apply contract is
|
|
1226
|
+
* "never mutate the previous state in place". Called EXACTLY ONCE per event
|
|
1227
|
+
* (each case names the collections its own path may write into; helpers
|
|
1228
|
+
* never call it), so building afresh per call is safe. The default is the
|
|
1229
|
+
* full clone — a future branch that omits its list degrades to the old
|
|
1230
|
+
* behavior instead of sharing a collection it mutates.
|
|
1231
|
+
*/
|
|
1232
|
+
const ensure = (keys = CLONE_ALL) => {
|
|
1233
|
+
st = { ...state };
|
|
1234
|
+
for (const key of keys) if (key === "surface") st.surface = [...state.surface];
|
|
1235
|
+
else if (key === "sums") st.sums = { ...state.sums };
|
|
1236
|
+
else if (key === "requests") st.requests = [...state.requests];
|
|
1237
|
+
else if (key === "events") st.events = [...state.events];
|
|
1238
|
+
else if (key === "archived") st.archived = [...state.archived];
|
|
1239
|
+
else if (key === "callNames") st.callNames = { ...state.callNames };
|
|
1240
|
+
else if (key === "fileOps") st.fileOps = [...state.fileOps];
|
|
1241
|
+
else if (state.timing !== void 0) st.timing = {
|
|
1177
1242
|
...state.timing,
|
|
1178
1243
|
tools: { ...state.timing.tools }
|
|
1179
|
-
}
|
|
1244
|
+
};
|
|
1245
|
+
return st;
|
|
1180
1246
|
};
|
|
1181
1247
|
const data = event.data;
|
|
1182
1248
|
try {
|
|
@@ -1184,22 +1250,8 @@ function applyTimeline(state, event, bounds) {
|
|
|
1184
1250
|
case "request/header": {
|
|
1185
1251
|
const header = data?.header ?? {};
|
|
1186
1252
|
const tools = Array.isArray(header.tools) ? header.tools : [];
|
|
1187
|
-
const s = ensure();
|
|
1253
|
+
const s = ensure(["events"]);
|
|
1188
1254
|
s.toolsTokens = estimateToolsTotal(tools);
|
|
1189
|
-
const systemText = header.system;
|
|
1190
|
-
if (typeof systemText === "string" && systemText !== "") {
|
|
1191
|
-
s.systems = [{
|
|
1192
|
-
seq: event.seq,
|
|
1193
|
-
time: event.time,
|
|
1194
|
-
tokens: estimateSystemTokens(systemText)
|
|
1195
|
-
}];
|
|
1196
|
-
s.systemsFromHeader = true;
|
|
1197
|
-
s.systemTokens = systemTokensOf(s.systems);
|
|
1198
|
-
} else if (s.systemsFromHeader === true) {
|
|
1199
|
-
s.systems = [];
|
|
1200
|
-
delete s.systemsFromHeader;
|
|
1201
|
-
s.systemTokens = 0;
|
|
1202
|
-
}
|
|
1203
1255
|
if (header.config && typeof header.config.model === "string") s.model = header.config.model;
|
|
1204
1256
|
if (header.config && typeof header.config.provider === "string") s.provider = header.config.provider;
|
|
1205
1257
|
if ((data?.reason === "change" || data?.reason === "resume") && s.model && s.lastModel && s.model !== s.lastModel) {
|
|
@@ -1216,7 +1268,11 @@ function applyTimeline(state, event, bounds) {
|
|
|
1216
1268
|
break;
|
|
1217
1269
|
}
|
|
1218
1270
|
case "system/message": {
|
|
1219
|
-
const s = ensure(
|
|
1271
|
+
const s = ensure([
|
|
1272
|
+
"surface",
|
|
1273
|
+
"sums",
|
|
1274
|
+
"archived"
|
|
1275
|
+
]);
|
|
1220
1276
|
delete s.pendingShadowedSeqs;
|
|
1221
1277
|
delete s.pendingShadowEventSeq;
|
|
1222
1278
|
const op = replaceRangeOf(event.surfaceOp);
|
|
@@ -1226,7 +1282,6 @@ function applyTimeline(state, event, bounds) {
|
|
|
1226
1282
|
for (const n of s.surface) if (n.seq >= op.start && n.seq <= op.end) claimed.add(n.seq);
|
|
1227
1283
|
if (removeSurfaceSeqs(s, claimed, event.seq).length > 0) bumpDetailRev(s);
|
|
1228
1284
|
}
|
|
1229
|
-
delete s.systemsFromHeader;
|
|
1230
1285
|
pushSystem(s, {
|
|
1231
1286
|
seq: event.seq,
|
|
1232
1287
|
time: event.time,
|
|
@@ -1235,7 +1290,7 @@ function applyTimeline(state, event, bounds) {
|
|
|
1235
1290
|
break;
|
|
1236
1291
|
}
|
|
1237
1292
|
case "request/context": {
|
|
1238
|
-
const s = ensure();
|
|
1293
|
+
const s = ensure([]);
|
|
1239
1294
|
if (data && typeof data.contextWindow === "number") s.contextWindow = data.contextWindow;
|
|
1240
1295
|
if (data && typeof data.model === "string") s.model = data.model;
|
|
1241
1296
|
if (data && typeof data.provider === "string") s.provider = data.provider;
|
|
@@ -1243,8 +1298,8 @@ function applyTimeline(state, event, bounds) {
|
|
|
1243
1298
|
}
|
|
1244
1299
|
case "tool/call":
|
|
1245
1300
|
if (data && typeof data.callId === "string" && typeof data.name === "string") {
|
|
1246
|
-
const s = ensure();
|
|
1247
|
-
const argsRaw = argsRawOf(data.arguments);
|
|
1301
|
+
const s = ensure(["callNames"]);
|
|
1302
|
+
const argsRaw = rawArgsNeeded(data.name) ? argsRawOf(data.arguments) : void 0;
|
|
1248
1303
|
s.callNames[data.callId] = {
|
|
1249
1304
|
name: data.name,
|
|
1250
1305
|
start: event.time,
|
|
@@ -1252,62 +1307,28 @@ function applyTimeline(state, event, bounds) {
|
|
|
1252
1307
|
};
|
|
1253
1308
|
}
|
|
1254
1309
|
break;
|
|
1255
|
-
case "tool/code-dispatch":
|
|
1256
1310
|
case "tool/ptc-dispatch": {
|
|
1257
1311
|
const rootCallId = data?.rootCallId;
|
|
1258
1312
|
const name = data?.name;
|
|
1259
1313
|
if (typeof rootCallId === "string" && typeof name === "string") {
|
|
1314
|
+
const argsRaw = opBearingTool(name) ? argsRawOf(data?.arguments) : void 0;
|
|
1260
1315
|
const ops = opsOfCall({
|
|
1261
1316
|
seq: event.seq,
|
|
1262
1317
|
time: event.time,
|
|
1263
1318
|
tool: name,
|
|
1264
|
-
argsRaw
|
|
1319
|
+
argsRaw,
|
|
1265
1320
|
err: data?.isError === true
|
|
1266
1321
|
});
|
|
1267
|
-
if (ops.length > 0) bufferCodeOps(ensure(), rootCallId, ops);
|
|
1322
|
+
if (ops.length > 0) bufferCodeOps(ensure([]), rootCallId, ops);
|
|
1268
1323
|
}
|
|
1269
1324
|
break;
|
|
1270
1325
|
}
|
|
1271
|
-
case "assistant/chunk": {
|
|
1272
|
-
const start = state.stepStart;
|
|
1273
|
-
if (start === void 0) return state;
|
|
1274
|
-
const chunk = data?.chunk;
|
|
1275
|
-
if (chunk !== null && typeof chunk === "object" && chunk.type === "block-start") {
|
|
1276
|
-
const kind = decodeKindOfBlock(chunk.blockType);
|
|
1277
|
-
if (start.block === void 0 && kind === void 0) return state;
|
|
1278
|
-
const s = ensure();
|
|
1279
|
-
const decode = { ...start.decode ?? {
|
|
1280
|
-
reasoning: 0,
|
|
1281
|
-
text: 0,
|
|
1282
|
-
toolarg: 0
|
|
1283
|
-
} };
|
|
1284
|
-
if (start.block !== void 0) decode[start.block.kind] += durOf(start.block.since, event.time);
|
|
1285
|
-
s.stepStart = {
|
|
1286
|
-
time: start.time,
|
|
1287
|
-
...start.firstToken !== void 0 ? { firstToken: start.firstToken } : {},
|
|
1288
|
-
decode,
|
|
1289
|
-
...kind !== void 0 ? { block: {
|
|
1290
|
-
kind,
|
|
1291
|
-
since: event.time
|
|
1292
|
-
} } : {}
|
|
1293
|
-
};
|
|
1294
|
-
break;
|
|
1295
|
-
}
|
|
1296
|
-
if (start.firstToken !== void 0) return state;
|
|
1297
|
-
if (!isTokenChunk(data?.chunk)) return state;
|
|
1298
|
-
const s = ensure();
|
|
1299
|
-
s.stepStart = {
|
|
1300
|
-
...start,
|
|
1301
|
-
firstToken: event.time
|
|
1302
|
-
};
|
|
1303
|
-
break;
|
|
1304
|
-
}
|
|
1305
1326
|
case "assistant/attempt": {
|
|
1306
1327
|
const start = state.stepStart;
|
|
1307
1328
|
if (start === void 0 || start.firstToken !== void 0) return state;
|
|
1308
1329
|
const first = firstTokenTimeOfStream(data?.stream);
|
|
1309
1330
|
if (first === void 0) return state;
|
|
1310
|
-
const s = ensure();
|
|
1331
|
+
const s = ensure([]);
|
|
1311
1332
|
s.stepStart = {
|
|
1312
1333
|
time: start.time,
|
|
1313
1334
|
firstToken: first
|
|
@@ -1315,21 +1336,26 @@ function applyTimeline(state, event, bounds) {
|
|
|
1315
1336
|
break;
|
|
1316
1337
|
}
|
|
1317
1338
|
case "step/start": {
|
|
1318
|
-
const s = ensure();
|
|
1339
|
+
const s = ensure([]);
|
|
1319
1340
|
s.stepStart = { time: event.time };
|
|
1320
1341
|
break;
|
|
1321
1342
|
}
|
|
1322
1343
|
case "step/end": {
|
|
1323
1344
|
const start = state.stepStart;
|
|
1324
1345
|
if (start === void 0) return state;
|
|
1325
|
-
const s = ensure();
|
|
1346
|
+
const s = ensure(["timing"]);
|
|
1326
1347
|
ensureTiming(s).wallMs += durOf(start.time, event.time);
|
|
1327
1348
|
delete s.stepStart;
|
|
1328
1349
|
break;
|
|
1329
1350
|
}
|
|
1330
1351
|
case "user/message": {
|
|
1331
1352
|
const msg = deriveEventMessage(event);
|
|
1332
|
-
const s = ensure(
|
|
1353
|
+
const s = ensure([
|
|
1354
|
+
"surface",
|
|
1355
|
+
"sums",
|
|
1356
|
+
"archived",
|
|
1357
|
+
"events"
|
|
1358
|
+
]);
|
|
1333
1359
|
bumpDetailRev(s);
|
|
1334
1360
|
const node = applySurface(s, event, event.type, data, msg);
|
|
1335
1361
|
const source = msg?.source;
|
|
@@ -1338,7 +1364,7 @@ function applyTimeline(state, event, bounds) {
|
|
|
1338
1364
|
seq: event.seq,
|
|
1339
1365
|
time: event.time,
|
|
1340
1366
|
kind: "inject",
|
|
1341
|
-
form: source.form
|
|
1367
|
+
form: typeof source.form === "string" && source.form !== "" ? source.form : "context",
|
|
1342
1368
|
tokens: node.tokens
|
|
1343
1369
|
};
|
|
1344
1370
|
if (source.kind === "skill-invocation") {
|
|
@@ -1366,7 +1392,14 @@ function applyTimeline(state, event, bounds) {
|
|
|
1366
1392
|
const blockId = firstBlock?.toolCallId;
|
|
1367
1393
|
const pendingEntry = (typeof srcId === "string" ? state.callNames[srcId] : void 0) ?? (typeof blockId === "string" ? state.callNames[blockId] : void 0);
|
|
1368
1394
|
const buffered = (typeof srcId === "string" ? state.pendingCodeOps?.[srcId] : void 0) ?? (typeof blockId === "string" ? state.pendingCodeOps?.[blockId] : void 0);
|
|
1369
|
-
const s = ensure(
|
|
1395
|
+
const s = ensure([
|
|
1396
|
+
"surface",
|
|
1397
|
+
"sums",
|
|
1398
|
+
"archived",
|
|
1399
|
+
"events",
|
|
1400
|
+
"fileOps",
|
|
1401
|
+
"timing"
|
|
1402
|
+
]);
|
|
1370
1403
|
bumpDetailRev(s);
|
|
1371
1404
|
const node = applySurface(s, event, event.type, data, toolMsg);
|
|
1372
1405
|
if (node.tool === ASK_USER_TOOL) s.humanInputs = (s.humanInputs ?? 0) + 1;
|
|
@@ -1413,7 +1446,14 @@ function applyTimeline(state, event, bounds) {
|
|
|
1413
1446
|
}
|
|
1414
1447
|
case "assistant/message": {
|
|
1415
1448
|
const usage = data?.usage;
|
|
1416
|
-
const s = ensure(
|
|
1449
|
+
const s = ensure([
|
|
1450
|
+
"surface",
|
|
1451
|
+
"sums",
|
|
1452
|
+
"archived",
|
|
1453
|
+
"events",
|
|
1454
|
+
"requests",
|
|
1455
|
+
"timing"
|
|
1456
|
+
]);
|
|
1417
1457
|
bumpDetailRev(s);
|
|
1418
1458
|
const total = s.systemTokens + s.toolsTokens + s.sums.user + s.sums.inject + s.sums.skill + s.sums.assistant + s.sums.tool;
|
|
1419
1459
|
const record = {
|
|
@@ -1449,6 +1489,8 @@ function applyTimeline(state, event, bounds) {
|
|
|
1449
1489
|
}
|
|
1450
1490
|
}
|
|
1451
1491
|
s.requests.push(record);
|
|
1492
|
+
const prevLast = state.requests.at(-1);
|
|
1493
|
+
s.turnRuns = (state.turnRuns ?? countTurnRuns(state.requests)) + (prevLast === void 0 ? record.turn !== void 0 ? 1 : 0 : record.turn === prevLast.turn ? 0 : 1);
|
|
1452
1494
|
const timing = ensureTiming(s);
|
|
1453
1495
|
timing.calls += 1;
|
|
1454
1496
|
const stepStart = state.stepStart;
|
|
@@ -1461,14 +1503,8 @@ function applyTimeline(state, event, bounds) {
|
|
|
1461
1503
|
timing.speedMs = (timing.speedMs ?? 0) + durOf(firstToken, event.time);
|
|
1462
1504
|
timing.speedTokens = (timing.speedTokens ?? 0) + output;
|
|
1463
1505
|
}
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
if (stepStart.block !== void 0) decode[stepStart.block.kind] += durOf(stepStart.block.since, event.time);
|
|
1467
|
-
for (const kind of DECODE_KINDS) addDecode(timing, kind, decode[kind]);
|
|
1468
|
-
} else {
|
|
1469
|
-
const spans = decodeSpansOfStream(data?.stream, event.time);
|
|
1470
|
-
for (const kind of DECODE_KINDS) addDecode(timing, kind, spans[kind]);
|
|
1471
|
-
}
|
|
1506
|
+
const tally = decodeTallyOfStream(data?.stream, event.time);
|
|
1507
|
+
for (const kind of DECODE_KINDS) addDecode(timing, kind, tally.spans[kind], tally.blocks[kind]);
|
|
1472
1508
|
}
|
|
1473
1509
|
}
|
|
1474
1510
|
const asstMsg = deriveEventMessage(event);
|
|
@@ -1477,7 +1513,7 @@ function applyTimeline(state, event, bounds) {
|
|
|
1477
1513
|
}
|
|
1478
1514
|
case "plan/mode":
|
|
1479
1515
|
if (data && typeof data.active === "boolean") {
|
|
1480
|
-
const s = ensure();
|
|
1516
|
+
const s = ensure(["events"]);
|
|
1481
1517
|
s.events.push({
|
|
1482
1518
|
seq: event.seq,
|
|
1483
1519
|
time: event.time,
|
|
@@ -1489,7 +1525,7 @@ function applyTimeline(state, event, bounds) {
|
|
|
1489
1525
|
break;
|
|
1490
1526
|
case "compaction/summary":
|
|
1491
1527
|
case "compaction/prune": {
|
|
1492
|
-
const s = ensure();
|
|
1528
|
+
const s = ensure(["events"]);
|
|
1493
1529
|
bumpDetailRev(s);
|
|
1494
1530
|
if (data && Array.isArray(data.shadowedSeqs)) {
|
|
1495
1531
|
s.pendingShadowedSeqs = data.shadowedSeqs.filter((x) => typeof x === "number");
|
|
@@ -2241,10 +2277,29 @@ function headerOf(record) {
|
|
|
2241
2277
|
if (typeof header.id !== "string" || header.id === "") return null;
|
|
2242
2278
|
return header;
|
|
2243
2279
|
}
|
|
2280
|
+
/** The probe keys: the projection keys the cache must already serve. */
|
|
2281
|
+
const PROBE_KEYS = ["contextActivity", "contextTimeline"];
|
|
2282
|
+
/**
|
|
2283
|
+
* Probe the cache through the face the running generation serves. The older
|
|
2284
|
+
* spelling is tried first and the newer one answers the fallback: on a
|
|
2285
|
+
* 0.1.7-rc.2 cache the 3-arg call binds the branded offset onto `keys`,
|
|
2286
|
+
* where `new Set(0)` throws as soon as a served record is reached — while a
|
|
2287
|
+
* record-less session returns `undefined` without throwing, which is the
|
|
2288
|
+
* correct "not served" either way. On 0.1.5-rc.1 the 3-arg face answers
|
|
2289
|
+
* directly; if it ever throws there, the 2-arg retry degrades to the same
|
|
2290
|
+
* "not served" verdict through the caller's catch.
|
|
2291
|
+
*/
|
|
2292
|
+
function cachedSnapshotOf(cache, header) {
|
|
2293
|
+
try {
|
|
2294
|
+
return cache.cachedSnapshot(header, SessionLogOffset(0), PROBE_KEYS);
|
|
2295
|
+
} catch {
|
|
2296
|
+
return cache.cachedSnapshot(header, PROBE_KEYS);
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2244
2299
|
/** Whether the cache already serves BOTH projection rows for this header (nothing to backfill). */
|
|
2245
2300
|
function servesRows(cache, header) {
|
|
2246
2301
|
try {
|
|
2247
|
-
const values = asRecord(asRecord(cache
|
|
2302
|
+
const values = asRecord(asRecord(cachedSnapshotOf(cache, header))?.values);
|
|
2248
2303
|
return values !== null && values.contextActivity !== void 0 && values.contextTimeline !== void 0;
|
|
2249
2304
|
} catch {
|
|
2250
2305
|
return false;
|
|
@@ -2399,18 +2454,21 @@ function watchActivityBackfill(ctx) {
|
|
|
2399
2454
|
//#endregion
|
|
2400
2455
|
//#region src/host/config.ts
|
|
2401
2456
|
/**
|
|
2402
|
-
* dsh-context
|
|
2403
|
-
* loader row in cordis.yml.
|
|
2404
|
-
*
|
|
2405
|
-
* Cordis validates the entry config against this exported `Config` schema
|
|
2406
|
-
* (any Standard Schema v1 validator — zod is ours) before `apply` runs, fills
|
|
2407
|
-
* per-field defaults, and fails the load loudly on invalid or unknown keys
|
|
2408
|
-
* (`.strict()`). The official plugin-config principle this answers: "anything
|
|
2409
|
-
* that two deployments may want to set differently is a configuration field".
|
|
2457
|
+
* dsh-context entry configuration — the `config:` block of the `dsh-context`
|
|
2458
|
+
* loader row in cordis.yml, plus the per-user display preferences.
|
|
2410
2459
|
*
|
|
2411
|
-
* The
|
|
2412
|
-
*
|
|
2413
|
-
*
|
|
2460
|
+
* The schema is schemastery so both consumers it must serve accept it: cordis
|
|
2461
|
+
* validates the entry config through the schema's Standard Schema face before
|
|
2462
|
+
* `apply` runs (defaults per field, unknown keys merged through), and the
|
|
2463
|
+
* harness's Config-form generation (dsh 0.1.7+) derives the Plugins page's
|
|
2464
|
+
* live form from this same schema — there the namespace is the entry id
|
|
2465
|
+
* (`dsh-context`) and only the `.volatile()` preference fields are served and
|
|
2466
|
+
* editable. Bounds edits remount the entry (the folds read them at apply);
|
|
2467
|
+
* preference edits commit volatile-only and never remount. On the older lines
|
|
2468
|
+
* the preferences are inert here — their surface is the registered settings
|
|
2469
|
+
* namespace (settings.ts). The `Config` type describes the RESOLVED shape
|
|
2470
|
+
* cordis hands to `apply`; the raw patch values are partial and the schema
|
|
2471
|
+
* fills every default.
|
|
2414
2472
|
*/
|
|
2415
2473
|
const DEFAULT_BOUNDS = {
|
|
2416
2474
|
maxRequestSteps: 1500,
|
|
@@ -2421,19 +2479,57 @@ const DEFAULT_BOUNDS = {
|
|
|
2421
2479
|
maxFileOps: 400
|
|
2422
2480
|
};
|
|
2423
2481
|
/**
|
|
2424
|
-
*
|
|
2425
|
-
*
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2482
|
+
* Mark a field live-editable where the harness's schemastery ships the
|
|
2483
|
+
* `.volatile()` modifier (the Config-form generation reads the mark to serve
|
|
2484
|
+
* the field on the Plugins page); a plain field on the older lines whose
|
|
2485
|
+
* schemastery predates the modifier.
|
|
2486
|
+
*/
|
|
2487
|
+
function volatileField(field) {
|
|
2488
|
+
const volatile = field.volatile;
|
|
2489
|
+
return typeof volatile === "function" ? volatile.call(field) : field;
|
|
2490
|
+
}
|
|
2491
|
+
/** One bounded positive-integer count. */
|
|
2492
|
+
function count(defaultValue) {
|
|
2493
|
+
return z$1.number().min(1).step(1).default(defaultValue);
|
|
2494
|
+
}
|
|
2495
|
+
/** The cordis `Config` validator and the Config-form generation's served schema. */
|
|
2496
|
+
const Config = z$1.object({
|
|
2497
|
+
maxRequestSteps: count(DEFAULT_BOUNDS.maxRequestSteps),
|
|
2498
|
+
maxKeptTurns: count(DEFAULT_BOUNDS.maxKeptTurns),
|
|
2499
|
+
maxEvents: count(DEFAULT_BOUNDS.maxEvents),
|
|
2500
|
+
maxNodes: count(DEFAULT_BOUNDS.maxNodes),
|
|
2501
|
+
maxArchiveNodes: count(DEFAULT_BOUNDS.maxArchiveNodes),
|
|
2502
|
+
maxFileOps: count(DEFAULT_BOUNDS.maxFileOps),
|
|
2503
|
+
defaultPlacement: volatileField(z$1.union([
|
|
2504
|
+
"all",
|
|
2505
|
+
"tab",
|
|
2506
|
+
"sidebar"
|
|
2507
|
+
]).default("all").loose()),
|
|
2508
|
+
defaultGranularity: volatileField(z$1.union(["step", "turn"]).default("step").loose()),
|
|
2509
|
+
defaultTrendMode: volatileField(z$1.union(["total", "delta"]).default("total").loose()),
|
|
2510
|
+
defaultToolSort: volatileField(z$1.union([
|
|
2511
|
+
"size",
|
|
2512
|
+
"count",
|
|
2513
|
+
"name"
|
|
2514
|
+
]).default("count").loose()),
|
|
2515
|
+
defaultFileSort: volatileField(z$1.union([
|
|
2516
|
+
"count",
|
|
2517
|
+
"latest",
|
|
2518
|
+
"path"
|
|
2519
|
+
]).default("count").loose()),
|
|
2520
|
+
insightsEntry: volatileField(z$1.union(["show", "hide"]).default("show").loose())
|
|
2521
|
+
});
|
|
2522
|
+
/** Resolve the fold's retention bounds (the schema fills every default). */
|
|
2435
2523
|
function resolveBounds(config) {
|
|
2436
|
-
|
|
2524
|
+
const resolved = Config(config ?? {});
|
|
2525
|
+
return {
|
|
2526
|
+
maxRequestSteps: resolved.maxRequestSteps,
|
|
2527
|
+
maxKeptTurns: resolved.maxKeptTurns,
|
|
2528
|
+
maxEvents: resolved.maxEvents,
|
|
2529
|
+
maxNodes: resolved.maxNodes,
|
|
2530
|
+
maxArchiveNodes: resolved.maxArchiveNodes,
|
|
2531
|
+
maxFileOps: resolved.maxFileOps
|
|
2532
|
+
};
|
|
2437
2533
|
}
|
|
2438
2534
|
//#endregion
|
|
2439
2535
|
//#region src/host/detail.ts
|
|
@@ -2548,7 +2644,7 @@ function watchDetailChannel(ctx, bounds) {
|
|
|
2548
2644
|
* (host/fallback.ts) instead of the real folds.
|
|
2549
2645
|
*/
|
|
2550
2646
|
/** The oldest supported dsh release (see the matrix note above). */
|
|
2551
|
-
const BASELINE_DSH_VERSION = "0.1.
|
|
2647
|
+
const BASELINE_DSH_VERSION = "0.1.5-rc.1";
|
|
2552
2648
|
/**
|
|
2553
2649
|
* Release-channel rank at an equal X.Y.Z: a final release outranks its
|
|
2554
2650
|
* release candidates, which outrank betas, which outrank alphas
|
|
@@ -2670,11 +2766,11 @@ const contextHeadersSchema = z.object({ headers: z.array(z.object({
|
|
|
2670
2766
|
}).strict()) }).strict();
|
|
2671
2767
|
function recordOf(event) {
|
|
2672
2768
|
if (event.type !== "request/header") return null;
|
|
2673
|
-
const rawHeader = event.data
|
|
2769
|
+
const rawHeader = event.data?.header;
|
|
2674
2770
|
if (rawHeader === null || rawHeader === void 0 || typeof rawHeader !== "object") return null;
|
|
2675
2771
|
const header = rawHeader;
|
|
2676
2772
|
const tools = Array.isArray(header.tools) ? header.tools : [];
|
|
2677
|
-
|
|
2773
|
+
return {
|
|
2678
2774
|
seq: event.seq,
|
|
2679
2775
|
time: event.time,
|
|
2680
2776
|
tools: tools.map((t) => {
|
|
@@ -2687,8 +2783,6 @@ function recordOf(event) {
|
|
|
2687
2783
|
return entry;
|
|
2688
2784
|
})
|
|
2689
2785
|
};
|
|
2690
|
-
if (typeof header.system === "string" && header.system.length > 0) record.systemTokens = estimateSystemTokens(header.system);
|
|
2691
|
-
return record;
|
|
2692
2786
|
}
|
|
2693
2787
|
/**
|
|
2694
2788
|
* The context-headers projection unit; registered alongside the timeline unit (host/index.ts); clients read it through
|
|
@@ -2887,8 +2981,11 @@ const timingTotalsSchema = z.object({
|
|
|
2887
2981
|
ttftMs: z.number().nonnegative(),
|
|
2888
2982
|
genMs: z.number().nonnegative(),
|
|
2889
2983
|
reasoningMs: z.number().nonnegative().optional(),
|
|
2984
|
+
reasoningBlocks: z.number().int().nonnegative().optional(),
|
|
2890
2985
|
textMs: z.number().nonnegative().optional(),
|
|
2986
|
+
textBlocks: z.number().int().nonnegative().optional(),
|
|
2891
2987
|
toolArgMs: z.number().nonnegative().optional(),
|
|
2988
|
+
toolArgBlocks: z.number().int().nonnegative().optional(),
|
|
2892
2989
|
speedTokens: z.number().nonnegative().optional(),
|
|
2893
2990
|
speedMs: z.number().nonnegative().optional(),
|
|
2894
2991
|
calls: z.number().int().nonnegative(),
|
|
@@ -2967,13 +3064,13 @@ const timelineStateSchema = z.object({
|
|
|
2967
3064
|
}).strict(),
|
|
2968
3065
|
systemTokens: z.number().int().nonnegative(),
|
|
2969
3066
|
systems: z.array(systemPromptNodeSchema).optional(),
|
|
2970
|
-
systemsFromHeader: z.literal(true).optional(),
|
|
2971
3067
|
toolsTokens: z.number().int().nonnegative(),
|
|
2972
3068
|
model: z.string().optional(),
|
|
2973
3069
|
provider: z.string().optional(),
|
|
2974
3070
|
lastModel: z.string().optional(),
|
|
2975
3071
|
contextWindow: z.number().optional(),
|
|
2976
3072
|
requests: z.array(requestRecordSchema),
|
|
3073
|
+
turnRuns: z.number().int().nonnegative().optional(),
|
|
2977
3074
|
events: z.array(contextEventSchema),
|
|
2978
3075
|
archived: z.array(surfaceNodeSchema),
|
|
2979
3076
|
cost: costUsageSchema.optional(),
|
|
@@ -2983,20 +3080,7 @@ const timelineStateSchema = z.object({
|
|
|
2983
3080
|
lastUser: z.string().optional(),
|
|
2984
3081
|
stepStart: z.object({
|
|
2985
3082
|
time: z.number(),
|
|
2986
|
-
firstToken: z.number().optional()
|
|
2987
|
-
decode: z.object({
|
|
2988
|
-
reasoning: z.number(),
|
|
2989
|
-
text: z.number(),
|
|
2990
|
-
toolarg: z.number()
|
|
2991
|
-
}).strict().optional(),
|
|
2992
|
-
block: z.object({
|
|
2993
|
-
kind: z.enum([
|
|
2994
|
-
"reasoning",
|
|
2995
|
-
"text",
|
|
2996
|
-
"toolarg"
|
|
2997
|
-
]),
|
|
2998
|
-
since: z.number()
|
|
2999
|
-
}).strict().optional()
|
|
3083
|
+
firstToken: z.number().optional()
|
|
3000
3084
|
}).strict().optional(),
|
|
3001
3085
|
callNames: z.record(z.string(), z.object({
|
|
3002
3086
|
name: z.string(),
|
|
@@ -3390,11 +3474,15 @@ function probeAnchor(resolve, packageNames) {
|
|
|
3390
3474
|
/**
|
|
3391
3475
|
* Whether `candidate` lies inside `root`'s tree. A `..`-prefixed relative path
|
|
3392
3476
|
* escapes it; a sibling such as `dsh-context-extra` is reached through `..` too,
|
|
3393
|
-
* so it never counts as a child.
|
|
3394
|
-
*
|
|
3477
|
+
* so it never counts as a child. On Windows, a cross-drive `relative()` returns
|
|
3478
|
+
* the candidate's ABSOLUTE path (no `..` prefix) — a different drive is never
|
|
3479
|
+
* inside this tree, so the absolute answer is rejected the same way. A
|
|
3480
|
+
* pathological child literally named `..foo` would degrade to the home anchor,
|
|
3481
|
+
* which is the safe direction.
|
|
3395
3482
|
*/
|
|
3396
3483
|
function isInside(root, candidate) {
|
|
3397
|
-
|
|
3484
|
+
const rel = relative(root, candidate);
|
|
3485
|
+
return !rel.startsWith("..") && !isAbsolute(rel);
|
|
3398
3486
|
}
|
|
3399
3487
|
/**
|
|
3400
3488
|
* The running module tree's answer, or undefined when it cannot be trusted.
|