dsh-context 0.63.0 → 0.65.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 +8 -8
- package/lib/client.js +2535 -1667
- package/lib/index.js +98 -21
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { SessionLogOffset, deriveEventMessage, interruptedTurnClosers } from "@d
|
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { dirname, isAbsolute, join, normalize, relative } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { getHeapStatistics } from "node:v8";
|
|
7
8
|
import z$1 from "@deepseek-ai/schemastery";
|
|
8
9
|
import { randomUUID } from "node:crypto";
|
|
9
10
|
//#region src/shared/providers.ts
|
|
@@ -2584,6 +2585,71 @@ function watchBalanceChannel(ctx) {
|
|
|
2584
2585
|
});
|
|
2585
2586
|
}
|
|
2586
2587
|
//#endregion
|
|
2588
|
+
//#region src/host/coldRead.ts
|
|
2589
|
+
/**
|
|
2590
|
+
* The host-wide cold-read governor (issue #121).
|
|
2591
|
+
*
|
|
2592
|
+
* Every cold read — the warm-up pass's per-session fold (backfill.ts) and
|
|
2593
|
+
* the detail route's cold rung (detail.ts) — materializes one session's
|
|
2594
|
+
* whole durable log: the persistence backend decodes every frame before it
|
|
2595
|
+
* can slice a single event, so a 10k-event session lands as a several-
|
|
2596
|
+
* hundred-MB object graph, and the harness pins its own floors on top (the
|
|
2597
|
+
* backend's two-entry decoded-log memo, the observation reader's five-entry
|
|
2598
|
+
* prepared-session cache). Those floors are the harness's to tune; what the
|
|
2599
|
+
* PLUGIN owns is how many of its cold reads run at once and whether a new
|
|
2600
|
+
* one starts on an already-hot heap. Before this gate the answer to both
|
|
2601
|
+
* was "unbounded": the dashboard's first open fires the corpus pass and the
|
|
2602
|
+
* session cards' / Agent network's per-session detail fan-out together, and
|
|
2603
|
+
* on a capped-heap host (an Electron main whose V8 ceiling cannot move)
|
|
2604
|
+
* that stacking was enough to push the process over its limit.
|
|
2605
|
+
*
|
|
2606
|
+
* The gate therefore admits one cold read at a time (FIFO) and only while
|
|
2607
|
+
* the heap leaves headroom for it. A skipped read never fails visibly: the
|
|
2608
|
+
* pass stops early (its folded rows persist; the rest refold on their next
|
|
2609
|
+
* live activity or a later process's pass) and the detail route answers its
|
|
2610
|
+
* typed null (the client keeps its last detail, the cards their no-data
|
|
2611
|
+
* note) — every degradation already designed, no functionality removed.
|
|
2612
|
+
*/
|
|
2613
|
+
/**
|
|
2614
|
+
* The high-water mark as a fraction of the process's own heap ceiling: a
|
|
2615
|
+
* cold read starts only below two-thirds full, leaving the top third for
|
|
2616
|
+
* the read itself plus the harness's floors. The ceiling is V8's real
|
|
2617
|
+
* limit, so every host class — a default-heap CLI, a capped Electron
|
|
2618
|
+
* main — gets its own mark from the same constant.
|
|
2619
|
+
*/
|
|
2620
|
+
const HIGH_WATER = 2 / 3;
|
|
2621
|
+
/** The process's own V8 reading. */
|
|
2622
|
+
function v8Reading() {
|
|
2623
|
+
const stats = getHeapStatistics();
|
|
2624
|
+
return {
|
|
2625
|
+
used: stats.used_heap_size,
|
|
2626
|
+
limit: stats.heap_size_limit
|
|
2627
|
+
};
|
|
2628
|
+
}
|
|
2629
|
+
const noop = () => {};
|
|
2630
|
+
/**
|
|
2631
|
+
* One governor per host process, shared by both cold paths. `reading` is
|
|
2632
|
+
* injectable for tests; a throwing or garbage probe fails OPEN — the meter
|
|
2633
|
+
* must never block a read (the version probe's philosophy).
|
|
2634
|
+
*/
|
|
2635
|
+
function makeColdReadGate(reading = v8Reading) {
|
|
2636
|
+
const clear = () => {
|
|
2637
|
+
try {
|
|
2638
|
+
const { used, limit } = reading();
|
|
2639
|
+
if (!Number.isFinite(used) || !Number.isFinite(limit) || limit <= 0) return true;
|
|
2640
|
+
return used < limit * HIGH_WATER;
|
|
2641
|
+
} catch {
|
|
2642
|
+
return true;
|
|
2643
|
+
}
|
|
2644
|
+
};
|
|
2645
|
+
let tail = Promise.resolve();
|
|
2646
|
+
return { admit(read) {
|
|
2647
|
+
const run = tail.then(() => clear() ? read() : void 0);
|
|
2648
|
+
tail = run.then(noop, noop);
|
|
2649
|
+
return run;
|
|
2650
|
+
} };
|
|
2651
|
+
}
|
|
2652
|
+
//#endregion
|
|
2587
2653
|
//#region src/host/backfill.ts
|
|
2588
2654
|
/** The plugin's warm-up trigger route, under the authenticated `/api` fence. */
|
|
2589
2655
|
const BACKFILL_ROUTE = "/api/dsh-context/backfill";
|
|
@@ -2702,9 +2768,12 @@ function messageOf(error) {
|
|
|
2702
2768
|
* when the dashboard first opens; the cold-path faces compose `launch`.
|
|
2703
2769
|
* Either half may land first; the first request after both launches the one
|
|
2704
2770
|
* pass, and every later request is a no-op. Returns the deferred injects'
|
|
2705
|
-
* disposer (abort on unload).
|
|
2771
|
+
* disposer (abort on unload). Every cold read runs through the shared
|
|
2772
|
+
* host-wide gate (coldRead.ts): one read at a time, and the pass stops
|
|
2773
|
+
* early when the gate skips under heap pressure — folded rows persist and
|
|
2774
|
+
* every remaining session keeps its on-demand refold paths.
|
|
2706
2775
|
*/
|
|
2707
|
-
function watchActivityBackfill(ctx) {
|
|
2776
|
+
function watchActivityBackfill(ctx, coldReads = makeColdReadGate()) {
|
|
2708
2777
|
let requested = false;
|
|
2709
2778
|
let started = false;
|
|
2710
2779
|
let launch = null;
|
|
@@ -2752,6 +2821,7 @@ function watchActivityBackfill(ctx) {
|
|
|
2752
2821
|
if (!Array.isArray(listed)) return;
|
|
2753
2822
|
let folded = 0;
|
|
2754
2823
|
let refused = 0;
|
|
2824
|
+
let paused = false;
|
|
2755
2825
|
for (const record of listed) {
|
|
2756
2826
|
if (abort.signal.aborted) return;
|
|
2757
2827
|
const header = headerOf(record);
|
|
@@ -2759,8 +2829,14 @@ function watchActivityBackfill(ctx) {
|
|
|
2759
2829
|
if (isLive(sessions, header.id)) continue;
|
|
2760
2830
|
if (servesRows(cache, header)) continue;
|
|
2761
2831
|
try {
|
|
2762
|
-
|
|
2763
|
-
|
|
2832
|
+
if (await coldReads.admit(async () => {
|
|
2833
|
+
const log = await readColdLog(persistence, header.id, abort.signal);
|
|
2834
|
+
cache.coldSnapshot(log.header, log.inheritedEventCount, log.events);
|
|
2835
|
+
return true;
|
|
2836
|
+
}) === void 0) {
|
|
2837
|
+
paused = true;
|
|
2838
|
+
break;
|
|
2839
|
+
}
|
|
2764
2840
|
folded++;
|
|
2765
2841
|
} catch (error) {
|
|
2766
2842
|
if (isUnsupportedFormat(error)) {
|
|
@@ -2771,6 +2847,7 @@ function watchActivityBackfill(ctx) {
|
|
|
2771
2847
|
await new Promise((resolve) => setTimeout(resolve, YIELD_MS));
|
|
2772
2848
|
}
|
|
2773
2849
|
if (folded > 0) ctx.logger.info(`dsh-context: projection rows backfilled for ${folded} session(s)`);
|
|
2850
|
+
if (paused) ctx.logger.info("dsh-context: projection backfill stopped early under heap pressure (remaining sessions refold on their next live activity)");
|
|
2774
2851
|
if (refused > 0) ctx.logger.info(`dsh-context: projection backfill skipped ${refused} session(s) (legacy log format is not migratable; sources left unchanged)`);
|
|
2775
2852
|
};
|
|
2776
2853
|
launch = () => {
|
|
@@ -2893,7 +2970,7 @@ function failure(code, message) {
|
|
|
2893
2970
|
* registration rides the injected fiber: either service unloading withdraws
|
|
2894
2971
|
* the route and closes the gate.
|
|
2895
2972
|
*/
|
|
2896
|
-
function watchDetailChannel(ctx, bounds) {
|
|
2973
|
+
function watchDetailChannel(ctx, bounds, coldReads = makeColdReadGate()) {
|
|
2897
2974
|
const gate = { live: false };
|
|
2898
2975
|
ctx.inject(["connection", "sessions"], (c) => {
|
|
2899
2976
|
const connection = c.get("connection");
|
|
@@ -2930,22 +3007,21 @@ function watchDetailChannel(ctx, bounds) {
|
|
|
2930
3007
|
ok: true,
|
|
2931
3008
|
value: null
|
|
2932
3009
|
});
|
|
2933
|
-
const observation = await observe(sessionId, { projectionMode: "none" });
|
|
2934
|
-
const events = observation?.events;
|
|
2935
|
-
if (!Array.isArray(events)) return reply({
|
|
2936
|
-
ok: true,
|
|
2937
|
-
value: null
|
|
2938
|
-
});
|
|
2939
|
-
let state = createTimelineState();
|
|
2940
|
-
try {
|
|
2941
|
-
for (const ev of events) state = applyTimeline(state, ev, bounds);
|
|
2942
|
-
} finally {
|
|
2943
|
-
const dispose = observation?.[Symbol.dispose];
|
|
2944
|
-
if (typeof dispose === "function") dispose.call(observation);
|
|
2945
|
-
}
|
|
2946
3010
|
return reply({
|
|
2947
3011
|
ok: true,
|
|
2948
|
-
value:
|
|
3012
|
+
value: await coldReads.admit(async () => {
|
|
3013
|
+
const observation = await observe(sessionId, { projectionMode: "none" });
|
|
3014
|
+
const events = observation?.events;
|
|
3015
|
+
if (!Array.isArray(events)) return null;
|
|
3016
|
+
let state = createTimelineState();
|
|
3017
|
+
try {
|
|
3018
|
+
for (const ev of events) state = applyTimeline(state, ev, bounds);
|
|
3019
|
+
} finally {
|
|
3020
|
+
const dispose = observation?.[Symbol.dispose];
|
|
3021
|
+
if (typeof dispose === "function") dispose.call(observation);
|
|
3022
|
+
}
|
|
3023
|
+
return buildTimelineDetail(state, bounds);
|
|
3024
|
+
}) ?? null
|
|
2949
3025
|
});
|
|
2950
3026
|
} catch (err) {
|
|
2951
3027
|
return failure("gateway/internal", err instanceof Error ? err.message : String(err));
|
|
@@ -3912,11 +3988,12 @@ function apply(ctx, config) {
|
|
|
3912
3988
|
}
|
|
3913
3989
|
const attribution = createToolAttribution(ctx);
|
|
3914
3990
|
watchStepIdentity(ctx);
|
|
3915
|
-
const
|
|
3991
|
+
const coldReads = makeColdReadGate();
|
|
3992
|
+
const gate = watchDetailChannel(ctx, resolveBounds(config), coldReads);
|
|
3916
3993
|
ctx.sessionProjections.register(createContextTimelineDefinition(config, () => gate.live));
|
|
3917
3994
|
ctx.sessionProjections.register(createContextHeadersDefinition((name) => attribution.ownerOf(name)));
|
|
3918
3995
|
ctx.sessionProjections.register(createContextActivityDefinition());
|
|
3919
|
-
watchActivityBackfill(ctx);
|
|
3996
|
+
watchActivityBackfill(ctx, coldReads);
|
|
3920
3997
|
installSettings(ctx);
|
|
3921
3998
|
}
|
|
3922
3999
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-context",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.65.0",
|
|
4
4
|
"description": "A DeepSeek Harness plugin for context insight and management, with context dashboard and context command, for understanding how the context is made of, and how it evolves.",
|
|
5
5
|
"icon": "icon.svg",
|
|
6
6
|
"author": "bowenliang123",
|